Is Repository an anti-pattern?

iamprovidence
23 min readNov 17, 2023

--

At last, the celestial moment has arrived to open the sacred doors and fully unlock the secrets of this majestic repository. Finally, we can answer the question, of whether the Repository is an anti-pattern. And the answer is…🥁 🥁 🥁 maybe🤔.

You know, it depends. In some cases, it is, while in others it isn’t. For example, it depends, on how you start a question. Is repository an anti-pattern? Probably not. Is repository in conjunction with EF an anti-pattern? 😬 Hard to say.

That’s the question that matter of controversy. So let’s discuss it.

What? Another article about the Repository pattern? There are millions of those!😤 Leave Repository alone, please😭

Hey, I didn’t start the fire. I just want to have my piece of the hype on this topic. However, you have my promise here. I will torture that cash cow only once but for good 😅

Today, we will see the cases where the repository is useful and when it isn’t. You’ll discover if there is still a place for it in modern programming. And finally, we would be able to answer the question of whether it is an anti-pattern or not.

So, are you ready? Here we go!

How it all started

A long time ago in a galaxy far, far away, at the dawn of the origin of software architecture, the ancient mysterious book was written by a powerful wizard named Martin Fowler. The book was called Patterns of Enterprise Application Architecture. The one who attains that secret knowledge… (the sound of a needle coming off a vinyl record)📀📀📀

Could you not?! 🤨

Alright, alright, boring you are 😒. We will do it your way.

Now, where was I? Ah, yes, Patterns on Enterprise Application Architecture. In this book, the Repository pattern was mentioned. The idea is simple here. We don’t want to see SQL in our code, so let’s move it to another class and abstract away from all those low-level details. Here is how it looks on the class diagram:

When talking about the Repository, there is also another frequently mentioned pattern — Unit of Work. What is the deal with it? Not to spam our database with multiple requests, we track all the changes on our entity, and then perform all of them in a single transaction.

Not many know, that Martin Fowler actually, mentioned a lot of other useful patterns. For example, Identity Map, which is a fancy name for a cache. To avoid loading the same entity from the database multiple times, we load it once, store it in the memory, and for all future calls, just use the one we already have.

And there are just a lot of those! For instance, Identity Field, Association Table Mapping, Table Inheritance, and so on. Jesus, this guy is a genius. Oh, sh*t, how much I love him 😤. Seriously, strongly suggest you get more familiar with it. Meanwhile, let’s get not distracted, and get back to our topic.

Another popularizer of Repository was Eric Evans with his DDD. He didn’t reinvent the wheel. Evans’s goal was to access the database from the business logic layer, which was achieved with a little help from dependency inversion. As a side effect, we have isolated our application from a database in a way that you can change the DB implementation without changing the rest of the app.

You can actually, find a lot of mentions about the repository pattern here and there, from now up to dinosaur ages. The conclusion suggests itself — developers always needed a way to access a database in their programs.

Implementation

As with other patterns, there are no strict rules to define your repository. You just simply create a class with all database-related methods:

As always, we need an interface for it (where can we go without it).

At some point, you will need another repository for a different entity. And then another one. And another one. Basically, to avoid God Object, you need a repository per database table.

Sooner or later, you will notice, there is a lot of code duplication between those interfaces and implementations. The solution suggests itself. Just move all the shared code to the abstract class called GenericRepository.

That’s how we get the final schema. There is nothing extraordinary. Just regular developer work. Create a class, move shared code to an abstract class, add interfaces here and there, and you got it.

Now, let’s see it in the code. Since we already know, what is coming on, there is no point in going through all that logical reasoning once again. We will immediately begin with a generic interface that contains definitions for all CRUD operations:

interface IRepository<TEntity>
{
void Add(TEntity entity);
IEnumerable<TEntity> AsEnumerable();
IReadOnlyCollection<TEntity> Where(ISpecification<TEntity> predicate);
void Remove(int id);
void Update(TEntity entity);
}

The implementation is quite simple. Just wrap DbContext with a class and reimplement all the methods.

/*abstract*/ class RepositoryBase<TEntity>: IRepository<TEntity>
{
protected AppDbContext _appDbContext;

public RepositoryBase(AppDbContext appDbContext)
{
_appDbContext = appDbContext;
}

public virtual void Add(TEntity entity)
{
_appDbContext.Set<TEntity>().Add(entity);
}

public virtual IEnumerable<TEntity> AsEnumerable()
{
return _appDbContext.Set<TEntity>().ToList();
}
. . .
}

As a final piece, we need our specific repository with an interface:

interface IUserRepository: IRepository<User>
{
User GetByName(string name);
}

class UserRepository: RepositoryBase<User>, IUserRepository
{
public User GetByName(string name)
{
return _appDbContext
.Set<User>()
.Single(u => u.FirstName == name);
}
}

And that’s it!

To complete the picture, we should also add a Unit of Work. It is such a simple pattern, that there is not even a point in drawing a diagram. Just see a code.

It serves one purpose: to make sure that when you use multiple repositories, they share a single database connection and all the changes will be committed at once. Alright, maybe it has two purposes 🤔

So how do you achieve that?

  1. Create all repositories in a single place sharing the same db connection
  2. Add a Complete() method to ensure that all related changes are coordinated
interface IUnitOfWork
{
IUserRepository Users { get; }
IRepository<TEntity> GetRepository<TEntity>();

void Complete();
}

class UnitOfWork: DbContext, IUnitOfWork
{
public IUserRepository Users => base.GetService<IUserRepository>()

public IRepository<TEntity> GetRepository<TEntity>()
{
return new RepositoryBase<TEntity>(this);
}

public void Complete()
{
base.SaveChanges();
}
}

I am using inheritance here since it’s undeservedly been criticized. However, you are probably will find more examples of Unit of Work where DbContext is injected in the constructor.

Also, notice, that for the Users property, we return the repository from the DI, while in the GetRepository() method we are creating concrete an instance of a class by using the constructor. We will discuss those options in detail later, for now just know they are both valid 🙃.

Usage

A typical use case like renaming a user, can be implemented with the repository and unit of work in the next way:

class UserDomainService
{
public void RenameUser(string oldName, string newName)
{
var user = _unitOfWork
.Users
.GetByName(oldName);

user.FirstName = newName;

_unitOfWork.Complete();
}
}

There is nothing special here, just get the record, update it, and submit the transaction. Quick, simple, that’s how I like it 😋

Benefits

Now, that you have seen what we have to deal with, it is time to list its benefits:

A repository behaves like a collection

Indeed, they are much simpler in usage compared to raw SQL queries. The implementation reminds in-build collections, so it is pretty intuitive, even for junior devs.

Compare, what we have with collections:

var users = new List<User>();

users.Add(new User("John", "Doe"));

To what we have with repositories:

var users = new UserRepository(new AppDbContext());

users.Add(new User("John", "Doe"));

Definitely better than direct usage of SQL:

var connectionString = "my-db-connection-string";
using (IDbConnection db = new SqlConnection(connectionString))
{
var param = new
{
FirstName = "John",
LastName = "Doe",
};

var sql = @$"
INSERT INTO [dbo].[User]
([FirstName], [LastName])
VALUES
({nameof(param.FirstName)}, {nameof(param.LastName)})";

db.Execute(sql, param);
}

Abstraction level between business logic and database

Can not agree more on that. That is the whole point of any architecture, to split responsibilities between layers.

If your business logic layer calls a database directly, your architecture sucks! If you have no layers at all, it sucks twice! That is what a spaghetti code is. Avoid it by any means, otherwise project development will turn out to nightmare.

Encapsulate database requests

Since all requests to the database are in one centralized place, everybody in a team can easily locate them. Just by investigating the interface, you already can tell which methods are available.

interface IUserRepository
{
IReadOnlyCollection<User> GetOlderThan(int age);
IReadOnlyCollection<User> GetWithStatus(UserStatus userStatus);
IReadOnlyCollection<User> GetByRole(Role role);
void UpdateName(int id, string newName);
void DeleteInactive();
. . .
}

The existence of these pre-implemented methods simplifies work for the next generation of developers since they can just reuse them in multiple places, without spending time on creating them from scratch.

Simplifies unit testing

How do you mock SqlConnection, SqlCommand, DbConnection? You probably don’t. The only way would be to wrap it with services and interfaces.

How do you mock an interface of a repository? You just mock it! It’s an interface, after all.

var mock = new Mock<IUserRepository>();

mock
.Setup(x => x.GetAll())
.Returns(new Users[]
{
new User("John", "Doe"),
new User("Jane", "Doe"),
});

Data access can be changed without affecting other layers

The whole point of abstraction is that you can substitute implementation without much efforts.

For example, when creating proof of concept, you can store entities in the memory or files. When it comes to deploying your app in the real world, the only layer you need to change is persistence, since all the others just depend on the interface. Just implement all the interfaces with SQL and you are good to go.

Drawbacks

There is no perfection in this world, so to speak about the repository. It has its drawbacks, and here are some:

More classes

I would like to get paid without writing any code at all 😅 Since it is not really possible, an acceptable option for me would be to write as little as possible. Repositories surely do not help with that.

Even though it is just copy-paste-rename work, doing it, quickly gets boring and annoying. One can not concentrate much on monotonous work and can easily provoke bugs.

Time-consuming

Writing repositories take time. Instead of focusing on a feature we are forced to write boilerplate code postponing what is actually important.

No customer will be happy hearing you are spending time on it. Time is money.

Bad performance

Writing methods on each case is as boring as annoying. Developers tend to take shortcuts and use already existing methods, which are likely not optimal:

var users = _userRepository
.GetActive() // loads in memory 100_000 records
.Where(x => x.Age > 18) // only 1_000 records needed
.ToList();

Even though you write a request for each case, you still have a performance downgrade with generics methods. Check out the method of loading a single entity:

class UserRepository: IUserRepository
{
. . .
public User Get(int id)
{
return _dbContext
.Users
.Include(u => u.Roles)
.Include(u => u.Roles)
.ThenInclude(r => r.Permissions)
.Include(u => u.Departments)
.Include(u => u.Orders)
.Include(u => u.Addresses)
.Include(u => u.Projects)
.Include(u => u.Photos)
.Include(u => u.Photos)
.ThenInclude(r => r.Comments)
.Include(u => u.Invites)
.Single(u => u.Id == id);
}
. . .
}

It is doing so many joins, since you don’t know where the method will be used. So the only valid solution would be here to load all the related data despite that it is not needed.

Limits the use of ORM features

Database itself is an extremely powerful tool, that gets nerfed as soon as you start accessing it from the code.

ORM gives you a much simpler API by the cost of hiding most of the stuff. As a trade-off, they offer lots of cool and fancy features.

With repositories, you have another abstraction, meaning you should refrain from all of those features.

Usually, developers, are glad not to have to work with SQL and low-level code. However, I haven’t seen many developers refuse to use modern ORM’s features.

Contains business logic

Even though repositories are not meant to contain any business logic and should behave just like collections, developers, for the benefit of performance, like to break this rule.

interface IOrderRepository
{
OrderStatistic CalculateOrderStatistic(int orderId);
void UpdateStatus(int id, OrderStatus newStatus);
}

It is as harmful to your app as having business logic in stored procedures. Hard to test, hard to maintain, and possible to bypass business invariants.

Contains storage logic

On the other side of the coin, for the sake of usability, storage-agnostic repositories tend to be not so agnostic. As a result, such methods appear:

interface IUserRepository
{
User GetByColumn(string columnName, object value);
}

It leaks the implementation details since columns are an unknown concept for a file, memory, MongoDb, and other kind of storages.

It quickly leads to having wrong references across projects. Where wrong references, there are violated boundaries. Where violated boundaries, there are no clear responsibilities. No clear responsibilities mean a spaghetti code. The spaghetti code means an unsupported project. I think you get the idea.

Violation of the Liskov Substitution Principle

When using a base repository class containing all your CRUD operations, you have to find a workaround for unsupported methods. For example, not every entity supports Delete(). Some data are read-only, so Create() is not an option either.

You can have multiple interfaces for each operation like IDeleteRepository, ICreateRepository, etc. However it more often just makes more mess and noise than helps.

So why it does not work?

So far, we have been discussing the pros and cons of generic repositories regardless of programming language, regardless particular storage, regardless concrete implementation.

Having EntityFramework with a DbContext adds its own drawback and kind of neglects all the advantages. Let’s see why.

Not consistent

Going back to our implementation.

interface IRepository<TEntity>
{
void Add(TEntity entity);
IEnumerable<TEntity> AsEnumerable();
IReadOnlyCollection<TEntity> Where(ISpecification<TEntity> predicate);
void Remove(int id);
void Update(TEntity entity);
}

Patterns are agile by their nature. There are no strict rules and can be done differently. While with other approaches it’s easy to be consistent, it is not so with a repository.

Developers constantly argue about how to implement and use it:

  • Can I have the Update() method in the repository or it is part of UnitOfWork?
  • Should I return an entity from a repository? After all interface of a repository is part of our business logic layer.
  • Maybe we should return the Data Access Object since repository implementations are part of the infrastructure layer.
  • What about dtos? I don’t want to load the entire object, only a few fields. Can I use dtos then?
  • In which repository should I put a method that returns the conjunction of two entities as dto?
  • Do I need to create a repository for a child entity if I already can fetch it from an existing repository?
  • Do I create a repository for each entity, or only for aggregates?
  • Can I use GenericRepository without a concrete interface?
  • Can I use IQueryable or only IEnumerable?
  • I don’t want to add a service that is doing nothing just calling a repository. Can I inject the repository into my controller directly?

I can continue the list, but you got the idea.

Everybody has their own vision of repository, defending which can be hard.

Abstraction that leaks

Let’s get back to our implementation once again.

interface IRepository<TEntity>
{
void Add(TEntity entity);
IEnumerable<TEntity> AsEnumerable();
IReadOnlyCollection<TEntity> Where(ISpecification<TEntity> predicate);
void Remove(int id);
void Update(TEntity entity);
}

Turns out, those methods are not enough for generic operations.

Developers often try to find a compromise between abstraction and performance. As a result, such a method appears:

interface IRepository<TEntity>
{
IReadOnlyCollection<TEntity> Where(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
params Expression<Func<TEntity, object>>[] includeProperties = null,
bool disableTracking = false,
);
}

We are not using anything EF-related here, however, we are still tightly coupled to it. Expressions, IQueryable, included properties, and tracking mechanism it is all concern of EF that have no sense for Dapper or MongoDb.

Another workaround, that seems to be harmless, while in reality it is as bad or even worse:

interface IRepository<TEntity>
{
IQueryable<TEntity> AsQueryable();
}

We are exposing an interface after all. It is agile but also leaks your database implementation to the calling layer. You are basically using EF directly:

  • The caller code must know the specifics of your ORM, like EF.Functions
  • ToListAsyncis part of EF as well
var users = await _userRepository
.AsQueryable()
.Where(u => EF.Functions.Like(u.FirstName, "[J]%"))
.ToListAsync();

That makes all the repository efforts pointless. Now, imagine you have to rewrite this repository using a file storage. To make calling code work, you have to implement your own IQueryable provider, the work no customer would like to pay for.

Unit Of Uselessness

Things are not better with Unit Of Work either.

Abstraction that leaks

The original idea implies that you would keep track of all changes over the entity and submit them all together as one transaction:

interface IUnitOfWork
{
void RegisterAdd(object enitity);
void RegisterUpdate(object enitity);
void RegisterDelete(object enitity);

void Commit();
}

There is a set of those RegisterXXX() methods that are supposed to track our entity lifetime. Usually, we don’t implement them, since EF already has a hidden change tracker mechanism. Not implementing those is again a leaky abstraction.

If you anticipate migration to another framework and still want stuff to be efficient, you must add them. Not only that, you must explicitly use them everywhere in your calling code! 🤯 Have you seen anybody doing that? Neither do I.

A factory?

Usually, Unit Of Work is implemented as a factory for repositories:

interface IUnitOfWork
{
IUserRepository Users { get; }
IOrderRepository Orders { get; }
. . .

void Commit();
}

Previously, I have shown an implementation of UnitOfWork where repositories are created differently, using DI and constructor:

class UnitOfWork: DbContext, IUnitOfWork
{
public IUserRepository Users => base.GetService<IUserRepository>()

public IRepository<TEntity> GetRepository<TEntity>()
{
return new RepositoryBase<TEntity>(this);
}
. . .
}

DI approach has next issues:

  • You have to register all the repositories in the DI
  • It does not prevent developers from injecting Repositories without UnitOfWork
  • It does not allow to build a dependency graph. This is another well-know anti-pattern called ServiceProvider.

Constructor is also not recommended because:

  • You have the DI 🙃
  • You have to manage the lifetime of the repositories, initialize, dispose it
  • You have to manually add decorators if there are ones
  • Changes in the constructor will provoke compilation errors in UnitOfWork
  • One can also argue that with this approach you have a dependency on concrete implementation. I don’t exactly agree with that. You see, from an architecture point, both UnifOfWork and RepositoryBase implementation would live in the same module and know about each other without breaking any dependency rules. But still, let’s count this one

The GetRepository<TEntity>() method has its own flaws. It supposed to be generic, but in fact , it just complicates code navigation.

Sometimes developers implement lazy initialization with Lazy<T> class for the repositories. It is done because… I guess it is really hard to instantiate an empty class, don’t look at me I still have no explanation for it😅

So in the enterprise code, you can find such UnitOfWork where all repositories are injected in the constructor:

class UnitOfWork: IUnitOfWork 
{
private IUserRepository _userRepository;
private IOrderRepository _orderRepository;
. . .

public IUserRepository Users => _userRepository;
public IOrderRepository Orders => _orderRepository;
. . .

public UnitOfWork(
IUserRepository userRepository,
IOrderRepository orderRepository,
. . .
)
{
_userRepository = userRepository;
_orderRepository = orderRepository;
. . .
}

. . .
}

Which means:

  • You have to support the boilerplate interface with tons of properties for each repository
  • You have to support hulking implementation
  • You have to support growing constructor
  • You have to support DI
  • You have to support clunky unit tests
  • You have to explain to your developers, why they can not inject repositories directly into their service

Naah… 😒Just let your developers use repositories directly.

Transactions?

Alright. We already refused to add RegisterXXX() methods. There is also no need for the Unit of Work to be a factory. So that is what we end up with:

interface IUnitOfWork
{
void Commit();
}

A single method to commit all the changes as a single transaction.

Can we at least keep the Commit() method?🙏

There is some stuff, I don’t even know how to tell you 😬 Not every storage supports transactions. You have no transactions when dealing with entities in memory. No transactions with files either. Redis? Nope. Mongo? Nope.

So often developers ignore performance benefits and remove UnitOfWork at all. As a result, you can see changes submitted right away in a repository. Some even argue it is a better abstraction…

class UserRepository: IUserRepository
{
public void Add(User user)
{
_dbContext.Users.Add(user);

// let's hope, there is no tracked changes outside this method 🙏
_dbContext.SaveChanges();
}
}

There is also an example of a repository from Microsoft, where they do a reverse. Unit Of Work is exposed by repositories:

interface IUnitOfWork
{
int SaveChanges();
bool SaveEntities();
}

interface IUserRepository
{
IUnitOfWork UnitOfWork { get; }

void Add(User user);
}

It is so bad, I don’t even want to talk about it 😭

  • Can I use the Unit of Work inside my repository?
  • I have two repositories. Which Unit Of Work should I use? Do I need to use both?
  • Can I call the Unit of Work of another repository? It works.
  • Can I just inject UnitOfWork into the service directly?
  • Which SaveXXX() method do I use?

You got me.

But the repository behaves like a collection

So does DbContext. Moreover, DbContext does it much smoother than repositories. It implements, IEnumerable, which allows using LINQ the same way as you would do with regular collection:

var userList = new List<User>();

var userIds = userList
.Where(u => u.Age > 18)
.OrderBy(u => u.Name)
.Select(u => u.Id)
.ToList();

And now DbContext:

var userSet = new AppDbContext().Set<User>();

var userIds = userSet
.Where(u => u.Age > 18)
.OrderBy(u => u.Name)
.Select(u => u.Id)
.ToList();

To what we have with a repository:

var userRepository = new UserRepository(new AppDbContext());

var userIds = userRepository
.Where(
u => u > 18,
u => u.Name)
.Select(u => u.Id)
.ToList();

If you are lucky, there already could be a method, which not collection-like at all:

var userIds = userRepository.GetIdsOrderedByName(18);

But it is an abstraction level between business logic and database

That is correct. But who said it can not be achieved with DBContext? Just use an interface:

interface IDbContext
{
IQueryable<User> Users { get; }
IQueryable<User> Orders { get; }

void Add<TEntity>(TEntity entity);
void Remove<TEntity>(TEntity entity);

void Complete();
}

Place it in your business logic layer, and you are gucci 😎

(Not really, since you would probably call ToListAsync() which requires your Business logic layer to reference to EF, but we will keep it a secret🤷‍♂️)

But it encapsulates database requests

What is the exact benefit of it? The goal of any application is to receive data from one place, process it, and store it. Moving all the storage-related methods in a single class, results in having God Object with a hundred of methods.

Generic methods are too generic and do not handle all the cases.

Specific methods are too specific. They are usually used in a single place and can not be reused in another.

There is another issue with the methods of a repository. It may not be obvious, but those methods can not be combined:

interface IUserRepository
{
IReadOnlyCollection<User> GetByAge(int age);
IReadOnlyCollection<User> GetByRole(Role role);
}

If you want to filter by age AND role, you need another method.

On the other hand, with DbContext you can write and combine filters in any way you like:

var usersQuery = _dbContext
.Users
.Where(u => u.Age == 18);

if (shouldGetOnlyAdmins)
{
usersQuery = usersQuery.Where(u => u.Role == Role.Admin);
}

var users = usersQuery.ToList();

Shared filter conditions can be moved to extension methods or some sort of predicate or functor. But I want to be honest with you. Whispering: nobody does that 🤫

Just write the query you need in this particular case. If you duplicate one line or two of filter conditions, it is not actually a problem. The use cases are different. When you change filters in one place, it should not affect other places, unless those are domain-specific filters. For business logic predicates, consider the Specification pattern, but only in that case.

But it simplifies unit testing

Certainly, mocking an interface of a repository is a piece of cake. Although there may be some difficulties in mocking DbSet, it is still doable:

var mock = new Mock<AppDbContext>();
mock
.Setup(x => x.Users)
.Returns(new Users[]
{
new User("John", "Doe"),
new User("Jane", "Doe"),
}.AsQueryable());

It gets even better. You would be able to unit test not only your business logic but also your query filters.

But data access can be changed without affecting other layers

Take a deep breath. That’s a big one. 😤

Developers always bring this one when it comes to standing up for repositories.

First of all, it never happens. You won’t be working with SQL for 10 years and then suddenly change it to MongoDb. When starting a new project, there is a phase, in which developers select a set of all the needed tools and frameworks.

Secondly. As I already mentioned above, the repository is an abstraction that leaks. You will feel it when it comes to migration.

Third. In case you actually need to change the DB, EF is an ORM that supports different providers starting from in memory up to Cassandra. So you can easily switch a provider, without heavily changing your code.

Fourth. While It is unlikely for underlying storage to change, it is still posible to swap one ORM to another. I have an experience, when we were migrating from Dapper to EF and as result we have deleted all the repositories 😃

Fifth. You also likely will migrate from one version of EF to a newer one. Replacing your Data Access layer will be a hella complicated work to do, no matter how many abstractions you have.

Sixth. Without repositories that hide any implementation, you can have better naming for your classes. If you are calling Github’s API with an http request name the service GithubApiClient instead of GithubRepository.

When did the Holly Wars begin?

In theory, repositories sound like a panacea for all the problems we were so desperately looking for. In practice, it is not so😕

Despite the weak advantages, it adds lots of additional work for nothing.

In addition, Microsoft only adds fuel to the fire with such line in their documentation:

A DbContext instance represents a combination of the Unit Of Work and Repository patterns such that it can be used to query from a database and group together changes that will then be written back to the store as a unit.

You know what? DbContext implements much more than that. They have stolen all the Fowler works. The same way they copied C# from Java, but never confesses it. Typical Microsoft in one world 😒

But, why would they mention those two patterns so specifically?

Hear me out. I have a theory on this:

I think they created EF. They saw it can not be tested. Suggested to use Repository and Unit of Work instead. Then, on the sly, they have provided a way to unit test it. And finally they were like, “Oh, no. We have never suggested you to implement your own Repository and UnitOfWork. We already did it for you, see the documentation. It’s not us, it’s your fault.”

Typical, typical Microsoft in one world 😒 And don’t you *ucking dare, delete that documentation. I have you on a tape! 😠

The solution

Having all that in mind, is there any better solution to repositories? 🤔

Turns out, there is. Add an interface similar to DbContext. You can place it in any architecture layer you want, up to the Business Logic Layer. Implementation will remain in the Infrastructure anyway:

// Note: keep properties in alphabetical order to simplify navigation
interface IApplicationDbContext
{
// DDD fans can only expose DbSet for aggregates
DbSet<Order> Orders { get; }
DbSet<User> Users { get; }

Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}

It has all the benefits of the repository and they actually work!

In terms of usage, it stays the same, however, this time we don’t pretend to have “an abstraction”, but know the tool we are using:

class UserDomainService
{
public void RenameUser(string oldName, string newName)
{
var user = _appDbContext
.Users
.Single(u => u.FirstName == oldName);

user.FirstName = newName;

_appDbContext.SaveChanges();
}
}

Why is it so hard to admit it?

Abstraction

I guess people just really like abstractions, otherwise I can not explain why Pablo Picasso is so popular😁 Even I have an article that you should abstract away from your UI framework and any infrastructure.

I mean, seeing the reference in your Domain project to EF does bring a feeling of uncomfortness. You have your pure business logic, why would it rely on some infrastructure’s nuget. It sucks. But let us be pragmatic.

What can go wrong about it? It is not as bad as having wrong references between projects. It is not like you don’t have clear boundaries. It is just a nuget. There are a lot of those, that you would not wrap with another abstraction. For example Newtonsoft.Json, Automapper, MediatR, and so on. You wouldn’t worry about EF, if it was part of C#. It would not be a problem if it had a better name, better namespaces, separated nugets for abstraction and implementation, an interface for the DbSet, and so on. But it does, and it makes us nauseous.

The secret here is to think about ORM as another abstraction 🙃

The matter of habit

Repositories have been with us for so long, that it is just hard to let them go.

Developers are really stubborn people. To become a software engineer, you should have a mindset of “everything is possible” guy. When something is not working the way we want it, we would just put more effort into it. The more effort we invest, the harder it is to let it go.

You know, how they said: habits prevent any progress.

Fear of unknown

Some people are just scared to try something new and unknown. It is understandable, after all, the fear is one of primal instinct.

It may sound surprising, but no-repository flashmob is actually a thing that has been going on for a while. Jason Taylor is against repositories. Jimmy Bogard does not use them. The same is true for Steve Smith and Jon P Smith. I am sure, even Uncle Bob, quietly, late at night, when no one is watching, with his laptop under the sheet, writes code without repositories.

Those guys are experts. If we can not trust them, then whom we can?

Is there still a place for repositories?

I would say that you should not add unnecessary complexity — for no reason. Repositories served their purpose as an abstraction when we had to write the DB access logic with raw SQL queries ourselves.

For the libraries where there is no well-established ORM like MongoDb repositories could be useful too.

However, EF Core is itself an abstraction (Unit of Work and Repository) — so why create another abstraction on top of that? That is unnecessary indirection. Especially when you most likely will never change it.

Then we have the DDD-style “repository” which is another concept related to DDD. It is about loading and persisting aggregates. Not just Data Access. Whether or not to implement a repository is your choice. I can not tell if you need it. But my advice is to use EF Core directly since you will just end up with all those issues I have listed above.

There is no shame is having a direct dependency on EF if it makes your life much easier. It is a shame to be a stubborn douchebag.

Making a conclusion

Finally, we can answer is repository antipattern. And the answer is…🥁 🥁 🥁 maybe 😅

An anti-pattern is a pattern that has more drawbacks than advantages. There is still a place for it if you are using Dapper, MongoDb, or another tool that doesn’t have a convenient API. However, with EF it is mostly pointless. It makes your work more complicated and time-consuming without any actual benefits. The only correct way of using repositories with EF is not using it.

If you still think that repository should be used together with EF, then

I hope it is not only because you want it that way. 😒

💬 No, seriously, leave a comment, I am free for discussion. Let us know what other issues with repositories you have faced or how helpful it is.

👨‍👦‍👦For those who agree with me, next time you see an article about how useful repository pattern is, just send them this link. Let them know you will no longer put up with their bullshit. Do not let this repository disease spread out😤

✅ Subscribe

⬇️ Check out my other articles

☕️ Buy me a coffee with a link below

👏 Don’t forget to clap

🙃 And stay awesome

--

--

iamprovidence

👨🏼‍💻 Full Stack Dev writing about software architecture, patterns and other programming stuff https://www.buymeacoffee.com/iamprovidence