AddScoped: Как вызвать правильную функцию конструктора?

#c# #asp.net-core #asp.net5

Вопрос:

Я ищу правильный код C# для внедрения этой службы в ASP.NET 5 MVC (ядро) таким образом, чтобы применялись значения по умолчанию для класса.

Если я добавлю службу с областью действия ниже, значения полей экземпляра будут пустыми. Если я это сделаю var a = new HtmlSanitizer(); , поля экземпляра будут заполнены ненулевыми значениями по умолчанию, такими как «длинная строка значений».

 services.AddScoped<IHtmlSanitizer, HtmlSanitizer>();
 

Если я перепишу инъекцию, как показано ниже, поля экземпляра будут заполнены. Соответствует ли этот код предполагаемому эффекту, как указано выше? Конечно, в чем разница в полученном объекте?

 services.AddScoped<IHtmlSanitizer, HtmlSanitizer>(
    _ => { return new HtmlSanitizer(); }
    // Why the difference?
    // Is this how one passes constant parameter values?
);
 

Я использовал HtmlSanitizer и Asp.Net 5 в этом примере, но я сомневаюсь, что это имеет значение.

Ответ №1:

На самом деле это имеет меньшее отношение к HtmlSanitizer конкретному и больше связано с тем, как работает внедрение зависимостей конструктора .NET Core.

Согласно документации:

Услуги могут быть решены с помощью:

  • IServiceProvider
  • Возможности активатора:
    • Создает объекты, которые не зарегистрированы в контейнере.
    • Используется с некоторыми функциями фреймворка.

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

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

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

В этом контексте вы используете IServiceProvider, и платформа может «посещать» аргументы определенного типа IEnumerable<T> , что требуется HtmlSanitizer конструктору:

 public HtmlSanitizer(IEnumerable<string>? allowedTags = null, IEnumerable<string>? allowedSchemes = null,
    IEnumerable<string>? allowedAttributes = null, IEnumerable<string>? uriAttributes = null, IEnumerable<string>? allowedCssProperties = null)
{
    AllowedTags = new HashSet<string>(allowedTags ?? DefaultAllowedTags, StringComparer.OrdinalIgnoreCase);
    AllowedSchemes = new HashSet<string>(allowedSchemes ?? DefaultAllowedSchemes, StringComparer.OrdinalIgnoreCase);
    AllowedAttributes = new HashSet<string>(allowedAttributes ?? DefaultAllowedAttributes, StringComparer.OrdinalIgnoreCase);
    UriAttributes = new HashSet<string>(uriAttributes ?? DefaultUriAttributes, StringComparer.OrdinalIgnoreCase);
    AllowedCssProperties = new HashSet<string>(allowedCssProperties ?? DefaultAllowedCssProperties, StringComparer.OrdinalIgnoreCase);
    AllowedAtRules = new HashSet<CssRuleType>(DefaultAllowedAtRules);
    AllowedClasses = new HashSet<string>(DefaultAllowedClasses, StringComparer.OrdinalIgnoreCase);
}
 

Когда распознаватель служб увидит конструктор с аргументами, он попытается просмотреть каждый аргумент. В случае IEnumerable<T> , аргументы обрабатываются специально, и массивы по умолчанию будут созданы для источника:

 protected override object VisitIEnumerable(IEnumerableCallSite enumerableCallSite, RuntimeResolverContext context)
{
    var array = Array.CreateInstance(
        enumerableCallSite.ItemType,
        enumerableCallSite.ServiceCallSites.Length);

    for (int index = 0; index < enumerableCallSite.ServiceCallSites.Length; index  )
    {
        object value = VisitCallSite(enumerableCallSite.ServiceCallSites[index], context);
        array.SetValue(value, index);
    }
    return array;
}
 

Вы можете доказать это с помощью очень простого тестового жгута:

 public class Test : ITest
{
    private ISet<string> _defaults = new HashSet<string> { "one", "two", "three" };
    private ISet<string> _filters;

    public Test(List<string> filters = null)
    {
        _filters = new HashSet<string>(filters.ToHashSet() ?? _defaults);
    }
}

public interface ITest { }
 

В этом случае фильтры параметров будут равны нулю, и вместо этого при разрешении будут использоваться значения по умолчанию provider.GetService(typeof(ITest)); . Однако, если мне потребуется IEnumerable вместо:

 public class Test : ITest
{
    private ISet<string> _defaults = new HashSet<string> { "one", "two", "three" };
    private ISet<string> _filters;

    public Test(IEnumerable<string> filters = null)
    {
        _filters = new HashSet<string>(filters.ToHashSet() ?? _defaults);
    }
}

public interface ITest { }
 

вы обнаружите, что передан массив по умолчанию, в результате чего фильтры по умолчанию НЕ будут использоваться.

Используя заводской экземпляр , в котором вы возвращаетесь new HtmlSanitizer() , вы обходите это поведение реализации и передаете значение null для каждого параметра, позволяя использовать значения по умолчанию.

Это очень удивительное поведение, и я не смог найти никакой документации, описывающей это как ожидаемое поведение. Я полагаю, что это может быть просто недосмотром со стороны команды .NET Core DI, поскольку обычно зависимости предназначены для типов, не имеющих числа. Также стоит отметить, что это поведение НЕ относится к параметрам типа IList<T> или ISet<T> .