In my project I have more than one database contexts, so I have created a provider for getting the context objects based on need. My provider is looks like this.
public class DbContextProvider
{
public Func<AccountingContext> AccountingDbContextResolver { get; set; }
public Func<ActiveDirectryContext> ActiveDirectryDbContextResolver { get; set; }
public AccountingContext GetAccountingDbContext() =>
this.AccountingDbContextResolver();
public ActiveDirectryContext GetActiveDirectryDbContext() =>
this.ActiveDirectryDbContextResolver();
}
One ServiceBase
class I have created for getting the provider
public class ServiceBase
{
public ServiceBase(DbContextProvider contextProvider)
{
this.ContextProvider = contextProvider;
}
protected DbContextProvider ContextProvider { get; }
public AccountingContext AccountingDbContext =>
this.ContextProvider.GetAccountingDbContext();
public ActiveDirectryContext ActiveDirectryDbContext =>
this.ContextProvider.GetActiveDirectryDbContext();
}
I am using simple injector and I need to create instance of both Database contexts.
For getting the instance I have created two static methods with delegate
private static DbContextProvider CreateActiveDirectryDbContextProvider(Container container)
{
return new DbContextProvider
{
ActiveDirectryDbContextResolver =
() => container.GetInstance<ActiveDirectryContext>();
};
}
private static DbContextProvider CreateAccountingDbContextProvider(Container container)
{
return new DbContextProvider
{
AccountingDbContextResolver = () => container.GetInstance<AccountingContext>();
};
}
And for registrations I have used the below code
var accountingProvider = CreateAccountingDbContextProvider(container);
container.RegisterInstance(accountingProvider);
var activeDirectryProvider = CreateAccountingDbContextProvider(container);
container.RegisterInstance(activeDirectryProvider);
If i run the code then I am getting an error like below
System.InvalidOperationException: 'Type DbContextProvider has already been registered. If your intention is to resolve a collection of DbContextProvider implementations, use the Container.Collection.Register overloads. For more information, see https://simpleinjector.org/coll1. If your intention is to replace the existing registration with this new registration, you can allow overriding the current registration by setting Container.Options.AllowOverridingRegistrations to true. For more information, see https://simpleinjector.org/ovrrd.'
But everything is working fine when I try with only one context that is ,
var accountingProvider = CreateAccountingDbContextProvider(container);
container.RegisterInstance(accountingProvider);
I have tried to register it using container.Collection.Register
, no error is coming but I am not getting the instance in the service layer , I am getting a null reference exception there.
can someone help me to resolve this ?
You only have one type (DbContextProvider
) that is responsible for constructing both AccountingContext
and ActiveDirectryContext
. From that perspective, it is really strange to create two DbContextProvider
instances that are each partly initialized. A consumer would not expect GetAccountingDbContext()
to return null or throw a NullReferenceException
. So instead, you should create one single instance that can be used for both cases:
container.Register<ActiveDirectryContext>(Lifestyle.Scoped);
container.Register<AccountingContext>(Lifestyle.Scoped);
container.RegisterInstance<DbContextProvider>(new DbContextProvider
{
ActiveDirectryDbContextResolver = () => container.GetInstance<ActiveDirectryContext>(),
AccountingDbContextResolver = () => container.GetInstance<AccountingContext>()
});
Or better, make DbContextProvider
immutable:
container.RegisterInstance<DbContextProvider>(new DbContextProvider(
activeDirectryDbContextResolver: () => container.GetInstance<ActiveDirectryContext>(),
accountingDbContextResolver: () => container.GetInstance<AccountingContext>()));
This fixes the problem, because there is no only one registration for DbContextProvider
. This removes the ambiguity, prevents possible bugs, and is a simpler solution.
But while this would work, I would like to suggest a few changes to your design.
First of all, you should get rid of the ServiceBase
base class. Although base classes are not bad per see, when they start to get dependencies of their own, they likely start to violate the Single Responsibility Principle, and their derivatives the Dependency Inversion Principle and the Open/Closed Principle:
Instead, of using base classes, do the following:
When you follow this advice, what you end up with is a set of (derived) service classes that depend on a base class that is nothing more than an empty shell. This is when you can remove the base class altogether. What you now achieved is Composition over Inheritance. Composition typically leads to more maintainable systems than inheritance does.
As JoostK mentioned, you can also inject the DbContext
types directly into consumers. Whether or not you want to do this, however, depends on the type of composition model you decided to use. What this means is that, when you choose to apply the Closure Composition Model, you should typically inject the DbContext
implementations directly into your consumers.
public class ProduceIncomeTaxService : IHandler<ProduceIncomeTax>
{
private readonly AccountingContext context;
// Inject stateful AccountingContext directly into constructor
public ProduceIncomeTaxService(AccountingContext context) => this.context = context;
public void Handle(ProduceIncomeTax command)
{
var record = this.context.AccountingRecords
.Single(r => r.Id == command.Id);
var tax = CalculateIncomeTax(record);
FaxIncomeTax(tax);
this.context.SaveChanges();
}
...
}
This simplifies the registrations of your system, because now you just register the DbContext
implementaions and you're done:
container.Register<ActiveDirectryContext>(Lifestyle.Scoped);
container.Register<AccountingContext>(Lifestyle.Scoped);
// Of course don't forget to register your service classes.
Your current DbContextProvider
seems to designed around the Ambient Composition Model. There are advantages of both composition models, and you might have chosen deliberately for the Ambient Composition Model.
Still, however, the DbContextProvider
exposes many (10) properties—one for each DbContext
. Classes and abstractions with many methods can cause a number of problems concerning maintainability. This stems from the Interface Segregation Principle that prescribes narrow abstractions. So instead of injecting one wide provider implementation that gives access to a single DbContext
type. Implementations would typically only require access to a single DbContext
type. If they require multiple, the class should almost certainly be split up into smaller, more-focused classes.
So what you can do instead is create a generic abstraction that allows access to a single DbContext
type:
public interface IDbContextProvider<T> where T : DbContext
{
T Context { get; }
}
When used in a consumer, this would look as follows:
public class ProduceIncomeTaxService : IHandler<ProduceIncomeTax>
{
private readonly IDbContextProvider<AccountingContext> provider;
// Inject an ambient context provider into the constructor
public ProduceIncomeTaxService(IDbContextProvider<AccountingContext> provider)
=> this.provider = provider;
public void Handle(ProduceIncomeTax command)
{
var record = this.provider.Context.AccountingRecords
.Single(r => r.Id == command.Id);
var tax = CalculateIncomeTax(record);
FaxIncomeTax(tax);
this.provider.Context.SaveChanges();
}
...
}
There are multiple ways to implement IDbContextProvider<T>
, but you can, for instance, create an implementation that directly depends on Simple Injector:
public sealed class SimpleInjectorDbContextProvider<T> : IDbContextProvider<T>
where T : DbContext
{
private readonly InstanceProducer producer;
public SimpleInjectorDbContextProvider(Container container)
{
this.producer = container.GetCurrentRegistrations()
.FirstOrDefault(r => r.ServiceType == typeof(T))
?? throw new InvalidOperationException(
$"You forgot to register {typeof(T).Name}. Please call: " +
$"container.Register<{typeof(T).Name}>(Lifestyle.Scope);");
}
public T Context => (T)this.producer.GetInstance();
}
This class uses the injected Container
to pull the right InstanceProducer
registration for the given DbContext
type. If this is not registered, it throws an exception. The InstanceProducer
is then used to get the DbContext
when Context
is called.
Since this class depends on Simple Injector, it should be part of your Composition Root.
You can register it as follows:
container.Register<ActiveDirectryContext>(Lifestyle.Scoped);
container.Register<AccountingContext>(Lifestyle.Scoped);
container.Register(
typeof(IDbContextProvider<>),
typeof(SimpleInjectorDbContextProvider<>),
Lifestyle.Singleton);