This actually has less to do with HtmlSanitizer specifically and more to do with how .NET Core Constructor Dependency Injection works.
Per the 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.
In this context, you are using the IServiceProvider and the framework can "visit" arguments that are specifically of type IEnumerable<T>, which is what is required by the 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);
}
When the service resolver sees a constructor with arguments, it will attempt to visit each argument. In the case of IEnumerable<T>, the arguments are specifically handled and default arrays will be created per the 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;
}
You can prove this out with a very simple test harness:
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 { }
In this case, the parameter filters will be null and the defaults will be used instead when resolving provider.GetService(typeof(ITest));. However, if I require an IEnumerable instead:
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 { }
you will find that a default array is passed, causing the default filters to NOT be used.
By using the factory instantiation where you return new HtmlSanitizer(), you bypass this implementation behavior and pass null for each parameter, allowing the defaults to be used.
This is very surprising behavior and I was unable to find any documentation that describes this as expected behavior. I believe this may simply be an oversight by the .NET Core DI team since typically dependencies are intended to be non-IEnumerable types. It is also worth noting that this behavior does NOT apply to parameters of IList<T> or ISet<T> type.