Reputation: 133
I would like to register the following items for DI using an open generic implementation and interface. I know the following example will not work, as well as other combinations I've tried with MakeGenericType
, or GetGenericArguments
. I would like to simply call AddRepository<MyDbContext>
and then be able to inject my implementation into classes without explicitly having to register the type I am using.
Interface
public interface IRepository<TEntity>
{
}
Implementation
public class Repository<TEntity, TContext> : IRepository<TEntity>
where TEntity : class
where TContext : DbContext
{
}
Registration
public static class RepositoryServiceCollectionExtensions
{
public static IServiceCollection AddRepository<TContext>(
this IServiceCollection services) where TContext : DbContext
{
services.TryAddScoped(
typeof(IRepository<>),
typeof(Repository< , TContext>));
return services;
}
}
Upvotes: 2
Views: 2253
Reputation: 387677
The dependency injection container Microsoft.Extensions.DependencyInjection
and its abstraction layer does not support open generic factories. So you generally cannot achieve what you would like to do there. There’s also no support planned.
Unlike many those other dependency injection related features, this is also not really possible to patch by just providing the right wrapper or factory types. So you will actually have to change your design here.
Since you want to resolve IRepository<TEntity>
and the only way to do this is by registering an equivalent open generic type, you will have to have some type Repository<TEntity>
that implements your repository. That makes it impossible to retrieve the database context type from the generic type argument, so you will have to use a different way here.
You have different options to do that. For example, you could configure your Repository<TEntity>
(e.g. using M.E.Options) with the context type and make that resolve the Repository<TEntity, TContext>
dynamically. But since you have actual control over your database context, I would suggest either adding a marker interface or introducing another type for the context which you can then register with the container:
public class Repository<TEntity> : IRepository<TEntity>
{
public Repository(IDbContext dbContextFactory)
{ … }
}
public class MyDbContext : DbContext, IDbContext
{ … }
Then, your extension method could look like this:
public static IServiceCollection AddRepository<TContext>(this IServiceCollection services)
where TContext : DbContext, IDbContext
{
services.AddTransient(typeof(IDbContext), sp => sp.GetService<TContext>());
services.TryAddScoped(typeof(IRepository<>), typeof(Repository<>));
return services;
}
Of course, this changes how your Repository
implementation works, but I don’t actually assume that you need to know the TContext
type other than to inject the database context type. So this will probably still work for you.
That being said, I have too agree with Chris Pratt, that you probably don’t need this. You say that you want to introduce the repository, because “coding stores and implementations for every entity is a time consuming task” but you should really think about whether you actually need that. A generic repository is very limited in what it can do, and mostly means that you are doing just CRUD operations. But exactly that is what DbContext
and DbSet<T>
already do:
DbContext.Add
, DbSet<T>.Add
DbContext.Find
, DbSet<T>.Find
DbContext.Update
, DbSet<T>.Update
DbContext.Remove
, DbSet<T>.Remove
In addition, DbContext
is a “unit of work” and DbSet<T>
is an IQueryable<T>
which gives you a lot more control and power than a generic repository could possible give you.
Upvotes: 4
Reputation: 239290
You cannot have a partially open generic reference. It's all or nothing. In other words, you can try:
services.TryAddScoped(
typeof(IRepository<>),
typeof(Repository<,>));
But, if that doesn't work, you'll likely need to add a type param to your AddRepository
method:
public static IServiceCollection AddRepository<TEntity, TContext>(this IServiceCollection services)
where TEntity : class
where TContext : DbContext
{
services.TryAddScoped(
typeof(IRepository<TEntity>),
typeof(Repository<TEntity, TContext>));
return services;
}
Of course, I think that breaks what you're ultimately trying to achieve here: registering repositories for all the entity types in one go. You can always use a bit of reflection find all entities in your assembly (they would need to share something in common: base class, interface, etc.) and then enumerate over them and use reflection to call AddScoped
on your service collection for each.
All that said, the best thing you can do here is to actually throw all this away. You don't need the repositories. EF already implements the repository and unit of work patterns. When you use an ORM like EF, you're essentially making that your data layer instead of a custom class library you create. Putting you own custom wrapper around EF not only adds entropy to your code (more to maintain, more to test, and more than can break), but it can also mess up the way EF works in many cases, leading to less efficiency in the best cases and outright introducing bugs into your application in the worst cases.
Upvotes: 2