Search code examples
asp.net-mvcentity-frameworkdependency-injectionioc-containersimple-injector

MVC - Simple Injector and Attribute calling the Context (EF) Throwing exceptions


If I start my application and let it settle, it works great.

However, when I debug my application and if I close the browser tab before it initializes anything and then call another like localhost:81/Home/Test, it throws an exception on retrieving data from DB (EF).

This exception occurs during a call to a Filter CultureResolver which then calls LanguageService. Inside LanguageService there is a call to the DB to retrieve all the available languages.

I got many different exceptions, like:

  • The context cannot be used while the model is being created. This exception may be thrown if the context is used inside the OnModelCreating method or if the same context instance is accessed by multiple threads concurrently. Note that instance members of DbContext and related classes are not guaranteed to be thread safe.
  • Object reference not set to an instance of an object.
  • The underlying provider failed on Open.

Those exceptions occur all in the same query, it depends on how much time I left the first tab running.

So it seems it's something like Thread-Unsafe code or this query trying to get items before the Context is initialized.

I've the following:

SimpleInjectorInitializer.cs

public static class SimpleInjectorInitializer
{
    /// <summary>Initialize the container and register it as MVC3 Dependency Resolver.</summary>
    public static void Initialize()
    {
        var container = new Container();
        container.Options.DefaultScopedLifestyle = new WebRequestLifestyle();

        InitializeContainer(container);
        container.RegisterMvcControllers(Assembly.GetExecutingAssembly());
        container.Verify();
        DependencyResolver.SetResolver(new SimpleInjectorDependencyResolver(container));

        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters, container);
    }

    private static void InitializeContainer(Container container)
    {
        container.Options.DefaultScopedLifestyle = new WebRequestLifestyle();

        /* Bindings... */

        container.RegisterPerWebRequest<IAjaxMessagesFilter, AjaxMessagesFilter>();
        container.RegisterPerWebRequest<ICustomErrorHandlerFilter, CustomErrorHandlerFilter>();
        container.RegisterPerWebRequest<ICultureInitializerFilter, CultureInitializerFilter>();
    }
}

FilterConfig.cs

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters, Container container)
    {
        filters.Add(container.GetInstance<ICultureInitializerFilter>());
        filters.Add(container.GetInstance<ICustomErrorHandlerFilter>());
        filters.Add(container.GetInstance<IAjaxMessagesFilter>());
    }
}

CultureResolver.cs

public class CultureResolver : ICultureResolver
{
    ILanguageService Service;
    public CultureResolver(ILanguageService Service)
    {
        this.Service = Service;
    }

    public string Resolve(string CultureCode)
    {
        // Get the culture by name or code (pt / pt-pt)
        ILanguageViewModel language = Service.GetByNameOrCode(CultureCode);

        if (language == null)
        {
            // Get the default language
            language = Service.GetDefault();
        }

        return language.Code;
    }
}

LanguageService.cs

public class LanguageService : ILanguageService
{
    IMembership membership;
    ChatContext context;
    ILanguageConverter converter;

    public LanguageService(
            ChatContext context,
            IMembership membership,
            ILanguageConverter converter
        )
    {
        this.membership = membership;
        this.context = context;
        this.converter = converter;
    }

    public virtual ILanguageViewModel GetByNameOrCode(string Text)
    {
        string lowerText = Text.ToLower();
        string lowerSmallCode = "";

        int lowerTextHiphen = lowerText.IndexOf('-');
        if (lowerTextHiphen > 0)
            lowerSmallCode = lowerText.Substring(0, lowerTextHiphen);

        Language item = this.context
                            .Languages
                            .FirstOrDefault(x => x.Code.ToLower() == lowerText
                                                 || x.SmallCode.ToLower() == lowerText
                                                 || x.SmallCode == lowerSmallCode);
        return converter.Convert(item);
    }

    public virtual ILanguageViewModel GetDefault()
    {
        Language item = this.context
                            .Languages
                            .FirstOrDefault(x => x.Default);
        return converter.Convert(item);
    }
}

This is the query that is giving me the exceptions

Language item = this.context
                    .Languages
                    .FirstOrDefault(x => x.Code.ToLower() == lowerText
                                         || x.SmallCode.ToLower() == lowerText
                                         || x.SmallCode == lowerSmallCode);

Solution

  • Global filters in MVC and Web API are singletons. There is only one instance of such filter during the lifetime of your application. This becomes obvious when you look at the following code:

    filters.Add(container.GetInstance<ICultureInitializerFilter>());
    

    Here you resolve the filter once from the container and store it for the lifetime of the container.

    You however, have registered this type as Scoped using:

    container.RegisterPerWebRequest<ICultureInitializerFilter, CultureInitializerFilter>();
    

    You are effectively saying that there should be one instance per web request, most likely because that class depends on a DbContext, which isn't thread-safe.

    To allow your filters to have dependencies, you should either make them humble objects, or wrap them in a humble object that can call them. For instance, you can create the following action filter:

    public sealed class GlobalActionFilter<TActionFilter> : IActionFilter 
        where TActionFilter : class, IActionFilter
    {
        private readonly Container container;
        public GlobalActionFilter(Container container) { this.container = container; }
    
        public void OnActionExecuted(ActionExecutedContext filterContext) {
            container.GetInstance<TActionFilter>().OnActionExecuted(filterContext);
        }
    
        public void OnActionExecuting(ActionExecutingContext filterContext) {
            container.GetInstance<TActionFilter>().OnActionExecuting(filterContext);
        }
    }
    

    This class allows you to add your global filters as follows:

    filters.Add(new GlobalActionFilter<ICultureInitializerFilter>(container));
    filters.Add(new GlobalActionFilter<ICustomErrorHandlerFilter>(container));
    filters.Add(new GlobalActionFilter<IAjaxMessagesFilter>(container));
    

    The GlovalActionFilter<T> will callback into the container to resolve the supplied type every time it is called. This prevents the dependency from becoming captive which prevents the problems you are having.