Wiring up Simple Injector in WebForms in .NET 4.7.

2020-03-19 14:47发布

问题:

With the changes in .NET 4.7.2, constructor injection is now possible in Web Forms. I have gotten Simple Injector working with Web Forms, but would like some input as to if there any "gotchas" I might be missing.

First I have the registration of the Pages themselves which is taken from 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);
    }
}

This has worked when using the property injection method from the link above just fine. I am including it here for completeness.

When I wired it up the first time, there was an issue with one OutputCacheModule having an internal constructor. Using the code from here I was able to fix that issue and any others that might have arisen from internal constructors. Here is the code for that implementation for completeness.

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);
    }
}

Now with the backstory out of the way, here is the meat of the question. This is the custom activator I have wired up.

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);
    }
}

The question is, is the GetService method enough? There is very little out there right now about how to use the new extension point for WebForms. There is an Autofac example that is significantly more complex than my simple one line pass through to Simple Injector, but as I am not familiar with Autofac I don't know how much of that is for the container.

Right now the solution works. Pages load without error. The container passes the call to Verify.

Is this enough or is there more work to be done? Are there any "gotchas" that I am missing? I am not very familiar with the deeper inner workings of ether Simple Injector or WebForms, so I'm worried I might be missing something huge.

As of right now there is no need nor plans for there to be any scoped containers.

回答1:

IMO, this new feature in Web Forms is not particularly well thought through. The main problem is that Web Forms breaks the IServiceProvider contract.

The IServiceProvider.GetService method defines that null should be returned if no such service exists. But once you actually return null, e.g. when you can’t construct that type, Web Forms throws a NullReferenceException from deep down its stack.

Would Web Forms, on the other hand, have conformed to the IServiceProvider abstraction, plugging in Simple Injector would have been a matter of a single statement, since SimpleInjector.Container actually implements IServiceProvider:

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

On top of this, when an IServiceProvider is set through HttpRuntime.WebObjectActivator, Web Forms will call it for almost everything, even for its own internal objects, which, to me, makes little sense.

Therefore, instead of supplying an IServiceProvider implementation that is compatible to the IServiceProvider contract, you will have to provide a special ASP.NET Web Forms-compatible IServiceProvider implementation (which therefore breaks the contract).

Note that most DI Containers actually implement IServiceProvider, but you would see most of them fail, because of this contract breach.

An adapter implementation would look like this:

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);
}

And can be set as follows:

HttpRuntime.WebObjectActivator =
    new SimpleInjectorWebFormsServiceActivator(container);

This implementation verifies whether the type contains public constructors and if so, it delegates the call to Simple Injector, which will construct the type. Otherwise, it will use Activator.CreateInstance to construct the type.

Do note that using this implementation you don’t need custom IConstructorSelectionBehavior, so you can remove your InternalConstructorResolutionBehavior altogether.



回答2:

We will publish a Adapter(Activator) nupkg(and a blog soon) which uses Unity container and will also open source it. Here are some general guidance of implementing Adapter(Activator).

  1. If the IoC container can't resolve a serviceType, you may consider to cache that type. Next time you can create the instance through reflection directly to get some perf gain.
  2. You may preserve the Adapter(Activator) registered before your registration and try that Adapter(Activator) if yours can't resolve the serviceType.
  3. If IoC container implements IDisposable, your Adapter(Activator) should implement IRegisteredObject where Container.Dispose can be called.