Der Dienst mit Gültigkeitsbereich kann vom Root-Anbieter .Net Core 2 nicht aufgelöst werden


83

Wenn ich versuche, meine App auszuführen, wird der Fehler angezeigt

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

Was seltsam ist, ist, dass dieses EmailRepository und diese Schnittstelle, soweit ich das beurteilen kann, genau so eingerichtet sind wie alle meine anderen Repositorys, aber für sie wird kein Fehler ausgegeben. Der Fehler tritt nur auf, wenn ich versuche, die App zu verwenden.UseEmailingExceptionHandling (); Linie. Hier sind einige meiner Startup.cs-Dateien.

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();
    }
}

Hier ist das 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;
        }
    }
}

Und schließlich die Ausnahmebehandlung für Middleware

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>();
    }
}

Update: Ich habe diesen Link https://github.com/aspnet/DependencyInjection/issues/578 gefunden, wodurch ich die BuildWebHost-Methode meiner Program.cs-Datei geändert habe

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

dazu

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

Ich weiß nicht genau, was los ist, aber es scheint jetzt zu funktionieren.


4
Was dort passiert, ist, dass die Scope-Verschachtelung nicht validiert wird. Wie in wird zur Laufzeit nicht überprüft, ob die Bereichsstufe nicht ordnungsgemäß verschachtelt ist. Anscheinend war dies in 1.1 standardmäßig deaktiviert. Sobald 2.0 kam, wurde es standardmäßig aktiviert.
Robert Burke

Antworten:


174

Sie haben den IEmailRepositoryals Scoped Service in der StartupKlasse registriert . Dies bedeutet, dass Sie es nicht als Konstruktorparameter einfügen können, Middlewareda nur SingletonDienste durch Einfügen eines Konstruktors in aufgelöst werden können Middleware. Sie sollten die Abhängigkeit Invokewie folgt auf die Methode verschieben:

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);
    }
}

12
Beeindruckend! Ich wusste nie, dass Sie in Methoden injizieren können, ist dies nur für Middleware oder kann ich diesen Trick in meinen eigenen Methoden verwenden?
Fergal Moran

Was ist mit IMiddleware, die als Gültigkeitsbereich registriert ist? Ich weiß sicher, dass ich eine neue Instanz von Middleware bekomme, aber ich kann immer noch keinen Scoped-Service hinzufügen.
Botis

2
@FergalMoran Leider ist dieser "Trick" ein spezielles Verhalten nur der Middleware- InvokeMethode. Sie können jedoch etwas Ähnliches über die Autofac-IoC-Bibliothek und die Eigenschaftsinjektion erreichen. Siehe ASP.NET Core MVC-Abhängigkeitsinjektion über Eigenschaft oder Setter-Methode? .
B12Toaster

4
Injektion ist keine Magie. Hinter den Kulissen befindet sich eine Engine, die den Abhängigkeitscontainer aufruft, um Instanzen zu generieren, die als Parameter an Konstruktoren oder Methoden übergeben werden. Diese bestimmte Engine sucht nach Methoden mit dem Namen "Invoke" mit einem ersten Argument von HttpContext und erstellt dann Instanzen für den Rest der Parameter.
Thanasis Ioannidis

86

Eine weitere Möglichkeit , die Instanz von scoped Abhängigkeit zu erhalten , ist Service - Provider (zu injizieren IServiceProvider) in den Middleware - Konstruktor erstellen scopein InvokeMethode und dann den gewünschten Dienst aus dem Anwendungsbereich erhalten:

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

    //do your stuff....
}

Schauen Sie sich Resolving Dienstleistungen in einem Verfahren Körper in asp.net Kern Dependency Injection Best Practices Tipps Tricks für weitere Details.


5
Super hilfreich, danke! Für alle, die versuchen, in Middleware auf EF-Kontexte zuzugreifen, ist dies der richtige Weg, da sie standardmäßig einen Gültigkeitsbereich haben.
Ntziolis


Zuerst dachte ich nicht, dass das funktioniert, aber dann wurde mir klar, dass du es tust, scope.ServiceProvideranstatt _serviceProviderin der zweiten Zeile. Danke dafür.
Adam0101

_serviceProvider.CreateScope (). ServiceProvider macht es besser für mich
XLR8

Ich denke, es wäre am besten, IServiceScopeFactoryfür diesen Zweck zu verwenden
Francesco DM

27

Middleware ist immer ein Singleton, sodass Sie im Konstruktor Ihrer Middleware keine Abhängigkeiten als Konstruktorabhängigkeiten festlegen können.

Middleware unterstützt die Methodeninjektion für die Invoke-Methode. Sie können also einfach das IEmailRepository emailRepository als Parameter zu dieser Methode hinzufügen. Es wird dort injiziert und ist in Ordnung.

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

    ....
}

Ich befand mich in einer ähnlichen Situation, dann fügte ich einen Dienst mit AddTransient hinzu und konnte die Abhängigkeit auflösen. Ich dachte, es würde nicht funktionieren, da die Middleware Singleton ist? etwas seltsam ..
Sateesh Pagolu

1
Ich denke, eine vorübergehende Abhängigkeit müsste manuell entsorgt werden, im Gegensatz zum Gültigkeitsbereich, der am Ende der Webanforderung, an der sie zuerst erstellt wird, automatisch entsorgt wird. Möglicherweise wird ein vorübergehender Einwegartikel innerhalb einer Abhängigkeit mit Gültigkeitsbereich entsorgt, wenn das äußere Objekt entsorgt wird. Trotzdem bin ich mir nicht sicher, ob eine vorübergehende Abhängigkeit innerhalb eines Singletons oder eines Objekts mit einer längeren Lebensdauer als eine vorübergehende Lebensdauer eine gute Idee ist. Ich denke, ich würde das vermeiden.
Joe Audette

2
Obwohl Sie in diesem Fall eine Abhängigkeit mit transientem Gültigkeitsbereich über den Konstruktor einfügen können, wird sie nicht wie gedacht instanziiert. Es wird nur einmal passieren, wenn der Singleton gebaut wird.
Jonathan

1
Sie haben erwähnt, dass Middleware immer ein Singleton ist, aber es ist nicht wahr. Es ist möglich, eine Middleware als werksbasierte Middleware zu erstellen und als Scoped-Middleware zu verwenden.
Harun Diluka Heshan

Es sieht so aus, als ob fabrikbasierte Middleware in asp.netcore 2.2 eingeführt und die Dokumentation 2019 erstellt wurde. Meine Antwort war also wahr, als ich sie veröffentlicht habe, soweit ich weiß. Factory-basierte Middleware scheint heute eine gute Lösung zu sein.
Joe Audette

4

Ihr middlewareund das servicemüssen miteinander kompatibel sein, um das serviceüber das constructorvon Ihnen injizieren zu können middleware. Hier wurde Ihr middlewareals erstellt, convention-based middlewarewas bedeutet, dass es als fungiert singleton serviceund Sie Ihren Dienst als erstellt haben scoped-service. Sie können also kein a scoped-servicein den Konstruktor von a injizieren, singleton-serviceweil es das zwingt scoped-service, als singletonEins zu agieren . Hier sind jedoch Ihre Optionen.

  1. Fügen Sie Ihren Service als Parameter in die InvokeAsyncMethode ein.
  2. Machen Sie Ihren Service nach Möglichkeit zu einem Singleton.
  3. Verwandle deine middlewarein eine factory-basedEins.

A Factory-based middlewarekann als scoped-service. Sie können also eine andere scoped-serviceüber den Konstruktor dieser Middleware einfügen. Im Folgenden habe ich Ihnen gezeigt, wie Sie eine factory-basedMiddleware erstellen .

Dies ist nur zur Demonstration. Also habe ich den ganzen anderen Code entfernt.

public class Startup
{
    public Startup()
    {
    }

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

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

Die TestMiddleware:

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

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

Die TestService:

public class TestService
{
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.