Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow services to be easily resolved from the application service provider from within EF Core #13540

Closed
Tracked by #22961
chernihiv opened this issue Oct 8, 2018 · 7 comments · Fixed by #29950
Closed
Tracked by #22961
Labels
area-global closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported needs-design punted-for-3.0 type-enhancement
Milestone

Comments

@chernihiv
Copy link

chernihiv commented Oct 8, 2018

I try to resolve CustomMigrationsSqlGenerator with IMyCustomService but it throws an exception.

I have overridden partially logic inside SqlServerMigrationsSqlGenerator, but for my goal I need to resolve extra service inside, e.g:

class CustomMigrationsSqlGenerator : SqlServerMigrationsSqlGenerator
{
    public CustomMigrationsSqlGenerator (
        IMyCustomService service,
        MigrationsSqlGeneratorDependencies dependencies,
        IMigrationsAnnotationProvider migrationsAnnotations)
        : base(dependencies, migrationsAnnotations)  { }

}

it throws an exception
System.InvalidOperationException: Unable to resolve service for type 'IMyCustomService' while attempting to activate 'CustomMigrationsSqlGenerator'.

IMyCustomService is registered as:

...
services.AddScoped<IMyCustomService, MyCustomService>();
...

Further technical details

EF Core: Microsoft.EntityFrameworkCore 2.1.4
Database Provider: Microsoft.EntityFrameworkCore.SqlServer 2.1.4
Operating system: Windows 10 Pro 1803, 17134.285
IDE: Visual Studio 2017 15.8.6

@chernihiv chernihiv changed the title Unable to resolve service for type '' while attempting to activate 'CustomMigrationsSqlGenerator' Unable to resolve service for type 'IMyCustomService' while attempting to activate 'CustomMigrationsSqlGenerator' Oct 8, 2018
@ajcvickers
Copy link
Contributor

@chernihiv Could you show the code for setting up the service provider(s)?

@chernihiv
Copy link
Author

chernihiv commented Oct 8, 2018

@ajcvickers Sure, I've built from scratch, empty solution.

Here is all configuration:

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IMyCustomService, MyCustomService>();
            services.AddDbContext<InitContext>(item => item
                .UseSqlServer(@"Data Source=.\SQLEXPRESS;Initial Catalog=Test1;Integrated Security=True;Persist Security Info=False;MultipleActiveResultSets=True")
                .ReplaceService<IMigrationsSqlGenerator, CustomMigrationsSqlGenerator>());

            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();            
            app.UseMvc();
        }
    }

    public class InitContext : DbContext
    {
        public InitContext(DbContextOptions<InitContext> options) : base(options)
        {
        }
        public DbSet<User> Users { get; set; }
    }
    
    public class User
    {
        public int Id { get; set; }
    }

    internal class CustomMigrationsSqlGenerator : SqlServerMigrationsSqlGenerator
    {
        public CustomMigrationsSqlGenerator(
            IMyCustomService myCustomService,
            MigrationsSqlGeneratorDependencies dependencies,
            IMigrationsAnnotationProvider migrationsAnnotations)
            : base(dependencies, migrationsAnnotations) { }

    }

    internal class MyCustomService : IMyCustomService
    {
        public string Test()
        {
            return "test";
        }
    }

    public interface IMyCustomService
    {
        string Test();
    }

Here is controller for testing. Don't forget to create Init migration.

[Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private readonly InitContext _context;
        private readonly IMyCustomService _myCustomService;
        public ValuesController(InitContext context, IMyCustomService myCustomService)
        {
            _context = context;
            _myCustomService = myCustomService;
        }

        [HttpGet("/test/users")]
        public async Task<IEnumerable<User>> TestUsers()
        {
            return await _context.Users.ToListAsync();
        }

        [HttpGet("/migrate")]
        public async Task Migrate()
        {
            await _context.Database.MigrateAsync();
        }

        [HttpGet("/test/service")]
        public ActionResult<string> TestService()
        {
            return _myCustomService.Test();
        }
    }

Without IMyCustomService in CustomMigrationsSqlGenerator you are able to Migrate and use all APIs, but with IMyCustomService service you should receive the following exception:

InvalidOperationException: Unable to resolve service for type 'WebApplication4.IMyCustomService' while attempting to activate 'WebApplication4.CustomMigrationsSqlGenerator'.

System.InvalidOperationException: Unable to resolve service for type 'WebApplication4.IMyCustomService' while attempting to activate 'WebApplication4.CustomMigrationsSqlGenerator'.
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type serviceType, Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(Type serviceType, Type implementationType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, Type serviceType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(Type serviceType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(Type serviceType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type serviceType, Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(Type serviceType, Type implementationType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, Type serviceType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(Type serviceType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(Type serviceType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type serviceType, Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(Type serviceType, Type implementationType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, Type serviceType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(Type serviceType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(Type serviceType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type serviceType, Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(Type serviceType, Type implementationType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, Type serviceType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(Type serviceType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(Type serviceType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type serviceType, Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(Type serviceType, Type implementationType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, Type serviceType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(Type serviceType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(Type serviceType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type serviceType, Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(Type serviceType, Type implementationType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, Type serviceType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(Type serviceType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(Type serviceType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.CreateServiceAccessor(Type serviceType)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T](IServiceProvider provider)
   at Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.GetRelationalService[TService](IInfrastructure`1 databaseFacade)
   at Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.MigrateAsync(DatabaseFacade databaseFacade, CancellationToken cancellationToken)
   at WebApplication4.Controllers.ValuesController.Migrate() in C:\Users\Сергій\source\repos\WebApplication4\WebApplication4\Controllers\ValuesController.cs:line 29
   at lambda_method(Closure , Object )
   at Microsoft.Extensions.Internal.ObjectMethodExecutorAwaitable.Awaiter.GetResult()
   at Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor.AwaitableResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at System.Threading.Tasks.ValueTask`1.get_Result()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeActionMethodAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync()
   at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

@ajcvickers
Copy link
Contributor

Notes for triage: there are several things going on here:

  • IMigrationsSqlGenerator is a singleton service and so cannot depend on a scoped custom service. This is the way D.I, works, so going forward I'm assuming the custom service is registered as a singleton.
  • The custom service is registered in the application's service provider, but IMigrationsSqlGenerator is resolved from the internal service provider
  • The normal pattern recommended here is to use the DbContext as a service locator since it has a bridge to the application service provider, but this pattern doesn't work for singleton services since the DbContext itself cannot be obtained in a singleton service

So this leaves taking over the internal service provider as the only solution, which is a bit drastic for just adding a singleton dependency to a singleton service.

@chernihiv
Copy link
Author

chernihiv commented Oct 9, 2018

  1. It doesn't work even when custom service is registered as singleton;
  2. I'm trying to register it within internal service provider, maybe I do it incorrectly, therefore it doesn't work (My IMyCustomService service resolved IConfiguration inside which hadn't been registered);
var efServiceProvider = new ServiceCollection()
                .AddEntityFrameworkSqlServer()
                .AddScoped<IMyCustomService, MyCustomService>()
                .AddScoped<IMigrationsSqlGenerator, CustomSqlServerMigrationsSqlGenerator>() 
                .BuildServiceProvider();

builder.UseSqlServer(dbConfiguration.Connection).UseInternalServiceProvider(efServiceProvider);

  1. I have thought about it, but it's anti-pattern.

BTW, what I am going to achieve is multi tenancy. I have generated migrations for several DB providers, but it has predefined table names and related. In my case, each client (user) will have unique prefix and I'd like to append this prefix to each table dynamically. Therefore, each client (user) will have own independent tables which starts from client's prefix.
One of possible ways are:

  1. Create table per all clients, which will contain TenantId, but it's bottleneck in future;
  2. Create schema per client, but I'm not sure that all db providers support this.

Could you assist?
Maybe there are other ways to append prefix to each table, maybe you know other ways, maybe solution with schemas will work, etc..

@ajcvickers
Copy link
Contributor

Notes from triage: we should consider allow registering an explicit bridge service in the internal service provider such that both singleton services can be registered and so that constructor injection can be used to resolve these in replaced services.

@chernihiv Overriding the MigrationsSqlGenerator is not the way to do this. It would allow different databases to be created, but would not allow them to be used with the underlying EF model. Instead consider dynamically building different models based on the tenant. Controlling when the different models are used can be done with a ModelCacheKey

@ajcvickers ajcvickers changed the title Unable to resolve service for type 'IMyCustomService' while attempting to activate 'CustomMigrationsSqlGenerator' Allow singleton services to be resolved in the internal service provider Oct 12, 2018
@ajcvickers ajcvickers added this to the 3.0.0 milestone Oct 12, 2018
@ajcvickers ajcvickers self-assigned this Oct 12, 2018
@chernihiv
Copy link
Author

@ajcvickers Sure, I do similar things in the same way, but instead schema I change table name. It works.

But, what if there is new Tenant? Tenant could specify any DB Provider and Connection he wish. What I need is to apply migrations to this database and seed it with data.

I have common clear migrations, it knows nothing about tenant and I should dynamically change schema or tableprefix and migrate it for new Tenant. Therefore, I need to override IMigrationsSqlGenerator for all supported db providers and change on spot schema or table name.

@ajcvickers ajcvickers modified the milestones: 3.0.0, Backlog May 27, 2019
@ajcvickers ajcvickers changed the title Allow singleton services to be resolved in the internal service provider Allow services to be easily resolved from the application service provider from within EF Core Dec 8, 2020
@ajcvickers
Copy link
Contributor

See also the scenarios and suggestions in #23559 when working on this issue.

ajcvickers added a commit that referenced this issue Dec 30, 2022
Fixes #13540

Scoped and transient internal services can obtain application services using the `DbContext` as a service locator. Singleton services cannot do this since they do not have access to the `DbContext`. This change allows the root application service provider to be registered as an `ISingletonOption` such that singleton internal services can resolve singleton and transient application services.
@ajcvickers ajcvickers added closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. and removed consider-for-current-release labels Dec 30, 2022
@ajcvickers ajcvickers modified the milestones: Backlog, 8.0.0 Dec 30, 2022
ajcvickers added a commit that referenced this issue Dec 31, 2022
Fixes #13540

Scoped and transient internal services can obtain application services using the `DbContext` as a service locator. Singleton services cannot do this since they do not have access to the `DbContext`. This change allows the root application service provider to be registered as an `ISingletonOption` such that singleton internal services can resolve singleton and transient application services.
@ajcvickers ajcvickers modified the milestones: 8.0.0, 8.0.0-preview1 Jan 29, 2023
@ajcvickers ajcvickers modified the milestones: 8.0.0-preview1, 8.0.0 Nov 14, 2023
@ajcvickers ajcvickers removed their assignment Sep 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-global closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported needs-design punted-for-3.0 type-enhancement
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants