WebApi:每个请求每个操作 DbSession 使用 IoC,怎么样?
WebApi: Per Request Per Action DbSession using IoC, how?
我们现有的数据库部署有一个 'master' 和一个只读副本。使用 ASP.NET 的 Web API2 和 IoC 容器,我想创建控制器操作,其属性(或缺少属性)指示哪个数据库连接将用于该请求(请参阅下面的控制器和服务用法)...
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;
}
}
我知道如何设置我的 IoC(结构图或 Autofac)来处理每个请求 IDbSession 实例。
但是,我不确定我将如何为请求创建 IDbSession 实例的类型,以关闭匹配控制器操作上的指示器属性(或缺少指示器属性)。我假设我需要创建一个 ActionFilter 来查找指示器属性,并使用该信息识别或创建正确类型的 IDbSession(只读或读写)。但是如何确保创建的 IDbSession 的生命周期是由容器管理的呢?您不会在运行时将实例注入容器,那会很愚蠢。我知道过滤器在启动时创建一次(使它们成为单例)所以我不能向过滤器的 ctor 中注入一个值。
我考虑过创建一个具有 'CreateReadOnlyDbSession' 和 'CreateReadWriteDbSession' 接口的 IDbSessionFactory,但我不需要 IoC 容器(及其框架)来创建实例,否则它不能管理其生命周期(在 http 请求完成时调用 dispose)。
想法?
PS 在开发过程中,我只是为每个操作创建一个读写连接,但我真的想长期避免这种情况。我也可以将 Services 方法拆分为单独的只读和读写 类,但我想避免这种情况,并将 GetFoo 和 WriteFoo 放在两个不同的 Service 实现中似乎有点靠不住。
更新:
我开始使用 Steven 的建议来制作 DbSessionProxy。这行得通,但我真的在寻找一个纯粹的 IoC 解决方案。必须使用 HttpContext and/or(在我的例子中)Request.Properties 对我来说感觉有点脏。所以,如果一定要弄脏,我还不如一路走下去,对吧?
对于 IoC,我使用了 Structuremap 和 WebApi.Structuremap。后一个包为每个 Http Request 设置了一个嵌套容器,而且它允许您将当前 HttpRequestMessage 注入 到服务中(这很重要)。这是我所做的...
IoC 容器设置:
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();
}
}
}
}
更新服务 1:
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;
}
}
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).
根据您当前的设计,我认为 ActionFilter 是可行的方法。但是,我确实认为另一种设计会更好地为您服务,即业务操作更多 explicitly modelled behind a generic abstraction,因为在这种情况下您可以将属性放在业务操作中,并且当您明确地将读取操作与写入操作分开时操作 (CQS/CQRS),您甚至可能根本不需要此属性。但我现在认为这超出了你的问题范围,所以这意味着 ActionFilter 是适合你的方式。
But how do I make sure that the created IDbSession's lifecycle is managed by the container?
诀窍是让 ActionFilter 存储有关在请求全局值中使用哪个数据库的信息。这允许您为 IDbSession
创建一个代理实现,它能够根据此设置在内部在可读和可写实现之间切换。
例如:
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;
}
}
}
我们现有的数据库部署有一个 'master' 和一个只读副本。使用 ASP.NET 的 Web API2 和 IoC 容器,我想创建控制器操作,其属性(或缺少属性)指示哪个数据库连接将用于该请求(请参阅下面的控制器和服务用法)...
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;
}
}
我知道如何设置我的 IoC(结构图或 Autofac)来处理每个请求 IDbSession 实例。
但是,我不确定我将如何为请求创建 IDbSession 实例的类型,以关闭匹配控制器操作上的指示器属性(或缺少指示器属性)。我假设我需要创建一个 ActionFilter 来查找指示器属性,并使用该信息识别或创建正确类型的 IDbSession(只读或读写)。但是如何确保创建的 IDbSession 的生命周期是由容器管理的呢?您不会在运行时将实例注入容器,那会很愚蠢。我知道过滤器在启动时创建一次(使它们成为单例)所以我不能向过滤器的 ctor 中注入一个值。
我考虑过创建一个具有 'CreateReadOnlyDbSession' 和 'CreateReadWriteDbSession' 接口的 IDbSessionFactory,但我不需要 IoC 容器(及其框架)来创建实例,否则它不能管理其生命周期(在 http 请求完成时调用 dispose)。
想法?
PS 在开发过程中,我只是为每个操作创建一个读写连接,但我真的想长期避免这种情况。我也可以将 Services 方法拆分为单独的只读和读写 类,但我想避免这种情况,并将 GetFoo 和 WriteFoo 放在两个不同的 Service 实现中似乎有点靠不住。
更新:
我开始使用 Steven 的建议来制作 DbSessionProxy。这行得通,但我真的在寻找一个纯粹的 IoC 解决方案。必须使用 HttpContext and/or(在我的例子中)Request.Properties 对我来说感觉有点脏。所以,如果一定要弄脏,我还不如一路走下去,对吧?
对于 IoC,我使用了 Structuremap 和 WebApi.Structuremap。后一个包为每个 Http Request 设置了一个嵌套容器,而且它允许您将当前 HttpRequestMessage 注入 到服务中(这很重要)。这是我所做的...
IoC 容器设置:
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();
}
}
}
}
更新服务 1:
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;
}
}
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).
根据您当前的设计,我认为 ActionFilter 是可行的方法。但是,我确实认为另一种设计会更好地为您服务,即业务操作更多 explicitly modelled behind a generic abstraction,因为在这种情况下您可以将属性放在业务操作中,并且当您明确地将读取操作与写入操作分开时操作 (CQS/CQRS),您甚至可能根本不需要此属性。但我现在认为这超出了你的问题范围,所以这意味着 ActionFilter 是适合你的方式。
But how do I make sure that the created IDbSession's lifecycle is managed by the container?
诀窍是让 ActionFilter 存储有关在请求全局值中使用哪个数据库的信息。这允许您为 IDbSession
创建一个代理实现,它能够根据此设置在内部在可读和可写实现之间切换。
例如:
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;
}
}
}