Reputation: 558
I'm building a .Net Web Api which uses a Service/Repository pattern w/ Entity Framework. Below is a controller with CRUD actions that interface with a Service:
public class SomeController : BaseApiController
{
private IService _service;
public SomeController(IService _service)
{
_service = service;
}
public object Get() { return _service.GetItems(); }
...
}
I want to use Microsoft Unity IoC to inject a database context into the service constructor below:
// Service implements IService
public Service(SomeContext ctx) : base(ctx)
{
_alpha = new AlphaRepository(ctx);
_bravo = new BravoRepository(ctx);
}
This works well for me with a single, static DbContext. However, the Api must use a dynamic DbContext b/c the Server and Database are not known until a Request is made and various config data is passed via query string, e.g. ?client=Client&property=Property. Every client has its own Database and each Database is located on one of two servers.
There is an internally developed NuGet package w/ exposed ContextFactory which may be called to retrieve the appropriate DbContext when a Request is made:
ContextFactory.GetSomeContext(client, prop);
At first I thought to use an ActionFilter on the BaseController to parse the HTTPActionContext Request's query string for the Client and Property keys. With this info, a context could be retrieved and registered with the Unity Container:
// Belongs to 'public class SomeFilterAttribute : ActionFilterAttribute'
public override void OnActionExecuting(HttpActionContext actionContext)
{
var someContext = ContextFactory.GetSomeContext(client, prop);
private IUnityContainer _unityContainer;
_unityContainer = (IUnityContainer)actionContext.Request.GetDependencyScope().GetService(typeof(IUnityContainer));
_unityContainer.RegisterInstance<SomeContext>(someContext, new PerThreadLifetimeManager());
...
}
This plan fell short when I realized the ActionFilter is executed after SomeController is initialized and so its constructor and the Service constructor have already completed execution and it is too late to register the DbContext.
Question: What is the best way to retrieve a dynamic DbContext and use Unity to inject this instance into a Service constructor?
I've read about using delegates, e.g.
public delegate ISomeContext CreateSomeContext(string client, string prop);
and then in UnityConfig.cs RegisterComponents()
container.RegisterInstance<CreateSomeContext>((c, p) => ContextFactory.GetSomeContext(c, p));
but I'm not sure how to actually provide the client and property at runtime from the query string.
Thanks for any help!
Upvotes: 1
Views: 1038
Reputation: 34698
This sounds like you want to use a database-per-tenant multi-tenant structure. The first thing would be to avoid passing tenant identifiers (ClientID) on the query string. Instead, I would recommend reading up on OAuth and Owin to embed client identification (and even the connection string) in the authentication token. I use a Db-per-tenant structure in my apps which works roughly as follows:
User visits site and is directed to the login.
Authentication service talks to a central DB managing all tenants. This DB serves as a hub to control which server/database that tenant is stored, and their DB version. (Directed to versioned app servers during upgrade transitions)
On successful authentication, the OAuth token is loaded with the tenant ID, schema name, (DB Name) and connection string. This is stored as encrypted claims.
Data server requests use a Strategy pattern to retrieve the Owin session context to retrieve the token, and validates it against the auth service. An example:
TenantConnectionModel ITenantIdentityStrategy.RetrieveTenant()
{
if (_tenant != null) // Cached copy...
return _tenant;
var context = HttpContext.Current.GetOwinContext();
var eMailAddress = context.Authentication.User.Identity.Name;
var tenantIdClaim = context.Authentication.User.Claims.SingleOrDefault(x => x.Type == "YourNamespace.TenantId");
var schemaClaim = context.Authentication.User.Claims.SingleOrDefault(x => x.Type == "YourNamespace.DBName");
var connectionStringClaim = context.Authentication.User.Claims.SingleOrDefault(x => x.Type == "YourNamespace.ConnectionString");
if (tenantIdClaim == null || schemaClaim == null || connectionStringClaim == null)
return null;
// TODO example: Call the auth service here with the e-mail address and tenant ID
// to validate that the current user has logged in and session hasn't timed
// out.
_tenant = new TenantConnectionModel
{
TenantId = long.Parse(tenantIdClaim.Value),
SchemaName = schemaClaim.Value,
ConnectionString = connectionStringClaim.Value,
};
return _tenant;
}
On successful auth check it extracts the tenant details including connection string.
IOC Container uses a delegate approach when constructing the DbContexts which retrieve the tenant identification strategy (part 3) and get the connection string to provide to the context. The strategy can cache the tenant details for multiple calls when resolving DbContexts, it's lifetime scope should be per-request.
I commonly use the DbContextScope unit of work pattern and have adapted a fork of it to suit multi-tenant environments for db-per-tenant or schema-per-tenant environments. Feel free to have a look @ https://github.com/StevePy/DbContextScope
Upvotes: 1