dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.79k stars 3.19k forks source link

AccessorExtensions.GetService method can not resolve multiple services with IEnumerable<> #31550

Open efeozyer opened 1 year ago

efeozyer commented 1 year ago

File a bug

[x] Please check that the documentation does not explain the behavior you are seeing. [x] Please search in both open and closed issues to check that your bug has not already been filed.

Description

I've developed a middleware layer for several purposes. Interestingly, while implementing and running unit tests, I discovered that the method dbContext.GetService<IEnumerable> could not resolve services. However, when I attempted to resolve a single service with the dbContext.GetService() command, the instance was successfully resolved.

InMemory and EntityFrameworkCore packages has same behavior. Inside of DbContext instance I couldn't resolve services with IEnumerable<>.

Example code


// Middleware interface and mock implementations
public interface ITransactionMiddleware { }
public class MockTransactionMiddleware1 : ITransactionMiddleware { }
public class MockTransactionMiddleware2 : ITransactionMiddleware { }

// MockDbContext
public class MockDbContext : DbContext
{
    public DbSet<MockEntity> Entities { get; set; }
    public MockDbContext(DbContextOptions<MockDbContext> options) : base(options) { }
}

// MockEntity
public class MockEntity 
{
    [Key]
    public long Id { get; set; }
}

// Unit test
[Fact]
public async Task Should_SaveChanges_CallMiddleware_When_AnyMiddlewareAvailable()
{
    // Arrange
    var services = new ServiceCollection();
    IServiceProvider? serviceProvider = null;
    services.AddDbContext<MockDbContext>(builder =>
    {
        builder.UseInMemoryDatabase("MockDatabase");
        builder.UseApplicationServiceProvider(serviceProvider);
    });

    var mockMiddleware1 = new Mock<MockTransactionMiddleware1>();
    var mockMiddleware2 = new Mock<MockTransactionMiddleware2>();

    mockMiddleware1.Setup(x => x.InvokeAsync(It.IsAny<DbContext>(), It.IsAny<MockEntity>(), It.IsAny<CancellationToken>()));

    services.AddSingleton(mockMiddleware1.Object);
    services.AddSingleton(mockMiddleware2.Object);

    serviceProvider = services.BuildServiceProvider();

    var dbContext = _serviceProvider.CreateScope().ServiceProvider.GetService<MockDbContext>()!;
    var repository = new MockRepositoryImp(dbContext);

    var entity = new MockEntity();
    await repository.AddAsync(entity, default);

    // Act
    await repository.SaveChangesAsync(default);

    // Assert
    mockMiddleware1.Verify(x => x.InvokeAsync(It.IsAny<DbContext>(), It.IsAny<MockEntity>(), It.IsAny<CancellationToken>()), Times.Once);
}

Also when I try to resolve services from application, it worked as expected.

var middlewares1 = app.Services.GetServices<ITransactionMiddleware>();
var middlewares2 = app.Services.GetService<IEnumerable<ITransactionMiddleware>>();`

Provider and version information

EF Core version: 7.0.10 Database provider: Microsoft.EntityFrameworkCore - 7.0.10 Namespace : Microsoft.EntityFrameworkCore.Infrastructure; Target frameworks: .NET 6.0/NET7.0 Operating system: Windows 11 Pro IDE: Microsoft Visual Studio Community 2022 (64-bit) - Current Version 17.6.1

Screenshot 2023-08-25 085511 Screenshot 2023-08-25 085618 Screenshot 2023-08-25 085650

roji commented 1 year ago

You should generally not be doing UseApplicationServiceProvider, which mixes EF's internal services with your application's; what's you reason for doing this?

efeozyer commented 1 year ago

You should generally not be doing UseApplicationServiceProvider, which mixes EF's internal services with your application's; what's you reason for doing this?

I tried with/without .UseApplicationServiceProvider(), but result was same. GetService<> method behavior is strange.

roji commented 1 year ago

Repository class shouldn't take additional dependencies, instead of that I want to use GetService method.

Thsee aren't really a reason to use UseApplicationServiceProvider; EF maintains a dependency injection (DI) service provider internally for its own purposes, as an implementation detail - you shouldn't be interfering with that unless you have a good reason to. In the same way, you should not be using DbContext.GetService(), which is generally about getting EF's own services, and not your own.

I'm not sure exactly what you're trying to achieve, but if developing an ASP.NET application, then there's already the DI container managed by ASP.NET; that's the proper place to register your custom middleware services. Otherwise, you don't necessarily need a DI container for your service (I have no idea how it's being used); but if you do want a DI container, you have to create your own and register it there. See the documentation on dependency injection to see how to do that.

efeozyer commented 1 year ago

Repository class shouldn't take additional dependencies, instead of that I want to use GetService method.

Thsee aren't really a reason to use UseApplicationServiceProvider; EF maintains a dependency injection (DI) service provider internally for its own purposes, as an implementation detail - you shouldn't be interfering with that unless you have a good reason to. In the same way, you should not be using DbContext.GetService(), which is generally about getting EF's own services, and not your own.

I'm not sure exactly what you're trying to achieve, but if developing an ASP.NET application, then there's already the DI container managed by ASP.NET; that's the proper place to register your custom middleware services. Otherwise, you don't necessarily need a DI container for your service (I have no idea how it's being used); but if you do want a DI container, you have to create your own and register it there. See the documentation on dependency injection to see how to do that.

Let's forget about what I'm trying to achieve (I can find another way or I can use IServiceProvider interface) but it's not resolves the problem. That obvious the behaviour of .GetService<> is strange.

ajcvickers commented 1 year ago

Note for triage: Resolving multiple internal services via IEnumerable works, but the bridge for this to the application service provider does not. Repro below.

var container = new ServiceCollection()
    .AddScoped<IFoo, Foo1>()
    .AddScoped<IFoo, Foo2>()
    .AddDbContext<SomeDbContext>(b => b.UseSqlServer())
    .BuildServiceProvider();

using (var scope = container.CreateScope())
using (var context = scope.ServiceProvider.GetService<SomeDbContext>())
{
    var services = context.GetService<IEnumerable<IResettableService>>();
    foreach (var service in services)
    {
        Console.WriteLine(service);
    }

    var externalServices = context.GetService<IEnumerable<IFoo>>();
    foreach (var service in externalServices)
    {
        Console.WriteLine(service);
    }
}

public class SomeDbContext : DbContext
{
    public SomeDbContext(DbContextOptions<SomeDbContext> options)
        : base(options)
    {
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer(@"Data Source=(LocalDb)\MSSQLLocalDB;Database=AllTogetherNow")
            .LogTo(Console.WriteLine, LogLevel.Information)
            .EnableSensitiveDataLogging();
}

public interface IFoo
{
}

public class Foo1 : IFoo
{
}

public class Foo2 : IFoo
{
}
stevendarby commented 1 year ago

Isn't mixing the service providers actually quite common because AddDbContext calls UseApplicationServiceProvider?