在 .NET 4.7.2 的 WebForms 中连接简单注入器

Wiring up Simple Injector in WebForms in .NET 4.7.2

随着 .NET 4.7.2 中的更改,现在可以在 Web 窗体中进行构造函数注入。我已经获得了使用 Web 窗体的简单注入器,但想要一些关于是否有任何 "gotchas" 我可能会丢失的信息。

首先,我从 here.

中获取了页面本身的注册信息
public static void RegisterWebPages(this Container container)
{
    var pageTypes = 
        from assembly in BuildManager.GetReferencedAssemblies().Cast<Assembly>()
        where !assembly.IsDynamic
        where !assembly.GlobalAssemblyCache
        from type in assembly.GetExportedTypes()
        where type.IsSubclassOf(typeof(Page))
        where !type.IsAbstract && !type.IsGenericType
        select type;

    foreach (Type type in pageTypes)
    {
        var reg = Lifestyle.Transient.CreateRegistration(type, container);
        reg.SuppressDiagnosticWarning(
            DiagnosticType.DisposableTransientComponent,
            "ASP.NET creates and disposes page classes for us.");
        container.AddRegistration(type, reg);
    }
}

这在使用上面 link 中的 属性 注入方法时有效。为了完整起见,我将其包含在这里。

当我第一次连接它时,有一个 OutputCacheModule 有一个内部构造函数的问题。使用 here 中的代码,我能够解决该问题以及可能由内部构造函数引起的任何其他问题。为了完整性,这是该实现的代码。

public class InternalConstructorResolutionBehavior : IConstructorResolutionBehavior
{
    private IConstructorResolutionBehavior original;

    public InternalConstructorResolutionBehavior(Container container)
    {
        this.original = container.Options.ConstructorResolutionBehavior;
    }

    public ConstructorInfo GetConstructor(Type implementationType)
    {
        if (!implementationType.GetConstructors().Any())
        {
            var internalCtors = implementationType.GetConstructors(
                BindingFlags.Instance | BindingFlags.NonPublic)
                .Where(c => !c.IsPrivate)
                .ToArray();

            if (internalCtors.Length == 1) return internalCtors.First();
        }

        return original.GetConstructor(implementationType);
    }
}

现在已经讲完了背景故事,下面是问题的实质。这是我连接的自定义激活器。

public class SimpleInjectorWebFormsActivator : IServiceProvider
{
    private readonly Container container;

    public SimpleInjectorWebFormsActivator(Container container)
    {
        this.container = container;
        this.container.Options.DefaultScopedLifestyle = new WebRequestLifestyle();
        this.container.Options.ConstructorResolutionBehavior =
            new InternalConstructorResolutionBehavior(this.container);
    }

    public object GetService(Type serviceType)
    {
        return container.GetInstance(serviceType);
    }
}

问题是,GetService方法够用吗?目前很少有关于如何使用 WebForms 的新扩展点的信息。有一个 Autofac 示例比我简单的一行传递到 Simple Injector 要复杂得多,但由于我不熟悉 Autofac,我不知道其中有多少是用于容器的。

现在解决方案有效。页面加载没有错误。容器将调用传递给 Verify。

这够了吗还是还有更多工作要做?有没有我遗漏的"gotchas"?我不太熟悉 ether Simple Injector 或 WebForms 的更深层内部工作原理,所以我担心我可能会遗漏一些重要的东西。

截至目前,没有必要也没有计划使用任何作用域容器。

我们将发布一个使用 Unity 容器的 Adapter(Activator) nupkg(以及一个博客),并且还将开源它。以下是实施 Adapter(Activator) 的一些一般指导。

  1. 如果 IoC 容器无法解析 serviceType,您可以考虑缓存该类型。下次可以直接通过反射创建实例来获得一些性能增益。
  2. 您可以保留注册前注册的Adapter(Activator),如果您的Adapter(Activator)无法解析serviceType,请尝试使用该Adapter(Activator)。
  3. 如果 IoC 容器实现了 IDisposable,您的 Adapter(Activator) 应该实现 IRegisteredObject,其中 Container.Dispose 可以被调用。

IMO,Web 窗体中的这个新功能没有经过特别深思熟虑。主要问题是 Web 窗体违反了 IServiceProvider 合同。

IServiceProvider.GetService 方法定义 null 如果不存在此类服务,则应 returned。但是一旦你实际上 return null,例如当您无法构造该类型时,Web 窗体会从其堆栈深处抛出一个 NullReferenceException

另一方面,如果 Web 窗体符合 IServiceProvider 抽象,插入简单注入器将是一个单一语句的问题,因为 SimpleInjector.Container 实际上实现了 IServiceProvider:

// WARNING: This won’t work
HttpRuntime.WebObjectActivator = container; 

最重要的是,当通过 HttpRuntime.WebObjectActivator 设置 IServiceProvider 时,Web 窗体几乎会调用它的所有内容,甚至是它自己的内部对象,这对我来说意义不大.

因此,您必须提供一个特殊的 ASP.NET Web 表单兼容 IServiceProvider 实现(因此 违反 合同)。

请注意,大多数 DI 容器实际上实现了 IServiceProvider,但您会看到大多数都失败了,因为违反了合同。

适配器实现如下所示:

class SimpleInjectorWebFormsServiceActivator : IServiceProvider
{
    private const BindingFlags flag =
        BindingFlags.Instance | BindingFlags.NonPublic |
        BindingFlags.Public | BindingFlags.CreateInstance;

    private readonly Container container;

    public SimpleInjectorWebFormsServiceActivator(Container container) =>
        this.container = container;

    public object GetService(Type serviceType) =>
        serviceType.GetConstructors().Length > 0
            ? this.container.GetInstance(serviceType)
            : Activator.CreateInstance(serviceType, flag, null, null, null);
}

并且可以设置如下:

HttpRuntime.WebObjectActivator =
    new SimpleInjectorWebFormsServiceActivator(container);

此实现验证类型是否包含 public 构造函数,如果是,它将调用委托给将构造类型的简单注入器。否则,它将使用 Activator.CreateInstance 来构造类型。

请注意,使用此实现您 不需要 自定义 IConstructorSelectionBehavior,因此您可以完全删除 InternalConstructorResolutionBehavior