VahidN/AspNetIdentityDependencyInjectionSample

Implementing Container Per Request pattern

Closed this issue · 1 comments

I'm going to add in the necessary infrastructure to bind each request to its own nested container, So in this case having a Container Per Request gives us a unique session (Context Per Request). But the code doesn't give us this ability, If you just set a break point in the ApplicationDbContext's constructor, You can see each time an instance is created. For example in my case I have these controllers:

public partial class HomeController : Controller
{
        private readonly IUnitOfWork _uow;

        public HomeController(IUnitOfWork uow)
        {
            _uow = uow;
        }
        public virtual ActionResult Index()
        {
            return View();
        }
}
public class TestController : Controller
{
        private readonly IUnitOfWork _uow;

        public TestController(IUnitOfWork uow)
        {
            _uow = uow;
        }

        public ActionResult GetData()
        {
            return Content("Data");
        }
}

So the view returned by Index action uses this code to pull in content from TestController:

@Html.Action("GetData", "Test") 

In that example, several instances are created per request!
So as you mentioned in your blog post, the HTML.Action starts its lifecycle from scratch!
I've made changes to the SmObjectFactory:

public class NewObjectFactory
    {
        public static IContainer Container { get; set; }

        static NewObjectFactory()
        {
            Container = new Container();
            Container.Configure(ioc =>
            {
                ioc.AddRegistry<AutomapperRegistry>();
                ioc.Scan(scan =>
                {
                    scan.AssemblyContainingType<CusomProfile>();
                    scan.WithDefaultConventions();
                    scan.AddAllTypesOf<Profile>().NameBy(item => item.FullName);
                });
                ioc.For<IUnitOfWork>()
                   .HybridHttpOrThreadLocalScoped()
                   .Use<ApplicationDbContext>()
                    // Remove these 2 lines if you want to use a connection string named connectionString1, defined in the web.config file.
                   .Ctor<string>("connectionString")
                   .Is("Data Source=(local);Initial Catalog=TestDbIdentity;Integrated Security = true");
                /*.Ctor<string>().Is(HttpContext.Current.User.Identity.Name ?? "anonymouse");*/

                ioc.For<ApplicationDbContext>().HybridHttpOrThreadLocalScoped()
                   .Use(context => (ApplicationDbContext)context.GetInstance<IUnitOfWork>());
                ioc.For<DbContext>().HybridHttpOrThreadLocalScoped()
                   .Use(context => (ApplicationDbContext)context.GetInstance<IUnitOfWork>());

                ioc.For<IUserStore<ApplicationUser, int>>()
                    .HybridHttpOrThreadLocalScoped()
                    .Use<UserStore<ApplicationUser, CustomRole, int, CustomUserLogin, CustomUserRole, CustomUserClaim>>();

                ioc.For<IRoleStore<CustomRole, int>>()
                    .HybridHttpOrThreadLocalScoped()
                    .Use<RoleStore<CustomRole, int, CustomUserRole>>();

                ioc.For<IAuthenticationManager>()
                      .Use(() => HttpContext.Current.GetOwinContext().Authentication);

                ioc.For<IApplicationSignInManager>()
                      .HybridHttpOrThreadLocalScoped()
                      .Use<ApplicationSignInManager>();

                ioc.For<IApplicationRoleManager>()
                      .HybridHttpOrThreadLocalScoped()
                      .Use<ApplicationRoleManager>();

                // map same interface to different concrete classes
                ioc.For<IIdentityMessageService>().Use<SmsService>();
                ioc.For<IIdentityMessageService>().Use<EmailService>();

                ioc.For<IApplicationUserManager>().HybridHttpOrThreadLocalScoped()
                   .Use<ApplicationUserManager>()
                   .Ctor<IIdentityMessageService>("smsService").Is<SmsService>()
                   .Ctor<IIdentityMessageService>("emailService").Is<EmailService>()
                   .Setter<IIdentityMessageService>(userManager => userManager.SmsService).Is<SmsService>()
                   .Setter<IIdentityMessageService>(userManager => userManager.EmailService).Is<EmailService>();

                ioc.For<ApplicationUserManager>().HybridHttpOrThreadLocalScoped()
                   .Use(context => (ApplicationUserManager)context.GetInstance<IApplicationUserManager>());

                ioc.For<ICustomRoleStore>()
                      .HybridHttpOrThreadLocalScoped()
                      .Use<CustomRoleStore>();

                ioc.For<ICustomUserStore>()
                      .HybridHttpOrThreadLocalScoped()
                      .Use<CustomUserStore>();

                //config.For<IDataProtectionProvider>().Use(()=> app.GetDataProtectionProvider()); // In Startup class
                // custom config

                ioc.Policies.SetAllProperties(y =>
                {
                    y.OfType<IUnitOfWork>();
                    y.OfType<IApplicationUserManager>();
                });

                configureAutoMapper(Container);
            });
        }
        private static void configureAutoMapper(IContainer container)
        {
            // automapper stuff
        }
    }

So in the Global.asax I've added these lines of code for using nested container:

public IContainer Container
{
            get
            {
                return (IContainer)HttpContext.Current.Items["_Container"];
            }
            set
            {
                HttpContext.Current.Items["_Container"] = value;
            }
}
public void Application_BeginRequest()
{
            Container = NewObjectFactory.Container.GetNestedContainer();
        }
        public void Application_EndRequest()
        {
                Container.Dispose();
                Container = null;
}

And inside Application_Start:

DependencyResolver.SetResolver(
                new StructureMapDependencyResolver(() => Container ?? NewObjectFactory.Container));

And in the DependencyResolver I've implemented the factory function this way:

public class StructureMapDependencyResolver : IDependencyResolver
{
            private readonly Func<IContainer> _factory;

            public StructureMapDependencyResolver(Func<IContainer> factory)
            {
                _factory = factory;
            }

            public object GetService(Type serviceType)
            {
                if (serviceType == null)
                {
                    return null;
                }

                var factory = _factory();

                return serviceType.IsAbstract || serviceType.IsInterface
                    ? factory.TryGetInstance(serviceType)
                    : factory.GetInstance(serviceType);
            }

            public IEnumerable<object> GetServices(Type serviceType)
            {
                return _factory().GetAllInstances(serviceType).Cast<object>();
            }
}

Finaly when I run the application I get this error:
No default Instance is registered and cannot be automatically determined for type 'Microsoft.Owin.Security.DataProtection.IDataProtectionProvider'
But when I comment this line in the Startup.cs file:

ConfigureAuth(app)

everything works and this time the ApplicationDbContext is created once and then disposed. So that's what I want: Only a single instance of the context is created now and it is correctly disposed of at the end of the web request, It means that the nested container reused the context to satisfy the dependencies of both controllers. :)
Any idea?

I couldn't reproduce it. You need to investigate this issue using DNTProfiler and not just by adding break-points. Because at the beginning, EF issues too many updates itself.
There are no duplicate contexts here:
id01
And there are multiple contexts per request, only at the startup and it's OK.
id2