Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Managing RavenDB IDocumentSession lifecycles with StructureMap for NServiceBus and MVC

I am using NServiceBus v4.3, MVC4, RavenDB 2.5 and StructureMap 2.6.4 in our solution.

I am having a similar issue under StructureMap to that described in this question's responses where I require different lifecycles for the MVC Controller and NServiceBus Handler use of RavenDB's IDocumentSession in my Web project.

Specifically in my case what happens is that if I use the HybridHttpOrThreadLocalScoped (as the above answer suggests for Windsor) lifecycle the sessions are not properly disposed of and I soon hit the 30 transaction limit error. If I use the HttpContext lifecycle my NSB event Handlers in the Web project do not get called.

In my Controllers the session is wrapped in a unit of work applied via an MVC ActionFilter. I also use the UoW within the Handlers as my Registry is wired up to retrieve the session from the UoW. The code is as such:

RavenDbWebRegistry.cs

public sealed class RavenDbWebRegistry : Registry
{
    public RavenDbWebRegistry()
    {
        // register RavenDB document store
        ForSingletonOf<IDocumentStore>().Use(() =>
        {
            var documentStore = new DocumentStore
            {
                ConnectionStringName = "RavenDB",
                Conventions =
                {
                    IdentityPartsSeparator = "-", 
                    JsonContractResolver = new PrivatePropertySetterResolver(),
                },

            };
            documentStore.Initialize();

            return documentStore;
        });


        For<IDocumentSession>().HybridHttpOrThreadLocalScoped().Add(ctx =>
        {
            var uow = (IRavenDbUnitOfWork)ctx.GetInstance<IUnitOfWork>();
            return uow.DocumentSession;
        });

        For<IUnitOfWork>().HybridHttpOrThreadLocalScoped().Use<WebRavenDbUnitOfWork>();            

    }
}

Example of Web project Handler:

public class SiteCreatedEventHandler : IHandleMessages<ISiteCreatedEvent>
{
    public IBus Bus { get; set; }
    public IUnitOfWork Uow { get; set; }
    public IDocumentSession DocumentSession { get; set; }

    public void Handle(ISiteCreatedEvent message)
    {
        try
        {
            Debug.Print(@"{0}{1}", message, Environment.NewLine);

            Uow.Begin();
            var site = DocumentSession.Load<Site>(message.SiteId);
            Uow.Commit();

            //invoke Hub and push update to screen
            var context = GlobalHost.ConnectionManager.GetHubContext<AlarmAndNotifyHub>();

            //TODO make sure this SignalR function is correct
            context.Clients.All.displayNewSite(site, message.CommandId);
            context.Clients.All.refreshSiteList();            
        }
        catch (Exception ex)
        {                
            Uow.Rollback();
        }            
    }
}

Usage of ActionFilter:

    [RavenDbUnitOfWork]
    public ViewResult CreateNew(int? id)
    {
        if (!id.HasValue || id.Value <= 0)
            return View(new SiteViewModel { Guid = Guid.NewGuid() });

        var targetSiteVm = MapSiteToSiteViewModel(SiteList(false)).FirstOrDefault(s => s.SiteId == id.Value);

        return View(targetSiteVm);
    }

WebRegistry (that sets up NSB in my MVC project)

public sealed class WebRegistry : Registry
{
    public WebRegistry()
    {
        Scan(x =>
        {
            x.TheCallingAssembly();
            x.Assembly("IS.CommonLibrary.ApplicationServices");
            x.LookForRegistries();
        });

        IncludeRegistry<RavenDbWebRegistry>();

        FillAllPropertiesOfType<IUnitOfWork>();
        FillAllPropertiesOfType<IDocumentSession>();
        FillAllPropertiesOfType<StatusConversionService>();
        FillAllPropertiesOfType<IStateRepository<TieState>>();
        FillAllPropertiesOfType<IStateRepository<DedState>>();
        FillAllPropertiesOfType<ITieService>();
        FillAllPropertiesOfType<IDedService>();
        FillAllPropertiesOfType<IHbwdService>();

        //NServiceBus
        ForSingletonOf<IBus>().Use(
        NServiceBus.Configure.With()
            .StructureMapBuilder()
            .DefiningCommandsAs(t => t.Namespace != null && t.Namespace.EndsWith("Command"))
            .DefiningEventsAs(t => t.Namespace != null && t.Namespace.EndsWith("Event"))
            .DefiningMessagesAs(t => t.Namespace == "Messages")
            .RavenPersistence("RavenDB")
            .UseTransport<ActiveMQ>()
            .DefineEndpointName("IS.Argus.Web")
            .PurgeOnStartup(true)
            .UnicastBus()
            .CreateBus()
            .Start(() => NServiceBus.Configure.Instance
            .ForInstallationOn<Windows>()
            .Install())
        );


        //Web             
        For<HttpContextBase>().Use(() => HttpContext.Current == null ? null : new HttpContextWrapper(HttpContext.Current));
        For<ModelBinderMappingDictionary>().Use(GetModelBinders());
        For<IModelBinderProvider>().Use<StructureMapModelBinderProvider>();
        For<IFilterProvider>().Use<StructureMapFilterProvider>();
        For<StatusConversionService>().Use<StatusConversionService>();
        For<ITieService>().Use<TieService>();
        For<IDedService>().Use<DedService>();
        For<IHbwdService>().Use<HbwdService>();
        For<ISiteService>().Use<SiteService>();

        IncludeRegistry<RedisRegistry>();
    }

I have tried configuring my Registry using every possible combination I can think of to no avail.

Given that the StructureMap hybrid lifecycle does not work as I would expect, what must I do to achieve the correct behaviour?

Is the UoW necessary/beneficial with RavenDB? I like it (having adapted it from my earlier NHibernate UoW ActionFilter) because of the way it manages the lifecycle of my sessions within Controller Actions, but am open to other approaches.

What I would ideally like is a way to - within the Web project - assign entirely different IDocumentSessions to Controllers and Handlers, but have been unable to work out any way to do so.

like image 598
tker Avatar asked Nov 01 '22 00:11

tker


1 Answers

Firstly, RavenDB already implements unit of work by the wrapping IDocumentSession, so no need for it. Opening a session, calling SaveChanges() and disposing has completed the unit of work

Secondly, Sessions can be implemented in a few ways for controllers.

The general guidance is to set up the store in the Global.asax.cs. Since there is only 1 framework that implements IDocumentSession - RavenDB, you might as well instantiate it from the Global. If it was NHibernate or Entity Framework behind a repository, I'd understand. But IDocumentSession is RavenDB specific, so go with a direct initialization in the Application_Start.

public class Global : HttpApplication
{
   public void Application_Start(object sender, EventArgs e)
   {
      // Usual MVC stuff

      // This is your Registry equivalent, so insert it into your Registry file 
      ObjectFactory.Initialize(x=> 
      {
         x.For<IDocumentStore>()
          .Singleton()
          .Use(new DocumentStore { /* params here */ }.Initialize());
   }

   public void Application_End(object sender, EventArgs e)
   {
      var store = ObjectFactory.GetInstance<IDocumentStore>();

      if(store!=null)
         store.Dispose();
   }
}

In the Controllers, add a base class and then it can open and close the sessions for you. Again IDocumentSession is specific to RavenDB, so dependency injection doesn't actually help you here.

public abstract class ControllerBase : Controller
{
   protected IDocumentSession Session { get; private set; }

   protected override void OnActionExecuting(ActionExecutingContext context)
   {
      Session = ObjectFactory.GetInstance<IDocumentStore>().OpenSession();
   }

   protected override void OnActionExecuted(ActionExecutedContext context)
   {
      if(this.IsChildAction)
         return;

      if(content.Exception != null && Session != null)
         using(context)
            Session.SaveChanges();
   }
}

Then from there, inherit from the base controller and do your work from there:

public class CustomerController : ControllerBase
{
   public ActionResult Get(string id)
   {
      var customer = Session.Load<Customer>(id);

      return View(customer);
   }

   public ActionResult Edit(Customer c)
   {
      Session.Store(c);

      return RedirectToAction("Get", c.Id);
   }
 }

Finally, I can see you're using StructureMap, so it only takes a few basic calls to get the Session from the DI framework:

public class SiteCreatedEventHandler : IHandleMessages<ISiteCreatedEvent>
{
    public IBus Bus { get; set; }
    public IUnitOfWork Uow { get; set; }
    public IDocumentSession DocumentSession { get; set; }

    public SiteCreatedEventHandler()
    {
       this.DocumentSession = ObjectFactory.GetInstance<IDocumentStore>().OpenSession();
    }

    public void Handle(ISiteCreatedEvent message)
    {
       using(DocumentSession)
       {
          try
          {
             Debug.Print(@"{0}{1}", message, Environment.NewLine);

             ///// Uow.Begin(); // Not needed for Load<T>

             var site = DocumentSession.Load<Site>(message.SiteId);

             //// Uow.Commit(); // Not needed for Load<T>

             // invoke Hub and push update to screen
             var context = GlobalHost.ConnectionManager.GetHubContext<AlarmAndNotifyHub>();

             // TODO make sure this SignalR function is correct
             context.Clients.All.displayNewSite(site, message.CommandId);
             context.Clients.All.refreshSiteList();            
         }
         catch (Exception ex)
         {                
             //// Uow.Rollback(); // Not needed for Load<T>
         }            
      }
    }
like image 75
Dominic Zukiewicz Avatar answered Nov 17 '22 10:11

Dominic Zukiewicz