Question

I'm trying to move from Ninject to Simple Injector but I'm experiencing an odd issue when trying to duplicate functionality that worked with Ninject.

In Ninject I had a service which contained:

private readonly ICollection<Message> messages;

This service was registered as

Bind<INotificationService>().To<NotificationService>()
    .InRequestScope();

This service allowed messages (UI and error) to be passed back to the MVC site.

This service was injected into an ActionFilterAttribute:

kernel.BindFilter<CriticalErrorAttribute>(FilterScope.Last, 1)
    .When((context, ad) => 
        !string.IsNullOrEmpty(ad.ActionName) &&
        ad.ControllerDescriptor.ControllerName.ToLower() != "navigation");

and used within OnActionExecuted.

Because the service was registered to Ninject with InRequestScope, any items pushed to the message queue were available in the Actionfiter. This allowed for a redirect to an error page (displaying critical errors) if necessary.

I've tried to duplicate this with simpleinjector:

container.RegisterPerWebRequest<INotificationService, NotificationService>();

container.RegisterInitializer<CriticalErrorAttribute>(handler =>
{
    handler.NotificationService =
        container.GetInstance<INotificationService>();
});

The injection is working fine, but even though the message collection contains messages prior to entering the ActionFilter, once in the filter the message collection is empty. It's like the RegisterPerWebRequest is being ignored.

Any help in solving this issues would be appreciated.

Was it helpful?

Solution

UPDATE:

In Simple Injector 2.5 a new RegisterMvcIntegratedFilterProvider extension method has been added to the MVC Integration package that replaces the old RegisterMvcAttributeFilterProvider. This new RegisterMvcIntegratedFilterProvider contains the behavior of the SimpleInjectorFilterAttributeFilterProvider that is given below and allows better integration of attributes into the Simple Injector pipeline. This does mean however that by default, no properties are injected, but this can extended by implementing a custom IPropertySelectionBehavior. The use of the new RegisterMvcIntegratedFilterProvider is adviced over the old RegisterMvcAttributeFilterProvider method, which will be marked [Obsolete] in a future release.


When using the RegisterMvcAttributeFilterProvider extension method, Simple Injector will not call any registered initializer on MVC attributes. If you set a break point inside the anonymous delegate that injects the NotificationService you'll see it's never hit.

Simple Injector does however call the container.InjectProperties method on MVC attributes, but InjectProperties does implicit property injection, which means that it tries to inject all public properties on a type, but skips it if the property can't be injected (for what ever reason).

I bet the CriticalErrorAttribute.NotificationService property has a type of NotificationService instead of INotificationService. Since you didn't register NotificationService explicitly, the container will create a transient instance for you, which means you'll get a different instance for the CriticalErrorAttribute than the rest of the application is getting.

Quick fix: change the property type to INotificationService.

To be honest, I regret ever implemented the MVC integration package for Simple Injector to use the InjectProperties method. Implicit Property injection is very evil, because it doesn't fail fast when there's a misconfiguration and I'm even thinking about removing support for InjectProperties in the future. The problem is however that many developers are depending on InjectProperties. Either directly by calling it, or indirectly by letting the container inject properties on MVC attributes.

InjectProperties does not run any initializer. That's by design, and there are other constructs that allow running the full initialization process on objects that are not created by the container. Problem is however, that adding this could break existing clients, since this could result in properties being injected multiple times.

In your case, I suggest a different solution:

Prevent calling container.RegisterMvcAttributeFilterProvider() in the startup path of your application. This will register a special FilterAttributeFilterProvider that calls InjectProperties internally. You don't want to use implicit property injection, you want a more explicit (and complete) behavior. Instead register the following class:

internal sealed class SimpleInjectorFilterAttributeFilterProvider
    : FilterAttributeFilterProvider
{
    private readonly ConcurrentDictionary<Type, Registration> registrations =
        new ConcurrentDictionary<Type, Registration>();

    private readonly Func<Type, Registration> registrationFactory;

    public SimpleInjectorFilterAttributeFilterProvider(Container container)
        : base(false)
    {
        this.registrationFactory = type => 
            Lifestyle.Transient.CreateRegistration(type, container);
    }

    public override IEnumerable<Filter> GetFilters(
        ControllerContext context,
        ActionDescriptor descriptor)
    {
        var filters = base.GetFilters(context, descriptor).ToArray();

        foreach (var filter in filters)
        {
            object instance = filter.Instance;

            var registration = registrations.GetOrAdd(
                instance.GetType(), this.registrationFactory);

            registration.InitializeInstance(instance);                
        }

        return filters;
    }
}

You can use the following code to register this custom provider:

var filterProvider = 
    new SimpleInjectorFilterAttributeFilterProvider(container);

container.RegisterSingle<IFilterProvider>(filterProvider);

var providers = FilterProviders.Providers
    .OfType<FilterAttributeFilterProvider>().ToList();

providers.ForEach(provider => FilterProviders.Providers.Remove(provider));

FilterProviders.Providers.Add(filterProvider);

This custom SimpleInjectorFilterAttributeFilterProvider calls the Registration.InitializeInstance method. This method allows initialization a type that is already created and will initialize it by (among other things) calling the type initializer delegates.

For more information about working with attributes, please read the following discussion.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top