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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With