In one of my previous posts (which you can read here), I’ve told about my feelings for the repository pattern. Complaining about something is one thing (please, don’t even try to tell me, that you have never seen some piece of code, that made you cry like a baby), however, if we want to (pretend to) be professionalists, it is very important to come up with some ideas in order to solve the given problem (at least partially). In this post, I’ll present to you one of my solutions to the commonly misused repository pattern (especially on the querying side).
Disclaimer – in case you have never heard of the extension methods, please read the following article on the MSDN. On top of that, you may find many useful (like hundreds of them) examples here.
Let’s get to work, and see how we can use the extension methods to act as a querying part of the repository pattern. I’ll provide the examples for both MSSQL and MongoDB databases using the popular Entity Framework and MongoDB Driver respectively, but keep in mind that presented techniques are universal and should be a good fit pretty much everywhere.
At first, I’ll define the core classes that will be used for both examples (on a side note, I’m not going to use any common patterns such as IoC to keep that code as simple as possible). So, ladies and gentlemen, here it comes, the legendary User entity, along with the IUserService that can act as an application service:
1 2 3 4 5 6 7 8 9 |
public abstract class Entity { public Guid Id { get; protected set; } protected Entity() { Id = Guid.NewGuid(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public class User : Entity { public string Email { get; protected set; } public string FirstName { get; protected set; } public string LastName { get; protected set; } public DateTime DateOfBirth { get; protected set; } protected User() { } public User(string email, string firstName, string lastName, DateTime dateOfBirth) { //Sophisticated validation if (string.IsNullOrWhiteSpace(email)) throw new ArgumentException("Email is required.", nameof(email)); if (string.IsNullOrWhiteSpace(firstName)) throw new ArgumentException("First name is required.", nameof(firstName)); if (string.IsNullOrWhiteSpace(lastName)) throw new ArgumentException("Last name is required.", nameof(lastName)); if (dateOfBirth.Year > (DateTime.UtcNow.Year - 13)) throw new ArgumentException("That fella is too young.", nameof(dateOfBirth)); Email = email; FirstName = firstName; LastName = lastName; DateOfBirth = dateOfBirth; } } |
1 2 3 4 5 6 7 |
public interface IUserService { Task<User> GetByIdAsync(Guid id); Task<User> GetByEmailAsync(string email); Task<IList<User>> FindAsync(string firstName = "", string lastName = "", int? age = null); Task CreateAsync(string email, string firstName, string lastName, DateTime dateOfBirth); } |
Please do note that whether the IUserService should return the domain entities (probably not and it could return DTO instead) is not important in this example. What truly matters is that this piece of code is responsible for performing some critical operations, such as creating a new user or finding another one for a given email address. Now, let’s take a look at the common, generic example of the repository pattern:
1 2 3 4 5 6 7 8 9 10 |
//Please, do not do this public interface IRepository<T> where T : Entity { Task<T> GetByIdAsync(Guid id); Task<IList<T>> GetAllAsync(); Task<IList<T>> FindAsync(Expression<Func<T, bool>> predicate); Task AddAsync(T entity); Task UpdateAsync(T entity); Task DeleteAsync(T entity); } |
And how it could be used within the IUserService:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
public class UserServiceUsingGenericRepository : IUserService { private readonly IRepository<User> _userRepository; public UserServiceUsingGenericRepository(IRepository<User> userRepository) { _userRepository = userRepository; } public async Task<User> GetByIdAsync(Guid id) => await _userRepository.GetByIdAsync(id); public async Task<User> GetByEmailAsync(string email) { var users = await _userRepository.FindAsync(x => x.Email == email); return users.FirstOrDefault(); } public async Task<IList<User>> FindAsync(string firstName = "", string lastName = "", int? age = null) { //TODO: Filtering using generic FindAsync() return await _userRepository.FindAsync(x => true); } public async Task CreateAsync(string email, string firstName, string lastName, DateTime dateOfBirth) { var existingUser = await GetByEmailAsync(email); if (existingUser != null) throw new Exception($"There is an already existing user with email: {email}."); var user = new User(email, firstName, lastName, dateOfBirth); await _userRepository.AddAsync(user); } } |
May I present to you another one, much better, specialized version of the UserRepository?
1 2 3 4 5 6 7 8 |
//Do this only if you are certain about specific operations, mappings, query performance etc. public interface IUserRepository { Task<User> GetByIdAsync(Guid id); Task<User> GetByEmailAsync(string email); Task<IList<User>> FindAsync(string firstName = "", string lastName = "", int? age = null); Task AddAsync(User user); } |
And its usage within the IUserService:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public class UserServiceUsingSpecializedRepository : IUserService { private readonly IUserRepository _userRepository; public UserServiceUsingSpecializedRepository(IUserRepository userRepository) { _userRepository = userRepository; } public async Task<User> GetByIdAsync(Guid id) => await _userRepository.GetByIdAsync(id); public async Task<User> GetByEmailAsync(string email) => await _userRepository.GetByEmailAsync(email); public async Task<IList<User>> FindAsync(string firstName = "", string lastName = "", int? age = null) => await _userRepository.FindAsync(firstName, lastName, age); public async Task CreateAsync(string email, string firstName, string lastName, DateTime dateOfBirth) { var existingUser = await GetByEmailAsync(email); if(existingUser != null) throw new Exception($"There is an already existing user with email: {email}."); var user = new User(email, firstName, lastName, dateOfBirth); await _userRepository.AddAsync(user); } } |
If you truly want to use the repository, please use the latter implementation. At that point you may actually stop reading that post (but I do encourage you not to). Otherwise, if you want to find out how to make use of the extension methods please stay with me. Let’s begin with the Entity Framework implementation first.
1 2 3 4 |
public class MyDbContext : DbContext { public virtual IDbSet<User> Users { get; protected set; } } |
And here we go:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public static class UserQueries { public static async Task<User> GetByIdAsync(this IQueryable<User> users, Guid id) => await users.FirstOrDefaultAsync(x => x.Id == id); public static async Task<User> GetByEmailAsync(this IQueryable<User> users, string email) => await users.FirstOrDefaultAsync(x => x.Email == email); public static async Task<IList<User>> FindAsync(this IQueryable<User> users, string firstName = "", string lastName = "", int? age = null) { var foundUsers = users.AsQueryable(); if (string.IsNullOrWhiteSpace(firstName)) foundUsers = foundUsers.Where(x => x.FirstName == firstName); if (string.IsNullOrWhiteSpace(lastName)) foundUsers = foundUsers.Where(x => x.LastName == lastName); if (age.HasValue) { var yearOfBirth = DateTime.UtcNow.Year - age.Value; foundUsers = foundUsers.Where(x => x.DateOfBirth.Year == yearOfBirth); } return await foundUsers.ToListAsync(); } } |
Which boils down to the following usage in the IUserService:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public class UserServiceUsingEf : IUserService { private readonly MyDbContext _dbContext; public UserServiceUsingEf(MyDbContext dbContext) { _dbContext = dbContext; } public async Task<User> GetByIdAsync(Guid id) => await _dbContext.Users.GetByIdAsync(id); public async Task<User> GetByEmailAsync(string email) => await _dbContext.Users.GetByEmailAsync(email); public async Task<IList<User>> FindAsync(string firstName = "", string lastName = "", int? age = null) => await _dbContext.Users.FindAsync(firstName, lastName, age); public async Task CreateAsync(string email, string firstName, string lastName, DateTime dateOfBirth) { var existingUser = await GetByEmailAsync(email); if (existingUser != null) throw new Exception($"There is an already existing user with email: {email}."); var user = new User(email, firstName, lastName, dateOfBirth); _dbContext.Users.Add(user); await _dbContext.SaveChangesAsync(); } } |
And pretty much the same for the MongoDB Driver:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public static class UserQueries { public static async Task<User> GetByIdAsync(this IMongoCollection<User> users, Guid id) => await users.AsQueryable().FirstOrDefaultAsync(x => x.Id == id); public static async Task<User> GetByEmailAsync(this IMongoCollection<User> users, string email) => await users.AsQueryable().FirstOrDefaultAsync(x => x.Email == email); public static async Task<IList<User>> FindAsync(this IMongoCollection<User> users, string firstName = "", string lastName = "", int? age = null) { var foundUsers = users.AsQueryable(); if (string.IsNullOrWhiteSpace(firstName)) foundUsers = foundUsers.Where(x => x.FirstName == firstName); if (string.IsNullOrWhiteSpace(lastName)) foundUsers = foundUsers.Where(x => x.LastName == lastName); if (age.HasValue) { var yearOfBirth = DateTime.UtcNow.Year - age.Value; foundUsers = foundUsers.Where(x => x.DateOfBirth.Year == yearOfBirth); } return await foundUsers.ToListAsync(); } } |
That we can use like that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public class UserServiceUsingMongo : IUserService { private const string UsersCollection = "users"; private readonly IMongoDatabase _database; public UserServiceUsingMongo(IMongoDatabase database) { _database = database; } public async Task<User> GetByIdAsync(Guid id) => await _database.GetCollection<User>(UsersCollection).GetByIdAsync(id); public async Task<User> GetByEmailAsync(string email) => await _database.GetCollection<User>(UsersCollection).GetByEmailAsync(email); public async Task<IList<User>> FindAsync(string firstName = "", string lastName = "", int? age = null) => await _database.GetCollection<User>(UsersCollection).FindAsync(firstName, lastName, age); public async Task CreateAsync(string email, string firstName, string lastName, DateTime dateOfBirth) { var existingUser = await GetByEmailAsync(email); if(existingUser != null) throw new Exception($"There is an already existing user with email: {email}."); var user = new User(email, firstName, lastName, dateOfBirth); await _database.GetCollection<User>(UsersCollection).InsertOneAsync(user); } } |
What have we gained? At least the following:
- We got rid of the one layer (which simplifies the overall project structure).
- Querying methods (but feel free to implement the remaining CRUD operations if you feel a strong need for that) can be easily used within all of the application services (no more “injection” of the many repositories interfaces).
- Since we are aware about the underlying data storage that is being used, we may compose extension methods queries e.g. by returning an IQueryable interface. It’s quite easy to build a custom, fluent interface for the data access layer. Moreover, we can take the advantage of that knowledge to maximize the application performance.
Maybe we can not easily swap the data storage anymore but hold on for a second… could we really do that before?
P.S.
You may download the full example here.
Pingback: dotnetomaniak.pl
Extensions method should be forbidden. They’re confusing and make testing harder.
I think that extension methods have as many opponents and supporters as e.g. “var” keyword.
I do get your point, and surely it may be used in a wrong way, but personally I find this to be a rather useful tool that helps with a fluent API design or defining a “clean” helper methods.
As for the testing in the above example, I don’t really find too many issues. You can easily test it, just need to be aware that e.g. with the integration testing, it’s not as obvious as with “mocking” the repository methods. Instead, you need to mock the database itself, which is pretty straightforward for example with Effort library if EF is being used.
Are you aware that you just created a repository and named it UserService?
Take a closer look at command and query approach to database access.
Check this out: https://lostechies.com/jimmybogard/2012/10/08/favor-query-objects-over-repositories/
and continue your research 🙂
Yes, I am, it was meant to be a very trivial example :). The only method that is not typical for the repository is CreateAsync(), as it takes some input parameters, and internally creates a domain object, which is saved into the database (infrastructure).
Thanks for the link, I do know this approach, both the Query and Command handlers, and have been using it for quite some time. Actually, I was planning, to write a post about that someday in the future :).
Jak to w końcu jest z tym wzorcem Repozytorium + ORM?
Czytałem wiele publikacji na ten temat i co człowiek to opinia.
Część osób twierdzi, że tworzenie repozytorium i korzystanie z ORM-a to niepotrzebne tworzenie abstrakcji nad abstrakcją, z kolei inna grupa programistów uważa, że ułatwia to podmianę źródła danych(z czym w sumie się zgadzam).
Jak to wygląda w Twoich projektach?
Z tego co widziałem w Twoich kursach, rekomendujesz użycie repozytoriów, aczkolwiek w kursach mamy do czynienia tylko z danymi przechowywanymi w pamięci.
Jak to wygląda w momencie kiedy źródłem danych jest relacyjna baza danych i korzystasz z EF?
Czy repozytorium powinno być dla każdej encji?
Co w takim razie kiedy potrzebujemy zrobić join-y z innych tabel?