Почему разрешение зависимостей разрешает параметры моей службы ПОСЛЕ самой службы?

У меня есть проект .NET Core 2.2 WebAPI, в котором я регистрирую три службы (назовем их MailerService, TicketService и AuditServce), а также промежуточное ПО (ExceptionMiddleware), которое зависит от одной из этих служб (MailerService). MailerService и TicketService оба зависят от строго типизированных объектов опций, которые я регистрирую с помощью service.Configure<TOption>(). Я позаботился о том, чтобы объекты опций были зарегистрированы до сервисов, а сами зависимости опций подключены к конструкторам сервисов.

Проблема в том, что TicketService прекрасно разрешает свой объект параметров из DI, но по какой-то причине конфигурация для MailerService разрешается ПОСЛЕ самой службы. Грубый набросок соответствующего кода ниже.

Я установил точки останова, чтобы следить за порядком разрешения, и делегат для установки MailerConfig постоянно срабатывает ПОСЛЕ конструктора MailerService. Поэтому каждый раз, когда я получаю экземпляр MailerSerivce, его параметр options равен NULL. И все же, наблюдая за тем же разрешением для TicketService, TicketConfig разрешается до того, как сработает конструктор TicketService, и TicketService получает правильно сконфигурированный объект параметров. Помимо того, что MailerService является зависимостью промежуточного программного обеспечения, я не могу понять, чем они могут отличаться.

Я уже несколько часов ломаю голову над этим, но не могу найти достойной документации, объясняющей, почему порядок разрешения DI может выйти из строя или что я мог сделать здесь неправильно. У кого-нибудь есть предположение, что я могу делать неправильно? Нужно ли промежуточное ПО исключений также регистрировать как службу?

Запуск

public class Startup
{
  public void ConfigureServices(IServiceCollection services)
  {
    services.AddMvcCore()
      .AddAuthorization()
      .AddJsonFormatters()
      .AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver());

    services.Configure<MailerConfig>(myOpts =>
    {
      // this always resolves AFTER MailerService's constructor
      myOpts = Configuration.GetSection("MailerSettings").Get<MailerConfig>();
      myOpts.SecretKey = _GetApiKey(Configuration.GetValue<string>("MailerApiKeyFile"));
    });

    services.Configure<ExceptionMiddlewareConfig>(myOpts =>
    {
      myOpts.AnonymousUserName = Configuration.GetValue<string>("AnonymousUserName");
      myOpts.SendToEmailAddress = Configuration.GetValue<string>("ErrorEmailAddress");
    });

    services.Configure<TicketConfig>(myOpts =>
    {
      // this always resovles BEFORE TicketService's constructor
      myOpts.ApiRoot = Configuration.GetValue<string>("TicketApiRoot");
      myOpts.SecretKey = _GetApiKey(Configuration.GetValue<string>("TicketApiKeyFile"));
    });

    services.AddTransient(provider =>
    {
      return new AuditService
      {
        ConnectionString = Configuration.GetValue<string>("Auditing:ConnectionString")
      };
    });

    services.AddTransient<ITicketService, TicketService>();
    services.AddTransient<IMailerService, AuditedMailerService>();
  }

  public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  {
    app.UseMiddleware<ExceptionMiddleware>();

    //app.UseHttpsRedirection();
    app.UseAuthentication();
    app.UseMvc();
  }
}

Конструктор MailerService

public AuditedMailerService(AuditService auditRepo, IOptions<MailerConfig> opts)
{
  // always gets a NULL opts object??????
  _secretKey = opts.Value.SecretKey;
  _defaultFromAddr = opts.Value.DefaultFromAddress;
  _defaultFromName = opts.Value.DefaultFromName;
  _repo = auditRepo;
}

Конструктор службы билетов

public TicketService(IOptions<TicketConfig> opts)
{
  // always gets an initialized opts object with proper values assigned
  ApiRoot = opts.Value.ApiRoot;
  SecretKey = opts.Value.SecretKey;
}

Конструктор промежуточного ПО

public ExceptionMiddleware(RequestDelegate next, IMailerService mailer, IOptions<ExceptionMiddlewareConfig> config)
{
  _mailer = mailer;
  _next = next;
  _anonymousUserName = config.Value.AnonymousUserName;
  _sendToEmailAddress = config.Value.SendToEmailAddress;
}

person Nate Kennedy    schedule 24.03.2020    source источник
comment
Не уверен, но если вы хотите поделиться одними и теми же вариантами, почему бы просто не зарегистрировать их как синглтоны... это избавит вас от многих головных болей.   -  person Jonathan Alfaro    schedule 24.03.2020
comment
Убедитесь, что Configuration.GetSection("MailerSettings").Get<MailerConfig>(); возвращает фактическое значение. Похоже, вы переопределяете параметры, предоставленные делегату. По умолчанию он будет потреблять любые ошибки, поэтому никаких исключений не будет.   -  person Nkosi    schedule 24.03.2020
comment
@NKosi: вызов GetSection() действительно работает. И следующая строка, где извлекается ключ API, тоже работает. Делегат, когда он срабатывает, устанавливает правильный объект параметров. Загадка заключается в том, что делегат запускается только ПОСЛЕ конструктора для AuditedMailerService, поэтому служба всегда получает нулевой объект параметров. Я совершенно озадачен.   -  person Nate Kennedy    schedule 27.03.2020
comment
@NateKennedy, проверьте мой данный ответ   -  person Nkosi    schedule 29.03.2020


Ответы (2)


Хотя это не совсем ответ (я до сих пор понятия не имею, почему DI разрешал параметры только после службы), я нашел решение проблемы. Я просто заканчиваю обзор Шаблон параметров и разрешение всех зависимостей явно внутри делегата, где я регистрирую службу почтовой программы. Я также настроил ExceptionMiddleware так, чтобы почтовая служба использовалась в качестве аргумента метода в InvokeAsync, а не в качестве аргумента конструктора. Не так уж важно, будут ли сервисы переходными или одноэлементными, но пока я просто предпочитаю переходные процессы.

Заметным недостатком этого подхода является то, что я больше не могу использовать механизмы оперативного обновления, встроенные в систему опций — если я изменю значение в настройках приложения на лету, приложение нужно будет перезапустить, чтобы подобрать его. Это не является реальной необходимостью в моем приложении, поэтому я могу с этим смириться, но другим следует помнить, прежде чем следовать моему подходу.

Новый регистрационный делегат MailerService:

  services.AddTransient<IMailerService>(provider =>
  {
    var cfg = Configuration.GetSection("MailerSettings").Get<MailerConfig>();
    cfg.SecretKey = _GetApiKey(Configuration.GetValue<string>("MailerApiKeyFile"));

    var auditor = provider.GetService<AuditService>();

    return new AuditedMailerService(auditor, Options.Create(cfg));
  });
person Nate Kennedy    schedule 27.03.2020

Потому что то, что ты делаешь, не имеет смысла.

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

Но промежуточное ПО всегда создается при запуске приложения (singleton). Поэтому любые зависимости также создаются при запуске приложения. Следовательно, экземпляр вашего «переходного» сервиса, созданный вашим промежуточным ПО, также является синглтоном!

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

У вас есть несоответствие образа жизни зависимости, что, как правило, является плохой идеей по многим причинам. Способ избежать этого, как указано выше, заключается в том, чтобы гарантировать, что все службы в вашей цепочке зависимостей зарегистрированы с одной и той же областью действия, то есть все, от чего зависит ваш ExceptionMiddleware — в этом случае AuditedMailerService — должно быть синглтоном.

Если - если - вы неявно предполагаете или должны сделать AuditedMailerService временным, то вместо того, чтобы внедрять его в конструктор промежуточного программного обеспечения, внедрить его с помощью метода Invoke:

public ExceptionMiddleware(RequestDelegate next, IOptions<ExceptionMiddlewareConfig> config)
{
  _mailer = mailer;
  _anonymousUserName = config.Value.AnonymousUserName;
  _sendToEmailAddress = config.Value.SendToEmailAddress;
}

public async Task Invoke(HttpContext httpContext, IMailerService mailer)
{
  ...
}

Но вот более интересный вопрос, вытекающий из симптомов этого несоответствия образа жизни: почему экземпляр IOptions<MailerConfig> в конечном итоге становится null?

Мое предположение — и это только предположение — заключается в том, что вы сталкиваетесь с тем фактом, что WebHost ASP.NET Core 2.x (компонент, который запускает ваше веб-приложение) фактически создает два IServiceProvider экземпляра. Существует начальный, «фиктивный», который создается для внедрения служб на самых ранних этапах запуска приложения, а затем «настоящий», который используется до конца жизненного цикла приложения. В связанной проблеме обсуждается, почему это проблематично: короче говоря, можно было получить экземпляры службы, зарегистрированные в фиктивном контейнере, а затем в реальном контейнере будет создан второй экземпляр той же службы, что вызовет проблемы. Я считаю, что из-за того, что промежуточное ПО запускается так рано в конвейере, используемый им контейнер IoC является фиктивным, не знающим IOptions<MailerConfig>, и поскольку расположение службы по умолчанию в ASP.NET Core возвращает null, когда запрошенная служба не найдена, вместо создания исключения, вы вернуть null.

person Ian Kemp    schedule 24.03.2020
comment
Ни один из предложенных подходов не решил проблему. Я попытался зарегистрировать как AuditService, так и AuditedMailerService как синглтоны - все равно получаю нулевой объект параметров. Попытался вернуть каждую службу к переходному состоянию и внедрить AuditMailerService в InvokeAsync - по-прежнему получаю нулевой объект параметров. Что бы ни происходило, это приводит к тому, что параметры последовательно разрешаются ПОСЛЕ AuditedMailerService. - person Nate Kennedy; 27.03.2020