Skip to content

Instantly share code, notes, and snippets.

@grofit
Created January 7, 2015 11:45
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save grofit/94e4d9d2abf518574bc9 to your computer and use it in GitHub Desktop.
Save grofit/94e4d9d2abf518574bc9 to your computer and use it in GitHub Desktop.
A GOOD Generic Repository Pattern
public class Account
{
Guid Id {get;set;}
string Name {get;set;}
}
public class DatabaseRepository<T> : IRepository<T>
{
private IDbConnection _connection;
public DatabaseRepository(IDbConnection connection)
{ _connection = connection; }
// Crud is a given
public IEnumerable<T> Find(IFindQuery<T> query)
{ return query.Find(_connection); }
public void Execute(IExecuteQuery<T> query)
{ query.Execute(_connection); }
}
// This allows you to not have to fudge all your logic into your repository classes.
// So this stops individual Repositories like PlayerRepository : SomeRepository<Player> with custom GetAllPlayers methods
public GetAllPlayersInAccountQuery : IFindQuery<Player>
{
public Guid AccountId {get; private set;}
public GetAllPlayersInAccountQuery(Guid accountId)
{ AccountId = accountId; }
public IEnumerable<Player> Find(IDbConnection connection)
{
using(var playerQuery = connection.GetCollection<Player>())
{ return playerQuery.Where(x => x.AccountId == AccountId); }
}
}
public interface IExecuteQuery<T>
{
void Execute(IDbConnection connection);
}
public interface IFindQuery<T>
{
IEnumerable<T> Find(IDbConnection connection); // this can be abstracted further to reduce IDbConnection
}
public interface IRepository<T> // Possibly add K for key
{
T Get(int id); // you may want to use K as a generic for the key
void Update(T entity);
void Save(T entity);
void Delete(T entity);
IEnumerable<T> Find(IFindQuery<T> query);
void Execute(IExecuteQuery<T> query);
}
public class Player
{
Guid AccountId {get;set;}
Guid Id {get;set;}
string Name {get;set;}
PlayerType Type {get;set;}
}
public class SomeClass
{
private IRepository<Player> _playerRepository;
public SomeClass(IRepository<Player> playerRepository)
{
_playerRepository = playerRepository; // or if you want to ignore IoC new DatabaseRepository(new SomeConnection());
}
public void GetPlayers()
{
var currentAccount = // Get Account Somehow
var getAllPlayersInAccountQuery = new GetAllPlayersInAccountQuery(currentAccount.Id);
return _playerRepository.Find(getAllPlayersInAccountQuery);
}
}
@grofit
Copy link
Author

grofit commented Dec 13, 2021

Overview

As I end up showing this to a lot of people I thought I should put some comments on as to why this is better than a conventional IRepository approach.

Common Generic Repository Pattern

So most Generic Repository Pattern approaches tell you to make an IRepository then implement a specific repository for each model or logical need you have, i.e UserRepository, SkillsRepository, GroupsRepository etc. Then within each of these repositories you end up with a method for each set of data you want back, i.e:

public class UserRepository : IRepository
{
   public IEnumerable<User> GetAllUsersAgedOver(int age) { ... }
}

So in this scenario you can have loads of methods all doing different things and everyone is happy, i.e userRepository.GetUsersWhoAreAdmins() and userRepostory.GetAllUsers() etc however this slowly becomes a dumping ground and there is also one HUGE downside to this approach.

Downsides Of This Pattern

As all the logic for getting data from the underlying data source (be it a database, in memory list, xml file etc) is contained at the repository level, if you want to have a method in another repository access the logic you need to have one repository access another, i.e.

public class SkillsRepository : IRepository
{
   public GroupRepository GroupRepository {get;} // set via constructor? assuming no interface, should probably have one too

   public IEnumerable<Skills> GetAllSkillsFromGroup() { ... } // Call group repo to get users in group then extract all their skills
}

So this in best case causes interdependencies between repositories, and at worst case can cause circular dependencies which are going to cause you a big headace. You may be luck and you never need to share logic between repositories, or you may just copy the query logic over which isnt very DRY of you, but it will at least get the job done.

So the problems with the original design that is often touted is:

  • Implementation per model type
  • Lots of implementations to maintain
  • Potentially become dumping grounds with each new permutation of logic needed
  • Can lead to circular dependencies or lots of duplicated logic

So Why This Pattern?

This pattern solves the issues above as we only really need an implementation per data source and then each specific bit of query logic becomes an IFindQuery or IExecuteQuery, which makes them easier to re-use across other repositories if needed as you can just new them up and use them within other queries.

If you had lots of logic in your repositories in the old world, you would end up with lots of queries and it now becomes a namespace separation issue rather than a repository separation issue.

Improvements?

This was thrown together 7 years ago on the fly to just show a possible approach, I have omitted a very useful feature like ISpecificQuery<T> or whatever you want to call it where it can return data separate to the repository generic, i.e:

public interface IRepository<T> // Possibly add K for key
{
   // Crud
   
   // Current query mechanisms
   IEnumerable<T> Find(IFindQuery<T> query);
   void Execute(IExecuteQuery<T> query);

   // New specific query
   TSpecific Find<TSpecific>(ISpecific<TSpecific> query);
}

This allows you to basically return anything from any repository, which can be useful for more specific queries which need to do very specific stuff.

You can also potentially split into IReadRepository and IWriteRepository too, which would allow separation between the logic of reading and writing.

I have put more details on this in Development For Winners Ebook.

Ultimately the only downside of this approach is potentially you have a new file per query, vs one file (the old repository) containing all logic, but your DI configuration becomes:

// old world DI
container.Bind<IUserRepository>().To<UserRepository>();
container.Bind<IGroupRepository>().To<GroupRepository>();

// new world DI
container.Bind<IRepository<User>>().To<Repository<User>>();
container.Bind<IRepository<Group>().To<Repository<Group>>(); // You could have a more specific interface though

As you can see its always the same repository just a different generic, which literally is only there to enforce the Find generic type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment