9

According to https://learn.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection the service provider should not be used until AFTER the startup has completed running. Indeed, if I try to get a registered service it will fail.

Example:

[assembly: FunctionsStartup(typeof(Startup))]

namespace Fx {
    public sealed class Startup : FunctionsStartup {
        public override void Configure(IFunctionsHostBuilder builder) {
            var configurationBuilder = new ConfigurationBuilder();
            configurationBuilder.AddEnvironmentVariables();

            var configuration = configurationBuilder.Build();

            builder.Services.AddInfrastructure(configuration);
            builder.Services.AddApplication();

            var serviceProvider = builder.Services.BuildServiceProvider();
            DependencyInjection.AddDatabase(serviceProvider).GetAwaiter().GetResult();
        }
    }
}
    public static class DependencyInjection {
        public static async Task AddDatabase(IServiceProvider services) {
            using var scope = services.CreateScope();

            var serviceProvider = scope.ServiceProvider;

            var context = serviceProvider.GetRequiredService<ApplicationDbContext>();
            //Error generated here
            if (context.Database.IsSqlServer()) {
                await context.Database.MigrateAsync();
            }
            await ApplicationDbContextSeed.SeedSamplePersonnelDataAsync(context);
        }

        public static IServiceCollection AddInfrastructure(
            this IServiceCollection services,
            IConfiguration configuration) {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"),
                    b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)));

            services.AddScoped<IApplicationDbContext>(provider => provider.GetService<ApplicationDbContext>());

            return services;
        }
    }

This produces the following error

Microsoft.EntityFrameworkCore: No database provider has been configured for this DbContext. A provider can be configured by overriding the DbContext.OnConfiguring method or by using AddDbContext on the application service provider. If AddDbContext is used, then also ensure that your DbContext type accepts a DbContextOptions<TContext> object in its constructor and passes it to the base constructor for DbContext.

Is there a good option for migrating and seeding during startup?

greven
  • 492
  • 1
  • 6
  • 15

3 Answers3

10

The easiest way I found to run code after startup was by registering a custom IWebJobsStartup by using the WebJobsStartupAttribute (the FunctionsStartupAttribute actually also inherits from this attribute). In the WebJobsStartup class you'll need to register your extension using the AddExtension where you are able to use dependency injection and seed your database. My code:

[assembly: WebJobsStartup(typeof(DbInitializationService), "DbSeeder")]

namespace Our.Database.Seeder
{
    public class DbInitializationService : IWebJobsStartup
    {
        public void Configure(IWebJobsBuilder builder)
        {
            builder.AddExtension<DbSeedConfigProvider>();
        }
    }

    [Extension("DbSeed")]
    internal class DbSeedConfigProvider : IExtensionConfigProvider
    {
        private readonly IServiceScopeFactory _scopeFactory;

        public DbSeedConfigProvider(IServiceScopeFactory scopeFactory)
        {
            _scopeFactory = scopeFactory;
        }

        public void Initialize(ExtensionConfigContext context)
        {
            using var scope = _scopeFactory.CreateScope();
            var dbContext = scope.ServiceProvider.GetService<YourDbContext>();

            dbContext.Database.EnsureCreated();
            // Further DB seeding, etc.
        }
    }
}
Martijn Lentink
  • 505
  • 6
  • 9
4

According to your code, I assume that you're building something aligned to the CleanArchitecture Repository on Github. https://github.com/jasontaylordev/CleanArchitecture

The main difference between this repo and your apporach, is that you're obviously not using ASP.NET, which is not a problem at all, but requires a little bit more configuration work.

The article already mentioned (https://markheath.net/post/ef-core-di-azure-functions) refers another blogpost (https://dev.to/azure/using-entity-framework-with-azure-functions-50aa), which briefly explains that EntityFramework Migrations are not capable of auto-discovering your migrations in an Azure Function. Therefore, you need to implement an instance of IDesignTimeDbContextFactory. I also stumbled upon it in the microsoft docs: https://learn.microsoft.com/en-us/ef/core/cli/dbcontext-creation?tabs=dotnet-core-cli#from-a-design-time-factory

You could for example place it inside your Infrastructure\Persistence\Configurations folder. (Once again, I'm only assuming that you're following the CleanArchitecture repo structure)

EimerReis
  • 827
  • 6
  • 11
  • I had similar issues setting up EFcore migrations for a shared projects dbcontext in website as the app settings(connectionString) were null. Added a new class which is run automagically by the migrations. `class AdminPortalDbContextFactory : IDesignTimeDbContextFactory { public AdminPortalDbContext CreateDbContext(string[] args) { // setup connectionString` – Tyeth Feb 02 '21 at 11:20
1

DI in AZURE Functions

Caveats

A series of registration steps run before and after the runtime processes the startup class. Therefore, keep in mind the following items:

The startup class is meant for only setup and registration. Avoid using services registered at startup during the startup process. For instance, don't try to log a message in a logger that is being registered during startup. This point of the registration process is too early for your services to be available for use. After the Configure method is run, the Functions runtime continues to register additional dependencies, which can affect how your services operate.

The dependency injection container only holds explicitly registered types. The only services available as injectable types are what are setup in the Configure method. As a result, Functions-specific types like BindingContext and ExecutionContext aren't available during setup or as injectable types

SKARVA Bodavula
  • 903
  • 11
  • 17