Mahdi
Mahdi

Reputation: 1827

Inject different DbContexts into generic repository based on Domain class - Autofac

In my application, I need to interact with two databases. I have two domain classes which are located in two different databases. I also have a generic repository pattern which accepts an UoW in its constructor. I am looking a way to inject appropriate UoW based on Domain class. I do not want to write second generic repository for the second database.. Is there any neat solution?

public interface IEntity
{
    int Id { get; set; }
}

Located in Database A

public class Team: IEntity
{
    public int Id { get; set; }
    public string Name{ get; set; }

}

Located in Database B

public class Player: IEntity
{
    public int Id { get; set; }
    public string FullName { get; set; }
}

I also have a generic repository pattern with UoW

public interface IUnitOfWork
{
    IList<IEntity> Set<T>();
    void SaveChanges();
}

public class DbADbContext : IUnitOfWork
{
    public IList<IEntity> Set<T>()
    {
        return new IEntity[] { new User() { Id = 10, FullName = "Eric Cantona" } };
    }

    public void SaveChanges()
    {

    }
}

public class DbBDataContext: IUnitOfWork
{
    public IList<IEntity> Set<T>()
    {
        return new IEntity[] { new Tender() { Id = 1, Title = "Manchester United" } };
    }

    public void SaveChanges()
    {

    }

public interface IRepository<TEntity> where TEntity: class, IEntity
{
    IList<IEntity> Table();
}

public class BaseRepository<TEntity> : IRepository<TEntity> where TEntity : class, IEntity
{

    protected readonly IUnitOfWork Context;
    public BaseRepository(IUnitOfWork context)
    {
        Context = context;
    }

    IList<IEntity> IRepository<TEntity>.Table()
    {
        return Context.Set<TEntity>();
    }
}

I've already found articles saying that Autofac overrides the registration with the last value. I know my problem is how DbContexts are registered.

 var builder = new ContainerBuilder();
 // problem is here
        builder.RegisterType<DbADbContext >().As<IUnitOfWork>()
        builder.RegisterType<DbBDbContext >().As<IUnitOfWork>()

 builder.RegisterGeneric(typeof(BaseRepository<>)).As(typeof(IRepository<>));
        var container = builder.Build();

Upvotes: 1

Views: 1461

Answers (3)

Mahdi
Mahdi

Reputation: 1827

I inspired from @tdragon's answer.

The first step is registering Named DbContext

        builder.RegisterType<Database1>()
            .Keyed<IUnitOfWork>(DbName.Db1)
            .Keyed<DbContext>(DbName.Db1).AsSelf().InstancePerRequest();

        builder.RegisterType<Database2>()
            .Keyed<IUnitOfWork>(DbName.Db2)
            .Keyed<DbContext>(DbName.Db2).AsSelf().InstancePerRequest();

Please note that DbName is just an enum.

The following code scans the data access layer assembly for finding Domain classes. Then, it registers ReadOnlyRepository and BaseRepository. the place of this code is in DIConfig

Type entityType = typeof(IEntity);
var entityTypes =   Assembly.GetAssembly(typeof(IEntity))
                    .DefinedTypes.Where(t => t.ImplementedInterfaces.Contains(entityType));


var baseRepoType = typeof(BaseRepository<>);
var readOnlyRepoType = typeof(ReadOnlyRepository<>);
var baseRepoInterfaceType = typeof(IRepository<>);
var readOnlyRepoInterfaceType = typeof(IReadOnlyRepository<>);
var dbContextResolver = typeof(DbContextResolverHelper).GetMethod("ResolveDbContext");

foreach (var domainType in entityTypes)
{
  var baseRepositoryMaker = baseRepoType.MakeGenericType(domainType);
  var readonlyRepositoryMarker = readOnlyRepoType.MakeGenericType(domainType);

 var registerAsForBaseRepositoryTypes = baseRepoInterfaceType.MakeGenericType(domainType);
 var registerAsForReadOnlyRepositoryTypes = readOnlyRepoInterfaceType.MakeGenericType(domainType);

 var dbResolver = dbContextResolver.MakeGenericMethod(domainType);
            // register BaseRepository
 builder.Register(c => Activator.CreateInstance(baseRepositoryMaker, dbResolver.Invoke(null, new object[] { c }))
            ).As(registerAsForBaseRepositoryTypes).InstancePerRequest(jobTag);
            //register readonly repositories
 builder.Register(c => Activator.CreateInstance(readonlyRepositoryMarker, dbResolver.Invoke(null, new object[] { c })))
           .As(registerAsForReadOnlyRepositoryTypes).InstancePerRequest(jobTag);

}

The following methods try to find DbSet in each DbContext in order to find out the Domain Classes belongs to which DataContext/Database.

public class DbContextResolverHelper
{
    private static readonly ConcurrentDictionary<Type, DbName> TypeDictionary = new ConcurrentDictionary<Type, DbName>();


    public static DbContext ResolveDbContext<TEntity>(IComponentContext c) where TEntity : class, IEntity
    {
        var type = typeof(DbSet<TEntity>);


        var dbName = TypeDictionary.GetOrAdd(type, t =>
        {

            var typeOfDatabase1 = typeof(Database1);
            var entityInDatabase1 = typeOfDatabase1 .GetProperties().FirstOrDefault(p => p.PropertyType == type);
            return entityInDatabase1 != null ? DbName.Db1: DbName.Db2;


        });

        return c.ResolveKeyed<DbContext>(dbName);
    }
}

Upvotes: 1

tdragon
tdragon

Reputation: 3329

If you want to keep single BaseRepository and its interface, you have to somehow configure, with entity would be handled by which DbContext. It could be done in registration part of application, but in that case you cannot register your BaseRepostory<T> as open generic, but be explicit in your registrations, like this:

containerBuilder.RegisterType<DbADataContext>().Named<IUnitOfWork>("A");
containerBuilder.RegisterType<DbBDataContext>().Named<IUnitOfWork>("B");

containerBuilder.Register(c => new BaseRepository<Team>(c.ResolveNamed<IUnitOfWork>("A")).As<IRepostory<Team>>();
containerBuilder.Register(c => new BaseRepository<Player>(c.ResolveNamed<IUnitOfWork>("B")).As<IRepository<Player>>();

(just proof of concept, code not tested)

Autofac is not smart enough to know "automatically" which unit of work you want to use in each of your repository.

Upvotes: 0

Zerghouni Idriss
Zerghouni Idriss

Reputation: 84

What about this:

builder.RegisterType<DbContextBase>().As<IUnitOfWork>()

And

    DbADataContext: DbContextBase,IUnitOfWork
    DbBDataContext: DbContextBase,IUnitOfWork

Or in your registration you can just do something like :

containerBuilder.RegisterGeneric(typeof(DbADataContext<>)).Named("DbADataContext", typeof(IUnitOfWork<>));
containerBuilder.RegisterGeneric(typeof(DbBDataContext<>)).Named("DbBDataContext", typeof(IUnitOfWork<>));

Upvotes: 0

Related Questions