AddScoped:如何调用正确的构造函数?

AddScoped: How to call the right constructor function?

我正在寻找正确的 C# 代码以在 ASP.NET 5 MVC(核心)中注入此服务,class 默认值适用。

如果我在下面添加范围服务,实例字段值为。如果我这样做 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,但我怀疑它是否重要。

这实际上与 HtmlSanitizer 具体关系不大,而更多地与 .NET Core 构造函数依赖注入的工作方式有关。

根据 documentation:

Services can be resolved by using:

  • IServiceProvider
  • ActivatorUtilities:
    • Creates objects that aren't registered in the container.
    • Used with some framework features.

Constructors can accept arguments that aren't provided by dependency injection, but the arguments must assign default values.

When services are resolved by IServiceProvider or ActivatorUtilities, constructor injection requires a public constructor.

When services are resolved by ActivatorUtilities, constructor injection requires that only one applicable constructor exists. Constructor overloads are supported, but only one overload can exist whose arguments can all be fulfilled by dependency injection.

在此上下文中,您正在使用 IServiceProvider,并且框架可以“访问”特定类型 IEnumerable<T> 的参数,这是 HtmlSanitizer constructor 所要求的:

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> 的情况下,参数被专门处理,默认数组将根据 source:

创建
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 { }

在这种情况下,参数 filters 将为空,解析 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 { }

你会发现传递了一个默认数组,导致默认过滤器无法使用。

通过使用 return new HtmlSanitizer() 中的工厂实例化,您可以绕过此实现行为并为每个参数传递 null,从而允许使用默认值。

这是非常令人惊讶的行为,我找不到任何文档将此描述为预期行为。我相信这可能只是 .NET Core DI 团队的疏忽,因为通常依赖项是非 IEnumerable 类型。还值得注意的是,此行为不适用于 IList<T>ISet<T> 类型的参数。