Nie można rozpoznać usługi objętej zakresem od głównego dostawcy .Net Core 2


92

Kiedy próbuję uruchomić aplikację, pojawia się błąd

InvalidOperationException: Cannot resolve 'API.Domain.Data.Repositories.IEmailRepository' from root provider because it requires scoped service 'API.Domain.Data.EmailRouterContext'.

Dziwne jest to, że to EmailRepository i interfejs są skonfigurowane dokładnie tak samo, o ile wiem, jak wszystkie moje inne repozytoria, ale nie jest dla nich zgłaszany żaden błąd. Błąd występuje tylko wtedy, gdy próbuję użyć app.UseEmailingExceptionHandling (); linia. Oto część mojego pliku Startup.cs.

public class Startup
{
    public IConfiguration Configuration { get; protected set; }
    private APIEnvironment _environment { get; set; }

    public Startup(IConfiguration configuration, IHostingEnvironment env)
    {
        Configuration = configuration;

        _environment = APIEnvironment.Development;
        if (env.IsProduction()) _environment = APIEnvironment.Production;
        if (env.IsStaging()) _environment = APIEnvironment.Staging;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        var dataConnect = new DataConnect(_environment);

        services.AddDbContext<GeneralInfoContext>(opt => opt.UseSqlServer(dataConnect.GetConnectString(Database.GeneralInfo)));
        services.AddDbContext<EmailRouterContext>(opt => opt.UseSqlServer(dataConnect.GetConnectString(Database.EmailRouter)));

        services.AddWebEncoders();
        services.AddMvc();

        services.AddScoped<IGenInfoNoteRepository, GenInfoNoteRepository>();
        services.AddScoped<IEventLogRepository, EventLogRepository>();
        services.AddScoped<IStateRepository, StateRepository>();
        services.AddScoped<IEmailRepository, EmailRepository>();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole();

        app.UseAuthentication();

        app.UseStatusCodePages();
        app.UseEmailingExceptionHandling();

        app.UseMvcWithDefaultRoute();
    }
}

Oto EmailRepository

public interface IEmailRepository
{
    void SendEmail(Email email);
}

public class EmailRepository : IEmailRepository, IDisposable
{
    private bool disposed;
    private readonly EmailRouterContext edc;

    public EmailRepository(EmailRouterContext emailRouterContext)
    {
        edc = emailRouterContext;
    }

    public void SendEmail(Email email)
    {
        edc.EmailMessages.Add(new EmailMessages
        {
            DateAdded = DateTime.Now,
            FromAddress = email.FromAddress,
            MailFormat = email.Format,
            MessageBody = email.Body,
            SubjectLine = email.Subject,
            ToAddress = email.ToAddress
        });
        edc.SaveChanges();
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
                edc.Dispose();
            disposed = true;
        }
    }
}

I wreszcie oprogramowanie pośredniczące do obsługi wyjątków

public class ExceptionHandlingMiddleware
{
    private const string ErrorEmailAddress = "errors@ourdomain.com";
    private readonly IEmailRepository _emailRepository;

    private readonly RequestDelegate _next;

    public ExceptionHandlingMiddleware(RequestDelegate next, IEmailRepository emailRepository)
    {
        _next = next;
        _emailRepository = emailRepository;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next.Invoke(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex, _emailRepository);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception exception,
        IEmailRepository emailRepository)
    {
        var code = HttpStatusCode.InternalServerError; // 500 if unexpected

        var email = new Email
        {
            Body = exception.Message,
            FromAddress = ErrorEmailAddress,
            Subject = "API Error",
            ToAddress = ErrorEmailAddress
        };

        emailRepository.SendEmail(email);

        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int) code;
        return context.Response.WriteAsync("An error occured.");
    }
}

public static class AppErrorHandlingExtensions
{
    public static IApplicationBuilder UseEmailingExceptionHandling(this IApplicationBuilder app)
    {
        if (app == null)
            throw new ArgumentNullException(nameof(app));
        return app.UseMiddleware<ExceptionHandlingMiddleware>();
    }
}

Aktualizacja: znalazłem ten link https://github.com/aspnet/DependencyInjection/issues/578, który doprowadził mnie do zmiany metody BuildWebHost mojego pliku Program.cs z tego

public static IWebHost BuildWebHost(string[] args)
{
    return WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .Build();
}

do tego

public static IWebHost BuildWebHost(string[] args)
{
    return WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .UseDefaultServiceProvider(options =>
            options.ValidateScopes = false)
        .Build();
}

Nie wiem, co dokładnie się dzieje, ale teraz wydaje się, że działa.


4
To, co się tam dzieje, polega na tym, że zagnieżdżanie zakresu nie jest sprawdzane; tak jak w, nie sprawdza, w czasie wykonywania, czy masz niewłaściwe zagnieżdżenie poziomu zakresu. Najwyraźniej zostało to domyślnie wyłączone w 1.1. Gdy pojawiła się wersja 2.0, włączyli ją domyślnie.
Robert Burke

Każdy, kto próbuje wyłączyć ValidateScopes, powinien przeczytać ten stackoverflow.com/a/50198738/1027250
Yorro,

Odpowiedzi:


188

Zarejestrowałeś IEmailRepositoryusługę jako usługę o określonym zakresie w Startupklasie. Oznacza to, że nie można go wstrzyknąć jako parametru konstruktora, Middlewareponieważ tylko Singletonusługi można rozpoznać przez wstrzyknięcie konstruktora w programie Middleware. Powinieneś przenieść zależność do Invokemetody takiej jak ta:

public ExceptionHandlingMiddleware(RequestDelegate next)
{
    _next = next;
}

public async Task Invoke(HttpContext context, IEmailRepository emailRepository)
{
    try
    {
        await _next.Invoke(context);
    }
    catch (Exception ex)
    {
        await HandleExceptionAsync(context, ex, emailRepository);
    }
}

13
Łał! Nigdy nie wiedziałem, że możesz wstrzyknąć do metod, czy to tylko dla oprogramowania pośredniego, czy mogę użyć tej sztuczki w moich własnych metodach?
Fergal Moran,

A co z IMiddleware zarejestrowanym jako scoped? Wiem na pewno, że otrzymuję nową instancję oprogramowania pośredniego, ale nadal nie mogę wstrzyknąć do niej usługi o określonym zakresie.
Botis

2
@FergalMoran Niestety ta „sztuczka” jest specjalnym zachowaniem samej Invokemetody oprogramowania pośredniego . Możesz jednak osiągnąć coś podobnego za pośrednictwem biblioteki IoC autofac i wstrzykiwania właściwości. Zobacz wstrzykiwanie zależności ASP.NET Core MVC za pośrednictwem właściwości lub metody ustawiającej? .
B12Toaster

4
Iniekcja to nie magia. Za kulisami znajduje się silnik, który w rzeczywistości wywołuje kontener zależności w celu wygenerowania wystąpień i przekazania ich jako parametrów do konstruktorów lub metod. Ten silnik cząstkowy szuka metod o nazwie „Invoke” z pierwszym argumentem HttpContext, a następnie tworzy wystąpienia dla pozostałych parametrów.
Thanasis Ioannidis

97

Innym sposobem, aby uzyskać instancję scoped zależność jest wstrzykiwać operatora ( IServiceProvider) do middleware konstruktora, tworzyć scopew Invokemetodzie, a następnie uzyskać żądaną usługę z zakresu:

using (var scope = _serviceProvider.CreateScope()) {
    var _emailRepository = scope.ServiceProvider.GetRequiredService<IEmailRepository>();

    //do your stuff....
}

Zapoznaj się z usługami rozwiązywania problemów w treści metody w poradach dotyczących najlepszych praktyk iniekcji zależności asp.net core, aby uzyskać więcej informacji.


5
Bardzo pomocne, dzięki! Dla każdego, kto próbuje uzyskać dostęp do kontekstów EF w oprogramowaniu pośredniczącym, jest to droga, ponieważ są one domyślnie objęte zakresem.
ntziolis

stackoverflow.com/a/49886317/502537 robi to bardziej bezpośrednio
RickAndMSFT

Na początku nie sądziłem, że to zadziałało, ale potem zdałem sobie sprawę, że robisz scope.ServiceProviderzamiast _serviceProviderna drugiej linii. Dzięki za to.
adam0101

_serviceProvider.CreateScope (). ServiceProvider działa lepiej dla mnie
XLR8

Myślę, że najlepiej byłoby użyć IServiceScopeFactorydo tego celu
Francesco DM

27

Oprogramowanie pośredniczące jest zawsze singletonem, więc nie można mieć zależności w zakresie jako zależności konstruktora w konstruktorze oprogramowania pośredniego.

Oprogramowanie pośredniczące obsługuje iniekcję metody w metodzie Invoke, więc możesz po prostu dodać IEmailRepository emailRepository jako parametr do tej metody i zostanie tam wstrzyknięty i będzie dobrze zgodny z zakresem.

public async Task Invoke(HttpContext context, IEmailRepository emailRepository)
{

    ....
}

Byłem w podobnej sytuacji, potem dodałem usługę za pomocą AddTransient i udało mi się rozwiązać zależność. Myślałem, że to nie zadziała, ponieważ oprogramowanie pośredniczące jest singleton? trochę dziwne ..
Sateesh Pagolu

1
Myślę, że zależność przejściowa musiałaby zostać usunięta ręcznie, w przeciwieństwie do zakresu, który zostanie automatycznie usunięty na końcu żądania internetowego, w którym jest po raz pierwszy utworzony. Być może przejściowy element jednorazowego użytku wewnątrz zależności o określonym zakresie zostałby usunięty, gdy obiekt zewnętrzny zostałby usunięty. Nadal nie jestem pewien, czy przejściowa zależność wewnątrz singletona lub obiektu o dłuższym niż przejściowy czas życia jest dobrym pomysłem, myślę, że uniknąłbym tego.
Joe Audette

2
Mimo że w tym przypadku można wstrzyknąć zależność o zakresie przejściowym za pośrednictwem konstruktora, nie zostanie ona utworzona, jak można by pomyśleć. Zdarzy się to tylko raz, gdy zostanie zbudowany Singleton.
Jonathan

1
Wspomniał Pan, że oprogramowanie pośredniczące jest zawsze singletonem, ale to nieprawda. Możliwe jest utworzenie oprogramowania pośredniego jako fabrycznego oprogramowania pośredniego i używanie go jako oprogramowania pośredniego o określonym zakresie.
Harun Diluka Heshan

Wygląda na to, że oprogramowanie pośredniczące oparte na fabryce zostało wprowadzone w asp.netcore 2.2, a dokumentacja została utworzona w 2019 r. Tak więc moja odpowiedź była prawdziwa, gdy opublikowałem ją, o ile wiem. Fabryczne oprogramowanie pośredniczące wygląda obecnie na dobre rozwiązanie.
Joe Audette

4

Twój middlewarei servicemusi być ze sobą kompatybilny, aby wstrzyknąć serviceprzez constructortwój middleware. Tutaj Twój middlewarezostał stworzony jako a, convention-based middlewareco oznacza, że ​​działa jako a, singleton servicea Ty utworzyłeś swoją usługę jako scoped-service. Nie możesz więc wstrzyknąć a scoped-servicedo konstruktora a, singleton-serviceponieważ zmusza to scoped-servicedo działania jako singletonjeden. Oto jednak opcje.

  1. Wstrzyknij usługę jako parametr InvokeAsyncmetody.
  2. Jeśli to możliwe, spraw, aby Twoja usługa była pojedyncza.
  3. Zmień swój middlewarew factory-basedjeden.

A Factory-based middlewaremoże działać jako scoped-service. Możesz więc wstrzyknąć inny scoped-serviceza pośrednictwem konstruktora tego oprogramowania pośredniego. Poniżej pokazałem, jak utworzyć factory-basedoprogramowanie pośredniczące.

To jest tylko do demonstracji. Więc usunąłem cały inny kod.

public class Startup
{
    public Startup()
    {
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<TestMiddleware>();
        services.AddScoped<TestService>();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseMiddleware<TestMiddleware>();
    }
}

The TestMiddleware:

public class TestMiddleware : IMiddleware
{
    public TestMiddleware(TestService testService)
    {
    }

    public Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        return next.Invoke(context);
    }
}

The TestService:

public class TestService
{
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.