Search code examples
c#asp.net-core.net-coreentity-framework-coreidentityserver4

ASP.NET Core - Dependency Injection - IdentityServer4 DbContext


I've been trying to find what the best/preferred way is to have my RoleService obtain a ConfigurationDbContext (IdentityServer4).

I would really like to decouple so that my RoleService can be testable.

The only way I found get access to the ConfigurationDbContext is via creation of a public static IServiceProvider in Startup.cs:

public class Startup
{
    private readonly IHostingEnvironment _environment;

    // THIS IS THE PROPERTY I USED
    public static IServiceProvider ServiceProvider { get; private set; }

    public ConfigurationDbContext GetConfigurationDbContext()
    {
        return null;
    }

    public Startup(ILoggerFactory loggerFactory, IHostingEnvironment environment)
    {
        loggerFactory.AddConsole(LogLevel.Debug);
        _environment = environment;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        var connectionString = DbSettings.IdentityServerConnectionString;
        var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;

        // configure identity server with in-memory stores, keys, clients and scopes
        var identityServerConfig = services.AddIdentityServer()
            .AddConfigurationStore(builder =>
                builder.UseSqlServer(connectionString, options =>
                    options.MigrationsAssembly(migrationsAssembly)))
            .AddOperationalStore(builder =>
                builder.UseSqlServer(connectionString, options =>
                    options.MigrationsAssembly(migrationsAssembly)))
            .AddSigningCredential(new X509Certificate2(Path.Combine(_environment.ContentRootPath, "certs", "IdentityServer4Auth.pfx"), "test"));

        identityServerConfig.Services.AddTransient<IResourceOwnerPasswordValidator, ActiveDirectoryPasswordValidator>();
        identityServerConfig.Services.AddTransient<IProfileService, CustomProfileService>();
        services.AddDbContext<ConfigurationDbContext>(options => options.UseSqlServer(connectionString));
        services.AddMvc();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        ServiceProvider = app.ApplicationServices;
        // other code emitted
    }
}

And then utilizing this code in RoleService.cs:

public class RoleService : IRoleService
{
    public async Task<ApiResource[]> GetApiResourcesByIds(int[] ids)
    {
        ApiResource[] result;

        using (var serviceScope = Startup.ServiceProvider.GetService<IServiceScopeFactory>().CreateScope())
        {
            var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
            result =
                context.ApiResources.Where(x => ids.Contains(x.Id))
                .Include(x => x.Scopes)
                .Include(x => x.UserClaims)
                .ToArray();

            return result;
        }
    }
}

Is this the best way to get a dependency in RoleService.cs?

Is there a way to abstract the serviceScope (since it is in a using statement, and probably IDisposable, I don't really know if there is a way to abstract obtaining a context?

Any other recommendations or best practices?


Solution

  • You can pass a dependency to the RoleService by creating a constructor with the desired dependency as a constructor parameter.

    Depending on the lifetime of your RoleService (can't tell based on the provided code) you would pass either the ConfigurationDbContext directly to the RoleService if the service is already Scoped. Or you would pass the IServiceScopeFactory to create a scope manually in your service.

    Something like:

    private readonly IServiceScopeFactory _scopeFactory;
    
    public RoleService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }
    
    public async Task<ApiResource[]> GetApiResourcesByIds(int[] ids)
    {
        using (var scope = _scopeFactory.CreateScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
        }
    }
    

    Since your method is thight coupled to the DbContext (which isn't necessary a bad thing) you would probably end up creating an in-memory ConfigurationDbContext. The IdentityServer4.EntityFramework repository that you use has some examples for testing that here.