I’m creating this post mainly because after making the post and video on the repository pattern, I was asked if I could create a post showing a generic abstraction of it.
Index
So in this post, what we are going to see is how to abstract as much as possible in our generic repository.
1 - What should we make common in the repository pattern?
In this post I’m not going to stop and explain what the repository pattern is, instead, we’ll take the most common elements and make them common.
Normally, or in a company, this abstraction should be in a NuGet package that can be shared across the company if possible.
Here, what we’ll look at is what is most common and can always, or nearly always, be shared. In the vast majority of cases, these are the CRUD operations: create, read, update, and delete records.
2 - Creating the generic repository
Before we start, don’t forget that this post is part of a more complete Entity Framework Core course and all the code is available on GitHub.
The easiest way to see what can be moved to a generic or common part is with code we already have written. In the case of the course, it’s very simple, but in a larger application, we’d have interfaces like this, very similar across all our repositories.
public interface IUserRepository
{
Task<User> Insert(User user);
Task<User?> GetById(int id);
Task<List<User>> GetAll();
void Update(User user);
Task<bool> Delete(int id);
}
As you can see, it has all the CRUD aspects we mentioned earlier.
2.1 - Creating the common interface
So that’s what we’re going to create for the interface of our first generic repository.
Of course, instead of using the entity in question, we’ll use generics to pass the type.
public interface IGenericRepository<T>
{
Task<T> Insert(T value);
Task<T?> GetById(int id);
IQueryable<T> GetAll();
void Update(T value);
Task<bool> Delete(int id);
}
As you can see, it’s practically the same, but there are two things to note.
First, the GetAll
method has changed from type List
to IQueryable
. Technically we could use IEnumerable
. Both options allow us to defer the query. But when we use IEnumerable
, if we filter or order the data, those operations are done in memory, while with IQueryable
they are translated into SQL, letting the database do the “heavy lifting”, which is faster and more efficient.
I could write a post on this, but I think this summary is clear enough: use IQueryable.
The second point is about search by ID. Each company will do this a bit differently. Some will always use int
or Guid
for the ID type, and move on.
Others, which I think is most common, will have, in the entity itself, a base entity that contains the ID as well as other common data. In our case, those common data may include the IsDeleted property we saw in the soft delete post; others will have an explicit interface for the ID.
In this post, we’ll simply include it in the base class, passing a generic type for the Id.
public abstract class CursoEFBaseEntity<TId>
{
public TId Id { get; set; }
public bool IsDeleted { get; set; }
public DateTime? DeletedTimeUtc { get; set; }
}
In our case it’s always an integer, but the code is prepared for any data type.
Of course, we should implement this new change in the entities of our project
public class Wokringexperience : CursoEFBaseEntity<int>
{
...
}
public class User : CursoEFBaseEntity<int>
{
...
}
Now, what we can do is go back to our generic repository interface and specify that T
has to be part of the object we need, which implies adding the ID type so that we can do both soft and hard deletes:
public interface IGenericRepository<T, TId>
where T : CursoEFBaseEntity<TId>
{
Task<T> Insert(T entity);
Task<T?> GetById(TId id);
IQueryable<T> GetAll();
void Update(T entity);
Task<bool> SoftDelete(TId id);
Task<bool> HardDelete(TId id);
}
Note: as I said, this example supports multiple types for the ID, but if you’re always going to use int or Guid, you can skip the generic type for that case.
2.2 - Implementing the common repository
The next step is to create the generic repository implementation. Just create a GenericRepository
that inherits from the interface we just created and implement its methods. Of course, don’t forget to inject the DbContext
, although in this case we’ll pass it from the classes that implement the abstract class.
public abstract class GenericRepository<T, TId> : IGenericRepository<T, TId>
where T : CursoEFBaseEntity<TId>
{
private readonly CursoEfContext _context;
protected DbSet<T> Entities => _context.Set<T>();
protected GenericRepository(CursoEfContext context)
{
_context = context;
}
public async Task<T> Insert(T entity)
{
EntityEntry<T> insertedValue = await _context.Set<T>().AddAsync(entity);
return insertedValue.Entity;
}
public async Task<T?> GetById(TId id)
=> await _context.Set<T>()
.FindAsync(id);
public IQueryable<T> GetAll()
=> _context.Set<T>();
public void Update(T entity)
{
_context.Set<T>().Update(entity);
}
public async Task<bool> SoftDelete(TId id)
{
T? entity = await GetById(id);
if (entity is null)
return false;
entity.IsDeleted = true;
entity.DeletedTimeUtc = DateTime.UtcNow;
_context.Set<T>().Update(entity);
return true;
}
public async Task<bool> HardDelete(TId id)
{
T? entity = await GetById(id);
if (entity is null)
return false;
_context.Set<T>().Remove(entity);
return true;
}
}
Things to note here:
We have to specify that T
is a class, in our particular case CursoEFBaseEntity<TId>
. Technically we did this in the interface too, but the reason is because we need it to be a class to work, same as in Entity Framework. And if we specify the class in particular, we have access to IsDeleted
for the soft delete.
The use of Set<T>
is because Set returns a DbSet and if we pass the type, we get the DbSet, which translated to Entity Framework and SQL gives us the table we need.
In our case, we have simplified the GetById
by removing the original Include which related more than two tables, this is very common and normal, since at the generic repository level, we don’t have the information to know what to load.
The solution, if you want to keep the original functionality, is to create another method in your specific repository that loads everything necessary.
Finally, using Find()
, Find can understand multiple data types for the Id, but if, for example, you definitely want to use FirstOrDefault
or similar, you won’t be able to do _context.set<T>().First(x=>x.id == id)
. To do this, you need to specify that TId is comparable with IEquatable<TId>
and instead of ==
use Equals
, like this:
public abstract class GenericRepository<T, TId> : IGenericRepository<T, TId>
where T : CursoEFBaseEntity<TId>
where TId : IEquatable<TId> //<----- this line
{
...
public async Task<T?> GetById(TId id)
=> await _context.Set<T>()
.FirstAsync(a=>a.Id.Equals(id)); //<----- this line
}
2.3 - Applying the common repository
What we have left now is to go to the repositories that we’ve already generated and update them to use the common repository. In a real project, it’s essential that we have tests at this point, preferably integration tests, because we need to verify that everything continues to work the same, and for example, we know that GetAll has changed since it no longer includes related objects.
The first thing is to go to the interface and update it to use the new one:
public interface IUserRepository : IGenericRepository<User, int>
{
Task<User?> GetByIdWithWorkingExperiences(int id);
}
We see that barely anything changes in general, but we’ve significantly reduced the code; we just need to change the GetById
to return the workingExperiences
.
Now we apply the change to the repository implementation:
public class UserRepository : GenericRepository<User, int>, IUserRepository
{
public UserRepository(CursoEfContext context) : base(context)
{
}
public async Task<User?> GetByIdWithWorkingExperiences(int id)
=> await Entities
.Include(a => a.Wokringexperiences)
.FirstOrDefaultAsync(x => x.Id == id);
}
With this change, we have centralized all the common operations inside a repository and, in the long run, reduced the number of lines we need to maintain.
If there is any problem you can add a comment bellow or contact me in the website's contact form