Search code examples
c#asp.net-web-apiinversion-of-controlautofacstructuremap

WebApi: Per Request Per Action DbSession using IoC, how?


Our existing database deployment has a single 'master' and a read-only replica. Using ASP.NET's Web API2 and an IoC container I want to create controller actions whose attribute (or lack there of) indicate which database connection is to be used for that request (See Controller and Services usage below)...

public MyController :  ApiController
{
    public MyController(IService1 service1, IService2 service2) { ... }

    // this action just needs the read only connection
    // so no special attribute is present
    public Foo GetFoo(int id)
    {
        var foo = this.service1.GetFoo(id);
        this.service2.GetSubFoo(foo);
        return foo;
    }

    // This attribute indicates a readwrite db connection is needed
    [ReadWrteNeeded]
    public Foo PostFoo(Foo foo)
    {
        var newFoo = this.service1.CreateFoo(foo);
        return newFoo;
    }
}

public Service1 : IService1
{
    // The dbSession instance injected here will be
    // based off of the action invoked for this request
    public Service1(IDbSession dbSession) { ... }

    public Foo GetFoo(int id) 
    { 
        return this.dbSession.Query<Foo>(...);
    }

    public Foo CreateFoo(Foo newFoo)
    {
        this.dbSession.Insert<Foo>(newFoo);
        return newFoo;
    }
}

I know how to setup my IoC (structuremap or Autofac) to handle per request IDbSession instances.

However, I'm not sure how I would go about making the type of IDbSession instance for the request to key off the indicator attribute (or lack there of) on the matching controller's action. I assume I will need to create an ActionFilter that will look for the indicator attribute and with that information identify, or create, the correct type of IDbSession (read-only or read-write). But how do I make sure that the created IDbSession's lifecycle is managed by the container? You don't inject instances into the container at runtime, that would be silly. I know Filters are created once at startup (making them singleton-ish) so I can't inject a value into the Filter's ctor.

I thought about creating an IDbSessionFactory that would have 'CreateReadOnlyDbSession' and 'CreateReadWriteDbSession' interfaces, but don't I need the IoC container (and its framework) to create the instance otherwise it can't manage its lifecycle (call dispose when the http request is complete).

Thoughts?

PS During development, I have just been creating a ReadWrite connection for every action, but I really want to avoid that long-term. I could also split out the Services methods into separate read-only and read-write classes, but I'd like to avoid that as well as placing GetFoo and WriteFoo in two different Service implementations just seems a bit wonky.

UPDATE:

I started to use Steven's suggestion of making a DbSessionProxy. That worked, but I was really looking for a pure IoC solution. Having to use HttpContext and/or (in my case) Request.Properties just felt a bit dirty to me. So, if I had to get dirty, I might as well go all the way, right?

For IoC I used Structuremap and WebApi.Structuremap. The latter package sets up a nested container per Http Request plus it allows you to inject the current HttpRequestMessage into a Service (this is important). Here's what I did...

IoC Container Setup:

For<IDbSession>().Use(() => DbSession.ReadOnly()).Named("ReadOnly");
For<IDbSession>().Use(() => DbSession.ReadWrite()).Named("ReadWrite");
For<ISampleService>().Use<SampleService>();

DbAccessAttribute (ActionFilter):

public class DbAccessAttribute : ActionFilterAttribute
{
    private readonly DbSessionType dbType;

    public DbAccessAttribute(DbSessionType dbType)
    {
        this.dbType = dbType;
    }

    public override bool AllowMultiple => false;

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var container = (IContainer)actionContext.GetService<IContainer>();
        var dbSession = this.dbType == DbSessionType.ReadOnly ?
                container.GetInstance<IDbSession>("ReadOnly") :
                container.GetInstance<IDbSession>("ReadWrite");

        // if this is a ReadWrite HttpRequest start an Request long
        // database transaction
        if (this.dbType == DbSessionType.ReadWrite)
        {
            dbSession.Begin();
        }

        actionContext.Request.Properties["DbSession"] = dbSession;
    }

    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
    {
        var dbSession = (IDbSession)actionExecutedContext.Request.Properties["DbSession"];
        if (this.dbType == DbSessionType.ReadWrite)
        {
            // if we are responding with 'success' commit otherwise rollback
            if (actionExecutedContext.Response != null &&
                actionExecutedContext.Response.IsSuccessStatusCode &&
                actionExecutedContext.Exception == null)
            {
                dbSession.Commit();                    
            }
            else
            {
                dbSession.Rollback();
            }
        }
    }
}

Updated Service1:

public class Service1: IService1
{
    private readonly HttpRequestMessage request;
    private IDbSession dbSession;

    public SampleService(HttpRequestMessage request)
    {
        // WARNING: Never attempt to access request.Properties[Constants.RequestProperty.DbSession]
        // in the ctor, it won't be set yet.
        this.request = request;
    }

    private IDbSession Db => (IDbSession)request.Properties["DbSession"];

    public Foo GetFoo(int id) 
    { 
        return this.Db.Query<Foo>(...);
    }

    public Foo CreateFoo(Foo newFoo)
    {
        this.Db.Insert<Foo>(newFoo);
        return newFoo;
    }
}

Solution

  • I assume I will need to create an ActionFilter that will look for the indicator attribute and with that information identify, or create, the correct type of IDbSession (read-only or read-write).

    With your current design, I would say an ActionFilter is the way to go. I do think however that a different design would serve you better, which is one where business operations are more explicitly modelled behind a generic abstraction, since you can in that case place the attribute in the business operation, and when you explicitly separate read operations from write operations (CQS/CQRS), you might not even need this attribute at all. But I'll consider this out of scope of your question right now, so that means an ActionFilter is the the way to go for you.

    But how do I make sure that the created IDbSession's lifecycle is managed by the container?

    The trick is let the ActionFilter store information about which database to use in a request-global value. This allows you to create a proxy implementation for IDbSession that is able to switch between a readable and writable implementation internally, based on this setting.

    For instance:

    public class ReadWriteSwitchableDbSessionProxy : IDbSession
    {
        private readonly IDbSession reader;
        private readonly IDbSession writer;
    
        public ReadWriteSwitchableDbSessionProxy(
            IDbSession reader, IDbSession writer) { ... }
    
        // Session operations
        public IQueryable<T> Set<T>() => this.CurrentSession.Set<T>();
    
        private IDbSession CurrentSession
        {
            get
            {
                var write = (bool)HttpContext.Current.Items["WritableSession"];
    
                return write ? this.writer : this.reader;
            }
        }
    }