Use Include with Generic Repo and IUnitOfWork


Consider the scenario that you want to structure your repositories with Generic Repository and IUnitOfWork, but you want to allow for repositories clients to use includes.

Two of the options are:

  1. Make your repositories to return IQuerables
  2. Implement the "Include" functionality on your Generic Repository.

The first option is straightforward, here, we will demo just another way to achieve the second option. Before we proceed with the implementation and its explanation, we should highlight two very important steps before we begin!

The entities that have an Id should be derived by a base entity that will "offer" the common property. Of course, this base entity can have more common properties, but the Id is important! Remember that the "params" is an optional array by default!

Let's consider these entities:

public abstract class BaseEntity
{
public Guid Id { get; set; } = Guid.NewGuid();
// Other common properties...
}

public class Player : BaseEntity
{
public string Name { get; set; } = "Guest";

// Each player can participate in many games
public ICollection<GamePlayer> GamePlayers { get; set; } = new List<GamePlayer>();
// Each player can create many games
public ICollection<Game> CreatedGames { get; set; } = new List<Game>();
}

public class Game : BaseEntity
{
// Each game can have many players
public ICollection<GamePlayer> GamePlayers { get; set; } = new List<GamePlayer>();
public Guid CreatorId { get; set; }
public Player Creator { get; set; }
public DateTime? Created { get; set; }
public bool IsClosed { get; set; }
}

public class GamePlayer
{
public Guid GameId { get; set; }
public Game Game { get; set; }
public Guid PlayerId { get; set; }
public Player Player { get; set; }
}

BaseEntity: This is an abstract class that defines common properties for any entity in the database, here, an Id of type Guid.

Player: Inherited from BaseEntity, this class represents the data model for a player. It has a Name, a list of GamePlayer representing the games in which the player participates and a list of Game representing the games created by the player.

Game: Again, derived from BaseEntity, this class represents a game. It has properties for the list of GamePlayer (representing the players who participated in this game), CreatorId (ID of the player who created the game), Creator (Creator player object), Created (creation timestamp of the game), and IsClosed (indicates if the game is closed).

GamePlayer: This class represents a many-to-many relationship between Game and Player. It has GameId, Game, PlayerId, and Player.

Now let's get into the cool part!

Generic Repository

public interface IRepository<T> where T : class
{
Task<T?> GetById(Guid id, params Expression<Func<T, object>>[] includes);
Task<IEnumerable<T>> GetAll(params Expression<Func<T, object>>[] includes);
Task Create(T entity);
Task<IEnumerable<T>> Find(Expression<Func<T, bool>> predicate);
void Update(T entity);
void Delete(T entity);
}

Task<T?> GetById(Guid id, params Expression<Func<T, object>>[] includes):

An async method that takes a Guid as an id and several expressions to include related entities to the query. It's designed to retrieve an entity of type T identified by its id from the data source. It returns a task with the result being the entity, if exists.

Task<IEnumerable<T>> GetAll(params Expression<Func<T, object>>[] includes):

An async method that takes several expressions to include related entities to the query. It's designed to retrieve all entities of type T from the data source.

Task Create(T entity):

An async method to create or add a new entity of type T to the data source.

Task<IEnumerable<T>> Find(Expression<Func<T, bool>> predicate): An async method to find entities of type T from the data source based on a predicate or condition.

void Update(T entity):

A method to update an existing entity of type T.

void Delete(T entity):

A method to delete an entity of type T from the data source.

Now this is where the fun begins!

Generic Repository Implementation

public class Repository<T> : IRepository<T> where T : class
{
protected readonly DefaultContext context;

public Repository(DefaultContext context)
{
this.context = context;
}

public async Task<IEnumerable<T>> GetAll(params Expression<Func<T, object>>[] includes)
{
var query = context.Set<T>().AsQueryable();

foreach (var include in includes)
{
query = query.Include(include);
}

return await query.ToListAsync();
}

public async Task<T?> GetById(Guid id, params Expression<Func<T, object>>[] includes)
{
IQueryable<T?> query = context.Set<T>();
foreach (var include in includes)
{
query = query.Include(include!);
}
// Use the base class `Entity` to access `Id` property
return await query.SingleOrDefaultAsync(e => ((e as BaseEntity)!).Id == id);
}

public async Task Create(T entity)
{
await context.Set<T>().AddAsync(entity);
}

public void Update(T entity)
{
context.Set<T>().Update(entity);
}

public async Task<IEnumerable<T>> Find(Expression<Func<T, bool>> predicate)
{
return await context.Set<T>().Where(predicate).ToListAsync();
}

public void Delete(T entity)
{
context.Set<T>().Remove(entity);
}
}

Here is a breakdown of the functionalities implemented in the Repository<T> class, where T is a generic type representing an Entity (i.e., a class that corresponds to a database table):


Repository(DefaultContext context):

This is the constructor that receives the database context and stores it in a protected member variable for access by the other class methods.


GetAll(params Expression<Func<T, object>>[] includes):

This asynchronous method is used to get all records of the entity T from the database. It accepts an optional parameter includes which represents the navigation properties to include (eager load) when querying the database.


GetById(Guid id, params Expression<Func<T, object>>[] includes):

This asynchronous method is used to get a specific entity T by Id. It also accepts an optional parameter includes for eager loading.


Create(T entity):

This asynchronous method is used to add a new entity to the database.


Update(T entity):

This method updates an existing entity in the database.


Find(Expression<Func<T, bool>> predicate):

This asynchronous method allows you to find specific entities in the database that match a given predicate or criteria.


Delete(T entity):

This method removes an entity from the database. All these methods interact with the context which is an instance of DefaultContext, a class deriving from Microsoft.EntityFrameworkCore.DbContext. This class manages the entity model (classes that map to database tables).

The IUnitOfWork interface

public interface IUnitOfWork
{
IGameRepository Games { get; }
IRepository<GamePlayer> GamePlayers { get; }
IRepository<Player> Players { get; }
Task<int> SaveAsync();
}

The IUnitOfWork implementation

public class UnitOfWork : IUnitOfWork
{
private readonly DefaultContext _context;
private IGameRepository _games;
private IRepository<GamePlayer> _gamePlayers;
private IRepository<Player> _players;

public UnitOfWork(DefaultContext context)
{
_context = context;
}

public IGameRepository Games => _games ??= new GameRepository(_context);

public IRepository<GamePlayer> GamePlayers => _gamePlayers ??= new GamePlayerRepository(_context);

public IRepository<Player> Players => _players ??= new PlayerRepository(_context);

public async Task<int> SaveAsync()
{
return await _context.SaveChangesAsync();
}
}

 

Usage

var allPlayers = await _unitOfWork.Players.GetAll();
var allGames = await _unitOfWork.Games.GetAll();
var allGamePlayers = await _unitOfWork.GamePlayers.GetAll();

or with includes:

var allPlayersIncluding = await _unitOfWork.Players.GetAll(p => p.GamePlayers, p => p.CreatedGames);
var allGamesIncluding = await _unitOfWork.Games.GetAll(g => g.GamePlayers, g=> g.Creator);
var allGamePlayersIncluding = await _unitOfWork.GamePlayers.GetAll(gp => gp.Player, gp => gp.Game);

Tip: Depending on your project's setup, Core version, or execution cycle you may get the Includes even if you do not specify them. If you want to make sure that you have the entities included make sure to add them.

If you want to quickly try it out then do the below:

DbContext

public class DefaultContext  : DbContext
{
public DefaultContext(DbContextOptions<DefaultContext> options)
: base(options)
{
}

public DbSet<Player> Players { get; set; }
public DbSet<Game> Games { get; set; }
public DbSet<GamePlayer> GamePlayers { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var player1Id = new Guid("11111111-1111-1111-1111-111111111111");
var player2Id = new Guid("22222222-2222-2222-2222-222222222222");
var game1Id = new Guid("33333333-3333-3333-3333-333333333333");
var game2Id = new Guid("44444444-4444-4444-4444-444444444444");

var player1 = new Player { Id = player1Id, Name = "Player1" };
var player2 = new Player { Id = player2Id, Name = "Player2" };

var game1 = new Game { Id = game1Id, CreatorId = player1Id, Created = DateTime.UtcNow };
var game2 = new Game { Id = game2Id, CreatorId = player2Id, Created = DateTime.UtcNow };

var gamePlayer1 = new GamePlayer { GameId = game1Id, PlayerId = player1Id };
var gamePlayer2 = new GamePlayer { GameId = game1Id, PlayerId = player2Id };
var gamePlayer3 = new GamePlayer { GameId = game2Id, PlayerId = player1Id };

modelBuilder.Entity<Player>().HasData(player1, player2);

modelBuilder.Entity<Game>().HasData(game1, game2);

modelBuilder.Entity<GamePlayer>().HasData(gamePlayer1, gamePlayer2, gamePlayer3);

modelBuilder.Entity<GamePlayer>()
.HasKey(gp => new { gp.GameId, gp.PlayerId });

modelBuilder.Entity<GamePlayer>()
.HasOne(gp => gp.Game)
.WithMany(g => g.GamePlayers)
.HasForeignKey(gp => gp.GameId);

modelBuilder.Entity<GamePlayer>()
.HasOne(gp => gp.Player)
.WithMany(p => p.GamePlayers)
.HasForeignKey(gp => gp.PlayerId);

modelBuilder.Entity<Game>()
.HasOne(g => g.Creator)
.WithMany(p => p.CreatedGames)
.HasForeignKey(g => g.CreatorId)
.OnDelete(DeleteBehavior.Restrict);
}
}

Register with DI container

// Add services to the container.
builder.Services.AddDbContext<DefaultContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
builder.Services.AddScoped<IGameRepository, GameRepository>();
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();

ConnectionString

"ConnectionStrings": {
"DefaultConnection": "Server=XXX.XXX.XX.XX\\SQLEXPRESS; Initial Catalog=XYZ;user id=XYZ;password=XYZ;Trusted_Connection=false;MultipleActiveResultSets=true;TrustServerCertificate=True"
},

(replace X,Y,Z with yours)





No files yet, migration hasn't completed yet!