Не удается разрешить ограниченную службу от корневого поставщика .Net Core 2

92

Когда я пытаюсь запустить свое приложение, я получаю сообщение об ошибке

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

Странно то, что этот EmailRepository и интерфейс настроены точно так же, насколько я могу судить, как и все другие мои репозитории, но для них не возникает никаких ошибок. Ошибка возникает только в том случае, если я пытаюсь использовать app.UseEmailingExceptionHandling (); линия. Вот некоторые из моих файлов 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();
    }
}

Вот репозиторий электронной почты

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

И, наконец, промежуточное ПО для обработки исключений

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

Обновление: я нашел эту ссылку https://github.com/aspnet/DependencyInjection/issues/578, которая заставила меня изменить метод BuildWebHost моего файла Program.cs с этого

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

к этому

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

Я не знаю, что именно происходит, но, похоже, сейчас это работает.

Джефф Шварц
источник
4
Что там происходит, так это то, что вложенность области не проверяется; например, во время выполнения он не проверяет, есть ли у вас неправильная вложенность уровня области видимости. По-видимому, это было отключено по умолчанию в 1.1. Когда вышла 2.0, они включили его по умолчанию.
Роберт Берк
Всем, кто пытается отключить ValidateScopes, прочтите этот stackoverflow.com/a/50198738/1027250
Йорро

Ответы:

188

Вы зарегистрировали IEmailRepositoryв Startupклассе услугу с ограниченной областью действия . Это означает, что вы не можете внедрить его как параметр конструктора в, Middlewareпотому что только Singletonслужбы могут быть разрешены путем внедрения конструктора в Middleware. Вы должны переместить зависимость к Invokeметоду следующим образом:

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);
    }
}
user1336
источник
13
Вот Это Да! Никогда не знал, что можно вводить в методы, это только для промежуточного программного обеспечения или я могу использовать этот трюк в своих собственных методах?
Fergal Moran
А как насчет IMiddleware, зарегистрированного как scoped? Я точно знаю, что получаю новый экземпляр промежуточного программного обеспечения, но я все еще не могу внедрить в него сервис с ограниченной областью действия.
Botis
2
@FergalMoran К сожалению, этот «трюк» является особым поведением метода промежуточного программного обеспечения Invoke. Однако вы можете добиться чего-то подобного с помощью autofac IoC lib и инъекции свойств. Смотрите ASP.NET Core MVC Dependency Injection через свойство или метод установки? .
B12Toaster
4
Инъекция - это не волшебство. За кулисами стоит движок, который фактически вызывает контейнер зависимостей для создания экземпляров для передачи в качестве параметров конструкторам или методам. Этот конкретный механизм ищет методы с именем «Invoke» с первым аргументом HttpContext, а затем создает экземпляры для остальных параметров.
Thanasis
97

Другой способ получить экземпляр зависимости с ограниченной областью видимости - это вставить service provider ( IServiceProvider) в конструктор промежуточного программного обеспечения, создать метод scopein Invokeи затем получить требуемую услугу из области:

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

    //do your stuff....
}

Дополнительные сведения см. В разделе «Разрешение служб в теле метода» в разделе «Рекомендации по внедрению зависимостей ядра asp.net» .

Риддик
источник
5
Супер полезно, спасибо! Для всех, кто пытается получить доступ к контекстам EF в промежуточном программном обеспечении, это правильный путь, поскольку они ограничены по умолчанию.
ntziolis
stackoverflow.com/a/49886317/502537 делает это напрямую
RickAndMSFT
Сначала я не думал, что это работает, но потом понял, что вы делаете это scope.ServiceProviderвместо _serviceProviderвторой строки. Спасибо за это.
adam0101
_serviceProvider.CreateScope (). ServiceProvider подходит для меня лучше
XLR8
Я думаю, что лучше всего было бы использовать IServiceScopeFactoryдля этой цели
Francesco DM
27

Промежуточное ПО всегда является одноэлементным, поэтому у вас не может быть зависимостей в виде зависимостей конструктора в конструкторе вашего промежуточного программного обеспечения.

Промежуточное ПО поддерживает внедрение метода в метод Invoke, поэтому вы можете просто добавить IEmailRepository emailRepository в качестве параметра к этому методу, и он будет внедрен туда и будет работать в соответствии с областью действия.

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

    ....
}
Джо Одетт
источник
У меня была аналогичная ситуация, затем я добавил службу с помощью AddTransient, и она смогла разрешить зависимость. Я думал, что это не сработает, так как промежуточное ПО одноэлементное? немного странно ..
Сатиш Паголу 03
1
Я думаю, что временная зависимость должна быть удалена вручную, в отличие от области действия, которая будет автоматически удалена в конце веб-запроса, где она создается впервые. Возможно, временный одноразовый объект внутри зависимости с ограниченной областью видимости будет удален при удалении внешнего объекта. Тем не менее я не уверен, что временная зависимость внутри синглтона или объекта с более длительным временем жизни, чем переходный, - хорошая идея, думаю, я бы этого избегал.
Joe Audette 03
2
Несмотря на то, что в этом случае вы можете внедрить зависимость с временной областью через конструктор, она не будет создана, как вы могли бы подумать. Это произойдет только один раз, когда будет собран синглтон.
Джонатан
1
Вы упомянули, что промежуточное ПО всегда одноэлементно, но это неправда. Можно создать промежуточное программное обеспечение как промежуточное ПО на основе фабрики и использовать его как промежуточное ПО с заданной областью действия.
Харун Дилука Хешан
Похоже, промежуточное ПО на базе фабрики было представлено в asp.netcore 2.2, а документация была создана в 2019 году. Насколько мне известно, мой ответ был правдой, когда я опубликовал его. Фабричное промежуточное ПО сегодня действительно выглядит хорошим решением.
Джо Одетт
4

Ваш middlewareи serviceон должны быть совместимы друг с другом, чтобы вводить serviceчерез constructorваш middleware. Вот твойmiddleware был создан как, convention-based middlewareчто означает, что он действует как, singleton serviceа вы создали свою службу как scoped-service. Итак, вы не можете вставить a scoped-serviceв конструктор a, singleton-serviceпотому что он заставляет scoped-serviceего действовать как singletonединицу. Однако вот ваши варианты.

  1. Добавьте свой сервис в качестве параметра InvokeAsyncметода.
  2. Если возможно, сделайте свою службу одноэлементной.
  3. Превратите своего middlewareв factory-basedодного.

А Factory-based middleware может действовать как scoped-service. Итак, вы можете внедрить еще один scoped-serviceчерез конструктор этого промежуточного программного обеспечения. Ниже я показал вам, как создать factory-basedпромежуточное ПО.

Это только для демонстрации. Итак, я удалил весь остальной код.

public class Startup
{
    public Startup()
    {
    }

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

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

В TestMiddleware:

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

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

В TestService:

public class TestService
{
}
Харун Дилука Хешан
источник