Where to put database initialisation code in Maui App - c#

I have created a .net standard class library service for accessing a sqlite database in my new Maui app. My question is where to call the initialisation code. I've added the DI registration in MauiProgram.cs which registers my DbService as the implementation of IDbService interface:
builder
.Services
.AddSingleton<IDbService>(serviceProvider =>
ActivatorUtilities.CreateInstance<DbService>(serviceProvider, databasePath))
.AddSingleton<MainViewModel>()
.AddSingleton<MainPage>();
The code to initialise the database (create tables, load test data) I've currently put in the constructor for the main page viewmodel which is registered as a singleton so the initialisation will only occur once. But obviously calling async initialisation code in the constructor is just wrong. Where is the correct location for this?
Task.Run(async () =>
{
await _dbService.Initialise();
if (!(await _dbService.GetExperiences(1, 0)).Any())
await _dbService.LoadTestData();
await GetData();
}).GetAwaiter().GetResult();

For custom startup logic, usually hosted services are the way to go. But MAUI does not currently support hosted services. However, there is an undocumented IMauiInitializeService interface that can be used to implement initialization logic.
internal class DatabaseInitializer : IMauiInitializeService
{
public void Initialize(IServiceProvider services)
{
var dbService = services.GetRequiredService<IDbService>();
Task.Run(async () =>
{
await dbService.Initialise();
if (!(await dbService.GetExperiences(1, 0)).Any())
await dbService.LoadTestData();
await GetData();
}).GetAwaiter().GetResult();
}
}
This class needs to be registered as an implementation of IMauiInitiailizeService:
builder.Services;
.AddSingleton<IDbService>(serviceProvider =>
ActivatorUtilities.CreateInstance<DbService>(serviceProvider, databasePath))
.AddSingleton<MainViewModel>()
.AddSingleton<MainPage>()
.AddTransient<IMauiInitializeService, DatabaseInitializer>();
It will be executed after the application is built, here.
It should work by the looks of things. Currently, I don't have MAUI installed so I can't verify for sure. Please let me know if there is a problem.

I'm a MAUI beginner so apologies if this is not best practice/helpful.
My MAUI app is set up with a local SQLite DB using EF Core code first migrations.
In App.xaml.cs, I'm using the service provider to create a new scope that gets the DB context. The DB context then applies migrations and adds seed/test data etc.
The following seems to work for me, perhaps you could do something similar using your dbService in place of the ctx...
public partial class App : Application
{
private readonly IServiceProvider _serviceProvider;
public App(IServiceProvider serviceProvider)
{
InitializeComponent();
_serviceProvider = serviceProvider;
AddTestData().Wait();
MainPage = new AppShell();
}
public async Task AddTestData()
{
using(var scope = _serviceProvider.CreateScope())
{
await using var ctx = scope.ServiceProvider.GetRequiredService<MyDbContext>();
await ctx.Database.MigrateAsync();
// Use ctx to add test/seed data etc
await ctx.SaveChangesAsync();
}
}
}

public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
var configurationBuilder = new ConfigurationBuilder()
.AddJsonFile($"appsettings.json", true, true);
IConfiguration configuration = configurationBuilder.Build();
var sqlServerConnectionString = configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<AppDbContext>(x => x.UseSqlServer(sqlServerConnectionString));
#if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools();
#endif
builder.Services.AddSingleton<WeatherForecastService>();
// Update Databases when app started
using (ServiceProvider serviceProvider = builder.Services.BuildServiceProvider())
{
AppDbContext dbContext = serviceProvider.GetRequiredService<AppDbContext>();
dbContext.Database.Migrate();
}
return builder.Build();
}
}

Related

How to instantiate DbContext from IHostingService

I have a class that derives from BackgroundService (IHostedService) for running background tasks. This will be added to my services using builder.Services.AddHostedService<BackgroundTaskService>()
BackgroundService's task runs for the entire duration of the web application, checking for queued data to process.
My question is, how do I instantiate an instance of DbContext from this code?
I could have the BackgroundTaskService constructor accept a DbContext. But wouldn't that keep the DbContext open forever?
And how else could I instantiate it without duplicating all the code to scan my settings file for the connection string, etc.?
The recemmended approach is to inject IDbContextFactory<TContext> as described in the following article: Using a DbContext factory (e.g. for Blazor)
Some application types (e.g. ASP.NET Core Blazor) use dependency injection but do not create a service scope that aligns with the desired DbContext lifetime. Even where such an alignment does exist, the application may need to perform multiple units-of-work within this scope. For example, multiple units-of-work within a single HTTP request.
In these cases, AddDbContextFactory can be used to register a factory for creation of DbContext instances.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContextFactory<ApplicationDbContext>(
options =>
options.UseSqlServer(#"Server=(localdb)\mssqllocaldb;Database=Test"));
}
Then in your controller:
private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;
public MyController(IDbContextFactory<ApplicationDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public void DoSomething()
{
using (var context = _contextFactory.CreateDbContext())
{
// ...
}
}
You can use scope service factory. Check here for reference.
Here you have an example:
// Injection
public class DataApi : BackgroundService
{
private readonly ILogger<DataApi> logger;
private readonly IServiceScopeFactory scopeFactory;
public DataApi(ILogger<DataApi> _logger, IConfiguration _cfg, IServiceScopeFactory _sSF)
{
logger = _logger;
scopeFactory = _sSF;
// e.g. data from appsettings.json
// var recovery = _cfg["Api:Recovery"];
}
// ...
// Usage
protected async Task DataCollector()
{
logger.LogInformation("Collector");
using (var scope = scopeFactory.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<MyDbContext>();
var myList = await db.MyEntity
.AsNoTracking()
.Where(t => t.active)
.ToListAsync();
if (myList.Count == 0)
{
logger.LogInformation("Empty...");
return;
}
// logic...
}
await Task.CompletedTask;
}

How to configure Logging via DI in Azure WebJob

I am working on Azure WebJobs (3.0.6) using dotnet core. I referred Microsoft's Get Started Guide. Per the example I wanted to have a console logging to begin with. The scope of that guide is limited. In my application, I will be using many classes in a different dll. I am not able to figure out how can I add logging in those classes. The sample code is
// The main method
static async Task Main()
{
var builder = new HostBuilder();
builder.ConfigureWebJobs(b =>
{
b.AddAzureStorageCoreServices();
b.AddAzureStorage();
});
builder.ConfigureLogging((context, b) =>
{
b.AddConsole();
});
var host = builder.Build();
using (host)
{
await host.RunAsync();
}
}
// The queue trigger class
public class QueueListenerService
{
public static void QueueListener([QueueTrigger("myqueue")] string message, ILogger logger)
{
logger.LogInformation("The logger works here");
// how can I pass an instance of ILogger in the constructor below
MyWorker myWorker = new MyWorker();
}
}
// MyWorker Class in a different assembly
public class MyWorker
{
public MyWorker(ILogger logger)
{
// I want to use logger here being injected
}
}
I have referred several examples of DI in dotnet core console applications and they use service collection approach. I also check this blog but to me, this is what I have done and yet my ILogger is not being resolved. It ask me to pass an instance of ILogger when I create MyWorker instance
You are close to the solution. The main thing you need to change is to let the service collection create the MyWorker instance for you.
I quickly extended my recent Webjob sample project to include console logging with dependency injection. See this commit for how I added it.
You mainly need to use constructor dependency injection for your QueueListenerService.
builder.ConfigureServices(services =>
{
services.AddScoped<QueueListenerService>();
services.AddScoped<MyWorker>();
});
public class QueueListenerService
{
public QueueListenerService(MyWorker worker){
_Worker = worker;
}
public static void QueueListener([QueueTrigger("myqueue")] string message, ILogger logger)
{
logger.LogInformation("The logger works here");
_Worker.DoStuff()
}
}

How to I access the DbContext of EF core from another project when used in ASP.NET core?

I followed the pattern to use EF Core with ASP.NET core and all is well. But recently I created a 'Calculation' project and want to make database calls from it.
The problem is I don't know how to create a new DbContextOptions. In my code that is done with
services.AddDbContext<RetContext>(options => options
.UseLazyLoadingProxies()
.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
But in a new .NET core class I need to provide it manually. How do I do this ? My code is like this:
public static class LoadData
{
public static IConfiguration Configuration { get; }
public static RefProgramProfileData Load_RefProgramProfileData(string code)
{
// var optionsBuilder = new DbContextOptionsBuilder<RetContext>();
// optionsBuilder.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
//How do I make an optionsbuilder and get the configuration from the WEB project?
UnitOfWork uow = new UnitOfWork(new RetContext(optionsBuilder));
var loadedRefProgramProfileData = uow.RefProgramProfileDataRepository
.Find(x => x.ProgramCode == code).FirstOrDefault();
return loadedRefProgramProfileData;
}
}
You may instantiate your DbContext like this:
var builder = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json");
var configuration = builder.Build();
var optionsBuilder = new DbContextOptionsBuilder<RetContext>();
optionsBuilder.UseSqlServer(configuration.GetConnection("DefaultConnection"));
_context = new RetContext(optionsBuilder.Options);
However, the ideal is to use dependency injection. Let's say you have a class CalculationService in your other project. For that, you need to register that class as a service that can be injected:
services.AddScoped<CalculationService>();
Then your class can receive DbContext (or any other services) through DI:
public class CalculationService
{
private RetContext _context;
public CalculationService(RetContext context)
{
_context = context;
}
}
Naturally, you won't be able to instantiate your class manually like this:
var service = new CalculationService();
Instead, you'd need to make whatever class needs to use your CalculationService to also receive it through DI and make that class injectable as well.

Reconfigure dependencies when Integration testing ASP.NET Core Web API and EF Core

I'm following this tutorial
Integration Testing with Entity Framework Core and SQL Server
My code looks like this
Integration Test Class
public class ControllerRequestsShould : IDisposable
{
private readonly TestServer _server;
private readonly HttpClient _client;
private readonly YourContext _context;
public ControllerRequestsShould()
{
// Arrange
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkSqlServer()
.BuildServiceProvider();
var builder = new DbContextOptionsBuilder<YourContext>();
builder.UseSqlServer($"Server=(localdb)\\mssqllocaldb;Database=your_db_{Guid.NewGuid()};Trusted_Connection=True;MultipleActiveResultSets=true")
.UseInternalServiceProvider(serviceProvider);
_context = new YourContext(builder.Options);
_context.Database.Migrate();
_server = new TestServer(new WebHostBuilder()
.UseStartup<Startup>()
.UseEnvironment(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")));
_client = _server.CreateClient();
}
[Fact]
public async Task ReturnListOfObjectDtos()
{
// Arrange database data
_context.ObjectDbSet.Add(new ObjectEntity{ Id = 1, Code = "PTF0001", Name = "Portfolio One" });
_context.ObjectDbSet.Add(new ObjectEntity{ Id = 2, Code = "PTF0002", Name = "Portfolio Two" });
// Act
var response = await _client.GetAsync("/api/route");
response.EnsureSuccessStatusCode();
// Assert
var result = Assert.IsType<OkResult>(response);
}
public void Dispose()
{
_context.Dispose();
}
As I understand it, the .UseStartUp method ensures the TestServer uses my startup class
The issue I'm having is that when my Act statement is hit
var response = await _client.GetAsync("/api/route");
I get an error in my startup class that the connection string is null. I think My understanding of the problem is that when my controller is hit from the client it injects my data repository, which in turn injects the db context.
I think I need to configure the service as part of the new WebHostBuilder section so that it used the context created in the test. But I'm not sure how to do this.
ConfigureServices method in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Add framework services
services.AddMvc(setupAction =>
{
setupAction.ReturnHttpNotAcceptable = true;
setupAction.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
setupAction.InputFormatters.Add(new XmlDataContractSerializerInputFormatter());
});
// Db context configuration
var connectionString = Configuration["ConnectionStrings:YourConnectionString"];
services.AddDbContext<YourContext>(options => options.UseSqlServer(connectionString));
// Register services for dependency injection
services.AddScoped<IYourRepository, YourRepository>();
}
#ilya-chumakov's answer is awesome. I just would like to add one more option
3. Use ConfigureTestServices method from WebHostBuilderExtensions.
The method ConfigureTestServices is available in the Microsoft.AspNetCore.TestHost version 2.1(on 20.05.2018 it is RC1-final). And it lets us override existing registrations with mocks.
The code:
_server = new TestServer(new WebHostBuilder()
.UseStartup<Startup>()
.ConfigureTestServices(services =>
{
services.AddTransient<IFooService, MockService>();
})
);
Here are two options:
1. Use WebHostBuilder.ConfigureServices
Use WebHostBuilder.ConfigureServices together with WebHostBuilder.UseStartup<T> to override and mock a web application`s DI registrations:
_server = new TestServer(new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddScoped<IFooService, MockService>();
})
.UseStartup<Startup>()
);
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
//use TryAdd to support mocking IFooService
services.TryAddTransient<IFooService, FooService>();
}
}
The key point here is to use TryAdd methods inside the original Startup class. Custom WebHostBuilder.ConfigureServices is called before the original Startup, so the mocks are registered before the original services. TryAdd doesn't do anything if the same interface has already been registered, thus the real services will not be even touched.
More info: Running Integration Tests For ASP.NET Core Apps.
2. Inheritance / new Startup class
Create TestStartup class to re-configure ASP.NET Core DI. You can inherit it from Startup and override only needed methods:
public class TestStartup : Startup
{
public TestStartup(IHostingEnvironment env) : base(env) { }
public override void ConfigureServices(IServiceCollection services)
{
//mock DbContext and any other dependencies here
}
}
Alternatively TestStartup can be created from scratch to keep testing cleaner.
And specify it in UseStartup to run the test server:
_server = new TestServer(new WebHostBuilder().UseStartup<TestStartup>());
This is a complete large example: Integration testing your asp .net core app with an in memory database.

Chicken and egg issue implementing Autofac with NServicebus

Background
I'm trying to set up a Web API 2 which needs to communicate to a NServicebus Endpoint.
I will need to implement IoC, which will be done using Autofac.
What I have
A controller defined like so:
[RoutePrefix("api")]
public class Controller : ApiController
{
private IEndpointInstance EndpointInstance { get; set; }
public public MyController(IEndpointInstance endpointInstance)
{
this.EndpointInstance = endpointInstance;
}
[HttpGet]
[Route("dostuff")]
public async Task DoStuff()
{
var command = new MyCommand
{
...
};
await this.EndpointInstance.SendLocal(command);
}
}
And in global.asax
Application_Start
protected async void Application_Start()
{
GlobalConfiguration.Configure(WebApiConfig.Register);
await RegisterNServiceBusWithAutofac();
}
RegisterNServiceBusWithAutofac
private async Task RegisterNServiceBusWithAutofac()
{
var builder = new ContainerBuilder();
var endpointConfiguration = await GetEndpointConfiguration("My.Service");
var endpointInstance = await Endpoint.Start(endpointConfiguration);
builder.RegisterInstance(endpointInstance);
var container = builder.Build();
endpointConfiguration.UseContainer<AutofacBuilder>(c => c.ExistingLifetimeScope(container));
builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
}
GetEndpointConfiguration
private static async Task<EndpointConfiguration> GetEndpointConfiguration(string name)
{
var endpointConfiguration = new EndpointConfiguration(name);
// Set transport.
var routing = endpointConfiguration.UseTransport<MsmqTransport>().Routing();
// Register publish to self
routing.RegisterPublisher(typeof(EventHasFinished), name);
endpointConfiguration.UseSerialization<JsonSerializer>();
endpointConfiguration.UsePersistence<InMemoryPersistence>();
endpointConfiguration.SendFailedMessagesTo("error");
endpointConfiguration.EnableInstallers();
return endpointConfiguration;
}
The result
I get the following error on the UseContainer line:
Unable to set the value for key:
NServiceBus.AutofacBuilder+LifetimeScopeHolder. The settings has been
locked for modifications. Move any configuration code earlier in the
configuration pipeline
What I think this means
I think I need to do all Autofac registrations for the NServicebus when creating the endpointConfiguration. The above manipulates the builder instance after that.
But
I can't do the above, because I need to register the endpointinstance to the IoC, because I need that in my controller to send messages. And that doesn't exist yet, because I need the endpointConfiguration first, for that.
So I have a chicken and egg situation ...
Question
Do I understand the issue correctly and how can I solve it while
making sure that IoC works correctly for the Controller?
I.e.: this.EndpointInstance has been correctly instantiated through IoC.
Instead of registering the actual instance, you could register it with a lambda expression that is going to be executed the first time the container will be asked to resolve IEndpointInstance.
builder
.Register(x =>
{
var endpointConfiguration = GetEndpointConfiguration("My.Service").GetAwaiter().GetResult();
var endpointInstance = Endpoint.Start(endpointConfiguration).GetAwaiter().GetResult();
return endpointInstance
})
.As<IEndpointInstance>()
.SingleInstance();

Categories

Resources