Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Autofac (+MVC + EF + SignalR + Hangfire) lifetime scopes

I have an ASP.NET MVC project which uses Entity Framwork, SignalR and Hangfire jobs.

My main (root) container is defined this way:

builder.RegisterType<DbContext>().InstancePerLifetimeScope(); // EF Db Context
builder.RegisterType<ChatService>().As<IChatService>().SingleInstance(); // classic "service", has dependency on DbContext
builder.RegisterType<ChatHub>().ExternallyOwned(); // SignalR hub
builder.RegisterType<UpdateStatusesJob>().InstancePerDependency(); // Hangfire job
builder.RegisterType<HomeController>().InstancePerRequest(); // ASP.NET MVC controller
IContainer container = builder.Build();

For MVC I'm using Autofac.MVC5 nuget package. Dependency resolver:

DependencyResolver.SetResolver(new AutofacDependencyResolver(container));

For SignalR I'm using Autofac.SignalR nuget package. Dependency resolver:

GlobalHost.DependencyResolver = new Autofac.Integration.SignalR.AutofacDependencyResolver(container);

My signalR hub is instantiated this way (http://autofac.readthedocs.org/en/latest/integration/signalr.html#managing-dependency-lifetimes):

private ILifetimeScope _hubScope;
protected IChatService ChatService;
public ChatHub(ILifetimeScope scope) {
  _hubScope = scope.BeginLifetimeScope(); // scope 
  ChatService = _hubScope.Resolve<IChatService>(); // this service is used in hub methods
}
protected override void Dispose(bool disposing)
{
  // Dipose the hub lifetime scope when the hub is disposed.
  if (disposing && _hubScope != null)
  {
    _hubScope.Dispose();
  }
  base.Dispose(disposing);
}

For Hangfire I'm using Hangfire.Autofac package:

config.UseActivator(new AutofacJobActivator(container));

Jobs are instantiated this way:

private readonly ILifetimeScope _jobScope;
protected IChatService ChatService;
protected BaseJob(ILifetimeScope scope)
{
    _jobScope = scope.BeginLifetimeScope();
    ChatService = _jobScope.Resolve<IChatService>();
}
public void Dispose()
{
    _jobScope.Dispose();
}

Question/problem: I always get same instance of DbContext in hubs and jobs. I want that all hub instances will get the same ChatService, but the DbContext (which is dependency of ChatService) will always be a new instance. Also Hangfire jobs should act the same.

Can this be done, or am I missing something?

Update 1:

After thinking (and sleeping over) I think I have two choices. I still want to to keep "session per request" ("session per hub", "session per job").

Option 1:

Change that all services will have InstancePerLifetimeScope. Instantiation of services is not expensive. For the services which maintains some kind of state I would create another "storage" (class) which will be SingleInstance and will not have dependency on session (DbContext). I think this will work for hubs and jobs also.

Option 2:

Create some kind of factory which was suggested by @Ric .Net. Something like this:

public class DbFactory: IDbFactory
{
    public MyDbContext GetDb()
    {
        if (HttpContext.Current != null)
        {
            var db = HttpContext.Current.Items["db"] as MyDbContext;
            if (db == null)
            {
                db = new MyDbContext();
                HttpContext.Current.Items["db"] = db;
            }
            return db;
        }

        // What to do for jobs and hubs?
        return new MyDbContext();
    }
}

    protected void Application_EndRequest(object sender, EventArgs e)
    {
        var db = HttpContext.Current.Items["db"] as MyDbContext;
        if (db != null)
        {
            db.Dispose();
        }
    }

I think that this would work for MVC, but I don't know hot to get it working for hubs (every hub call is new instance of the hub) and jobs (every run of a job is a new instance of the job class).

I am leaning towards option 1. What do you think?

Many thanks!

like image 762
rrejc Avatar asked Mar 16 '15 20:03

rrejc


1 Answers

I'm completely unexperienced with AutoFac. But what got my attention was:

I want that all hub instances will get the same ChatService, but the DbContext (which is dependency of ChatService) will always be a new instance.

What you're basically saying here is:

"My car is in maintenance by the same the car company which have a dependency on their garage, but everytime I bring my car I want the garage to be a new one".

When you inject the (completely build instance, including dependencies of) ChatService in some other component, of course other dependencies it has will be build also, whether they have an other kind of lifestyle or not. When an object with a shorter lifetime than the object it is injected in is created, you've created a so called 'captive dependency'

The only way to get a new 'instance' of the DbContext in your ChatService is not to inject the DbContext itself but by injecting a DbContextFactory which creates the DbContext for you whenever you use it.

An implementation would look something like:

public class DbContextFactory
{
    public DbContext Create()
    {
         return new DbContext();
    }
}

//usage:
public class ChatService
{
     private readonly DbContextFactory dbContextFactory;

     public ChatService(DbContextFactory dbContextFactory)
     {
         this.dbContextFactory = dbContextFactory;
     }

    public void SomeMethodInChatService()
    {
         using (var db = this.dbContextFactory.Create())
         {
             //do something with DbContext    
         }
     }
}

The DbContextFactory could be registered in AutoFac using the Singleton Lifestyle.

This maybe is however not what you aim for. Because in this case every time you use the DbContext you get a new one. On the other hand, a new DbContext is probably the safest way to approach this, as you can read here.

This great answer is worth reading for more than one reason because it has an explanation of how to use the command / handler pattern which should be very suitable for your situation.

This would make your chatservice completely unknowing of the DbContext which improves the 'SOLID' design of your application and creates the possibilty to test the ChatService which is something that is practically undoable when injecting a DbContext or DbContextFactory directly.

like image 96
Ric .Net Avatar answered Sep 28 '22 07:09

Ric .Net