Dans cet article nous allons aborder plusieurs sujets qui permettront d’avoir une solution clĂ© en main permettant de gĂ©rer nos donnĂ©es avec Entity Framework.
Dans un premier temps nous rappellerons les principes d’Entity Framework, du pattern Repository et aborderons celui du Unit Of Work.
La partie suivante sera dédiée à son implémentation.
Pré requis
- Connaissance de l’ORM Entity Framework
- Connaissance du pattern Repository
- Connaissance de la modélisation objet
- Installation des packages Nuget Microsoft.EntityFrameworkCore et Microsoft.EntityFrameworkCore.Relational
Approche conceptuelle
Rappel sur Entity Framework
Entity Framework est un ORM (Object Relationnal Mapping). C’est un outil permettant de crĂ©er une couche d’accès aux donnĂ©es (DAL pour Data Access Layer) liĂ©e Ă une base de donnĂ©es relationnelle. Il propose la crĂ©ation d’un schĂ©ma conceptuel composĂ© d’entitĂ©s qui permettent la manipulation d’une source de donnĂ©es, sans Ă©crire une seule ligne de SQL, grâce Ă LinQ To Entities. ComparĂ© Ă d’autres solutions de mapping objet-relationnel (ORM), Entity Framework assure l’indĂ©pendance du schĂ©ma conceptuel (entitĂ©s ou objets) du schĂ©ma logique de la base de donnĂ©es, c’est-Ă -dire des tables. Ainsi, le code produit et le modèle conceptuel ne sont pas couplĂ©s Ă une base de donnĂ©es spĂ©cifique.
Rappel du pattern Repository
Ce design pattern rĂ©pond Ă un besoin d’accès aux donnĂ©es stockĂ©es en base. Son objectif principal est d’isoler la couche d’accès aux donnĂ©es de la couche mĂ©tier.
Il expose diverses mĂ©thodes s’appuyant sur le modèle CRUD (Create, Read, Update, Delete). Dans le contexte d’un projet Entity Framework, un Repository (ou dĂ©pĂ´t) est souvent cantonnĂ© Ă la manipulation d’une entitĂ© spĂ©cifique. On retrouve donc un Repository par entitĂ© gĂ©rĂ©e.
Qu’est ce que le pattern Unit Of Work ?
Unit Of Work est un design pattern qui répond à beaucoup de problèmes de développement et apporte les avantages suivants :
- Il permet de garder en mémoire les modifications logiques de base de données dans un ensemble cohérent.
- Il permet d’orchestrer les opĂ©rations de base sous forme de transactions pour pouvoir annuler les modifications en cas de problèmes.
- Il permet d’isoler votre application des modifications de la couche de donnĂ©es et ainsi faciliter les tests et le partage des dĂ©veloppements.
- Il coordonne le travail des diffĂ©rents Repositories en ne crĂ©ant qu’un seul contexte partagĂ©.

Un peu de généricité dans ce monde complexe
Dans un soucis de performance, de cohĂ©rence, de maintenabilitĂ© et de rapiditĂ© des dĂ©veloppements, le dĂ©veloppeur, flemmard comme Ă son habitude, tend Ă s’orienter, au possible, vers la factorisation et la rĂ©-utilisabilitĂ© de son code.
Dans l’implĂ©mentation suivante, il en sera question. Nous allons crĂ©er des Repositories implĂ©mentant un modèle de Repository gĂ©nĂ©rique.
N’oublions pas de partir sur les bonnes bases du modèle objet
Le modèle objet n’est pas des plus simples Ă apprĂ©hender. Il est plus facile de raisonner de manière linĂ©aire et concrète, que de manière abstraite et avec des notions d’hĂ©ritage ou de polymorphisme. Cela nĂ©cessite un certain recul et une capacitĂ© d’abstraction qui n’est pas forcĂ©ment intuitive et plus difficilement implĂ©mentable.
Dans l’implĂ©mentation qui va suivre, nous utiliserons les principes de l’hĂ©ritage, de l’abstraction, du polymorphisme et les types gĂ©nĂ©riques.
Nous respecterons aussi le principe SOLID.
Implémentation de la solution
Présentation du contexte
Dans cet article nous traiterons de sĂ©ries, de saisons et d’Ă©pisodes. Une sĂ©rie contient une liste de saisons qui chacunes contiennes des Ă©pisodes.
Soit le modèle d’entitĂ©s Entity Framework suivant : chaque netitĂ© que nous voudrons gĂ©rer hĂ©ritera d’une classe abstraite mère nommĂ©e Entity.
// Abstract class used for each entities
public abstract class Entity {
public Guid Id { get; set; }
}
public class Serie: Entity { // So we extends the Entity class
public string Title { get; set; }
public double Rating { get; set; }
public IEnumerable<Season> Seasons { get; set; }
}
public class Season: Entity {
public int Number { get; set; }
public Guid IdSerie { get; set; }
public Serie Serie { get; set; }
}
public class Episode: Entity {
public int Number { get; set; }
public double Duration { get; set; }
public Guid IdSeason { get; set; }
public Season Season { get; set; }
}
La couche repository
Le GenericRepository
Ici, nous allons nous atteler Ă la crĂ©ation d’un modèle gĂ©nĂ©rique de Repository qui pourra ĂŞtre implĂ©mentĂ© par tous nos Entity Repositories.
Le but est de crĂ©er un modèle simple et viable de Repository implĂ©mentant les fonctions de base du CRUD (Create, Read, Update, Delete) et pouvant convenir Ă tous types d’entitĂ©s gĂ©rĂ©es par Entity Framework.
Pour commencer nous allons crĂ©er l’Interface de ce Repository GĂ©nĂ©rique acceptant n’importe quel type d’entitĂ©s hĂ©ritant de la classe abstraite Entity afin de pouvoir l’implĂ©menter plus tard.
// Dans cet article, les commentaires sont présents dans l'Interface mais ne le seront pas dans son implémentation
public interface IGenericRepository<TEntity> where TEntity : Entity
{
#region CREATE
/// <summary>
/// Inserts a new entity.
/// </summary>
/// <param name="entity">The entity to insert.</param>
void Add(TEntity entity);
/// <summary>
/// Inserts a range of entities.
/// </summary>
/// <param name="entities">The entities to insert.</param>
void Add(IEnumerable<TEntity> entities);
#endregion
#region READ
/// <summary>
/// Finds an entity with the given primary key values.
/// </summary>
/// <param name="keyValues">The values of the primary key.</param>
/// <returns>The found entity or null.</returns>
TEntity GetById(params object[] keyValues);
/// <summary>
/// Gets the first or default entity based on a predicate, orderby and children inclusions.
/// </summary>
/// <param name="predicate">A function to test each element for a condition.</param>
/// <param name="orderBy">A function to order elements.</param>
/// <param name="include">Navigation properties separated by a comma.</param>
/// <param name="disableTracking">A boolean to disable entities changing tracking.</param>
/// <returns>The first element satisfying the condition.</returns>
/// <remarks>This method default no-tracking query.</remarks>
TEntity GetFirstOrDefault(
Expression<Func<TEntity, bool>> predicate = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
bool disableTracking = true
);
/// <summary>
/// Gets all entities.
/// </summary>
/// <returns>The all dataset.</returns>
IQueryable<TEntity> GetAll();
/// <summary>
/// Gets the entities based on a predicate, orderby and children inclusions.
/// </summary>
/// <param name="predicate">A function to test each element for a condition.</param>
/// <param name="orderBy">A function to order elements.</param>
/// <param name="include">A function to include navigation properties</param>
/// <param name="disableTracking">A boolean to disable entities changing tracking.</param>
/// <returns>A list of elements satisfying the condition.</returns>
/// <remarks>This method default no-tracking query.</remarks>
IEnumerable<TEntity> GetMuliple(
Expression<Func<TEntity, bool>> predicate = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
bool disableTracking = true
);
/// <summary>
/// Uses raw SQL queries to fetch the specified entity data.
/// </summary>
/// <param name="sql">The raw SQL.</param>
/// <param name="parameters">The parameters.</param>
/// <returns>A list of elements satisfying the condition specified by raw SQL.</returns>
IQueryable<TEntity> FromSql(string sql, params object[] parameters);
#endregion
#region UPDATE
/// <summary>
/// Updates the specified entity.
/// </summary>
/// <param name="entity">The entity.</param>
void Update(TEntity entity);
/// <summary>
/// Updates the specified entities.
/// </summary>
/// <param name="entities">The entities.</param>
void Update(IEnumerable<TEntity> entities);
#endregion
#region DELETE
/// <summary>
/// Deletes the entity by the specified primary key.
/// </summary>
/// <param name="id">The primary key value.</param>
void Delete(object id);
/// <summary>
/// Deletes the specified entity.
/// </summary>
/// <param name="entity">The entity to delete.</param>
void Delete(TEntity entityToDelete);
/// <summary>
/// Deletes the specified entities.
/// </summary>
/// <param name="entities">The entities to delete.</param>
void Delete(IEnumerable<TEntity> entities);
#endregion
#region OTHER
/// <summary>
/// Gets the count based on a predicate.
/// </summary>
/// <param name="predicate">A function to test each element for a condition.</param>
/// <returns>The number of rows.</returns>
int Count(Expression<Func<TEntity, bool>> predicate = null);
/// <summary>
/// Check if an element exists for a condition.
/// </summary>
/// <param name="predicate">A function to test each element for a condition.</param>
/// <returns>A boolean</returns>
bool Exists(Expression<Func<TEntity, bool>> predicate);
#endregion
}
Nous voilĂ donc avec un contrat de Repository. Il n’est cependant pas fonctionnel en l’Ă©tat, nous devons l’implĂ©menter dans une classe mère utilisable en tant que telle pour des besoins simple, mais que les Repositories enfants pourront Ă©tendre. ImplĂ©mentons les mĂ©thodes CRUD et le contexte de base de donnĂ©es.
public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : Entity
{
protected readonly DbContext _dbContext;
protected readonly DbSet<TEntity> _dbSet;
/// <summary>
/// Initializes a new instance of the GenericRepository<TEntity>.
/// </summary>
/// <param name="dbContext">The database context.</param>
public GenericRepository(DbContext dbContext)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
_dbSet = _dbContext.Set<TEntity>();
}
#region CREATE
public virtual void Add(TEntity entity)
{
var entry = _dbSet.Add(entity);
}
public virtual void Add(IEnumerable<TEntity> entities) => _dbSet.AddRange(entities);
#endregion
#region READ
public virtual TEntity GetById(params object[] keyValues) => _dbSet.Find(keyValues);
public virtual TEntity GetFirstOrDefault(
Expression<Func<TEntity, bool>> predicate = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
bool disableTracking = true
)
{
IQueryable<TEntity> query = _dbSet;
if (disableTracking)
{
query = query.AsNoTracking();
}
if (include != null)
{
query = include(query);
}
if (predicate != null)
{
query = query.Where(predicate);
}
if (orderBy != null)
{
return orderBy(query).FirstOrDefault();
}
else
{
return query.FirstOrDefault();
}
}
public IQueryable<TEntity> GetAll()
{
return _dbSet;
}
public virtual IEnumerable<TEntity> GetMuliple(
Expression<Func<TEntity, bool>> predicate = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
bool disableTracking = true
)
{
IQueryable<TEntity> query = _dbSet;
if (disableTracking)
{
query = query.AsNoTracking();
}
if (include != null)
{
query = include(query);
}
if (predicate != null)
{
query = query.Where(predicate);
}
if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
}
public virtual IQueryable<TEntity> FromSql(
string sql,
params object[] parameters
) => _dbSet.FromSql(sql, parameters);
#endregion
#region UPDATE
public virtual void Update(TEntity entity)
{
_dbSet.Update(entity);
}
public virtual void Update(IEnumerable<TEntity> entities) => _dbSet.UpdateRange(entities);
#endregion
#region DELETE
public virtual void Delete(object id)
{
var entityToDelete = _dbSet.Find(id);
if (entityToDelete != null)
{
_dbSet.Remove(entityToDelete);
}
}
public virtual void Delete(TEntity entityToDelete)
{
if (_dbContext.Entry(entityToDelete).State == EntityState.Detached)
{
_dbSet.Attach(entityToDelete);
}
_dbSet.Remove(entityToDelete);
}
public virtual void Delete(IEnumerable<TEntity> entities) => _dbSet.RemoveRange(entities);
#endregion
#region OTHER
public virtual int Count(Expression<Func<TEntity, bool>> predicate = null)
{
if (predicate == null)
{
return _dbSet.Count();
}
else
{
return _dbSet.Count(predicate);
}
}
public virtual bool Exists(Expression<Func<TEntity, bool>> predicate)
{
return _dbSet.Any(predicate);
}
#endregion
}
Notre GenericRepository est maintenant utilisable pour des besoins très simples mais suffisant pour dans de nombreux cas d’utilisation. Nous pourrions d’ores et dĂ©jĂ l’utiliser de la façon suivante :
using (var context = new MyContext()) {
var serieRepo = new GenericRepository<Serie>(context);
// Get all the series rating equal 10/20 order by names
var series = serieRepo.GetMultiple(
predicat: (
s => s.Rating == 10d
),
/*
* If you also want to retrieve each seasons and their episodes
* inclusions: (
* source => source.Include(serie => serie.Seasons).ThenInclude(season => season.Episodes)
* ),
*/
orderBy: (
s => s.OrderByDescending(s1 => s1.Rating)
)
);
series.ForEach(var serie in series) {
Console.Writeline($"Nom : {serie.Title} / Rating : {serie.Rating}");
}
}
Notre Repository gĂ©nĂ©rique est près Ă rĂ©pondre Ă quasiment tous les besoins. Nous allons quand mĂŞme aller un peu plus loin et prĂ©voir d’hĂ©riter de ce modèle gĂ©nĂ©rique dans des Repositores qui auraient des fonctions plus complexes que nos fonctions de CRUD. Aussi les mĂ©thodes du GenericRepository sont dĂ©clarĂ©es virtual pour pouvoir ĂŞtre implĂ©mentĂ©es et redĂ©finies, et notre classe mère est hĂ©ritable.
Dans l’exemple suivant nous allons Ă©crire un CustomSerieRepository qui ajoutera une fonction implĂ©mentant un requĂŞte LINQ plus complexe comme un union entre deux tables ou la lecture d’une vue etc…
Commençons par Ă©crire l’interface :
public interface ICustomSerieRepository : IGenericRepository<Serie>
{
#region READ
IEnumerable<Serie> ComplexQuery();
#endregion
}
Puis son implémentation :
public class CustomSerieRepository : GenericRepository<Serie>, ICustomSerieRepository
{
/// <summary>
/// Initializes a new instance of the CustomSerieRepository.
/// </summary>
/// <param name="dbContext">The database context.</param>
public CustomSerieRepository(DbContext dbContext)
: base(dbContext)
{
}
#region READ
public IEnumerable<Serie> ComplexQuery()
{
// Simulate a complex query
return new List<Serie>();
}
#endregion
}
Ce CustomSerieRepository pourrait s’utiliser de la façon suivante :
using (var context = new MyContext()) {
var customSerieRepo = new CustomSerieRepository(context);
// Get all the series rating equal 10/20 order by names
var series = customSerieRepo .GetMultiple(
predicat: (
s => s.Rating == 10d
),
/*
* If you want also to retrieve each seasons and their episodes
* inclusions: (
* source => source.Include(serie => serie.Seasons).ThenInclude(season => season.Episodes)
* ),
*/
orderBy: (
s => s.OrderByDescending(s1 => s1.Rating)
)
);
series.ForEach(var serie in series) {
Console.Writeline($"Nom : {serie.Title} / Rating : {serie.Rating}");
}
// Retrieve series with complex query
var seriesFromComplexQuery = customSerieRepo.ComplexQuery();
}
VoilĂ donc deux façons de manipuler des Series via les Repositories : de la manière gĂ©nĂ©rique ou par l’hĂ©ritage.
Gardez Ă l’esprit que toute manipulation mĂ©tier d’une ou plusieurs entitĂ©s trouvera sa place dans le Business Layer, le Repository ne fait que l’interface entre la base de donnĂ©es et le modèle.
La couche Unit Of Work
Le Unit Of Work, pour rappel vient se placer entre la couche de service (Business Layer) et la couche de données (Data Access Layer).
Il a la responsabilité du DBContext, des différents Repositories et de la cohérence transactionnelle des opérations.
Commençons encore une fois par l’Interface. Celle-ci dĂ©crit le comportement du Unit Of Work.
Il implĂ©mente l’Interface IDisposable pour pouvoir ĂŞtre supprimĂ© de la mĂ©moire au besoin ou soit dans un bloc d’instruction using. Cela nous permet d’imposer une durĂ©e de vie au contexte afin de ne pas maintenir de connexions ouverte, de contextes chargĂ©s en mĂ©moire etc…
// Dans cet article, les commentaires sont présents dans l'Interface mais ne le seront pas dans son implémentation
public interface IUnitOfWork<TContext> : IDisposable where TContext : DbContext
{
/// <summary>
/// Gets the db context.
/// </summary>
/// <returns>The instance of type TContext.</returns>
TContext DbContext { get; }
/// <summary>
/// Gets the specified repository for the TEntity.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <returns>An instance of type inherited from GenericRepository interface.</returns>
IGenericRepository<TEntity> GetRepository<TEntity>() where TEntity : Entity;
/// <summary>
/// Executes the specified raw SQL command.
/// </summary>
/// <param name="sql">The raw SQL.</param>
/// <param name="parameters">The parameters.</param>
/// <returns>The number rows affected.</returns>
int ExecuteSqlCommand(string sql, params object[] parameters);
/// <summary>
/// Uses raw SQL queries to fetch the specified TEntity data.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="sql">The raw SQL.</param>
/// <param name="parameters">The parameters.</param>
/// <returns>An IQueryable for TEntity that contains elements that satisfy the condition specified by raw SQL.</returns>
IQueryable<TEntity> FromSql<TEntity>(string sql, params object[] parameters) where TEntity : Entity;
/// <summary>
/// Commit all changes made in this context to the database.
/// </summary>
/// <returns>The number of state entries written to the database.</returns>
int Save();
}
Et voici son implémentation :
public class UnitOfWork<TContext> : IUnitOfWork<TContext> where TContext : DbContext
{
private readonly TContext _context;
private bool disposed = false;
private Dictionary<Type, object> _repositories;
private ICustomSerieRepository _customSerieRepo;
/// <summary>
/// Initializes a new instance of the UnitOfWork<TContext>.
/// </summary>
/// <param name="context">The context.</param>
public UnitOfWork(TContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public TContext DbContext => _context;
public IGenericRepository<TEntity> GetRepository<TEntity>() where TEntity : Entity
{
if (_repositories == null)
{
_repositories = new Dictionary<Type, object>();
}
var type = typeof(TEntity);
if (!_repositories.ContainsKey(type))
{
_repositories[type] = new GenericRepository<TEntity>(_context);
}
return (IGenericRepository<TEntity>)_repositories[type];
}
public ICustomSerieRepository CustomSerieRepository {
get
{
if (_customSerieRepo == null) {
_customSerieRepo = new CustomSerieRepository(_context);
}
return _customSerieRepo;
}
}
public int ExecuteSqlCommand(
string sql,
params object[] parameters
) => _context.Database.ExecuteSqlCommand(sql, parameters);
public IQueryable<TEntity> FromSql<TEntity>(
string sql,
params object[] parameters
) where TEntity : Entity => _context.Set<TEntity>().FromSql(sql, parameters);
public int Save()
{
return _context.SaveChanges();
}
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
_repositories.Clear();
_context.Dispose();
}
}
this.disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
La class UnitOfWork contient le contexte de base de donnĂ©es. Il est souvent liĂ© Ă la vie de la session utilisateur et retourne des IGenericRepository<TEntity> et custom Repositories au besoin. Elle s’occupe du commit de la transaction des opĂ©rations sur la base de donnĂ©es et rend le tout disposable.
Nous pouvons maintenant l’utiliser dans un cas un peu plus concret en profitant de l’injection de dĂ©pendance proposĂ©e par Microsoft .Net Core.
Prenons l’exemple d’un projet APP.NET Core.
Le fichier Statup.cs devra être configuré comme suit :
...
public void ConfigureServices(IServiceCollection services)
{
// Your code
services.AddScoped<IUnitOfWork<MyContext>, UnitOfWork<MyContext>>();
services.AddScoped<ISerieService>(
provider => new SerieService
(
provider.GetRequiredService<ILogger<ICarteService>>(),
provider.GetService<IUnitOfWork>()
)
);
}
...
La classe de service MyService pourrait ĂŞtre la suivante :
public class MyService : IMyService {
private ILogger _logger;
private IUnitOfWork _uow;
public MyService(ILogger logger, IUnitOfWork uow) {
_logger = logger;
_uow = uow;
}
public void MyFunction() {
// Get series
var series = _uow.CustomSerieRepository.ComplexQuery();
// Update each series ratings
series.ForEach(var serie in series) {
serie.Rating = serie.Rating + 1;
}
// Insert a new episode
_uow.GetRepository<Episode>().Insert(new Episode { ... });
// Delete the first season of the first serie
_uow.GetRepository<Season>().Delete(series[0].Season[0].IdSeason);
try {
// Commit the operations transaction
_uow.Save();
}
catch (Exception ex) {
_logger.Error(ex, "Reason");
// If needed, dispose the Unit Of Work
// _uow.Dispose();
}
}
}
Ou :
using (var uow = UnitOfWork(new MyContext())) {
// Get series
var series = _uow.CustomSerieRepository.ComplexQuery();
// Update each serie ratings
series.ForEach(var serie in series) {
serie.Rating = serie.Rating + 1;
}
// Insert new episode
_uow.GetRepository<Episode>().Insert(new Episode { ... });
// Delete the first season of the first serie
_uow.GetRepository<Season>().Delete(series[0].Season[0].IdSeason);
// Commit transaction
_uow.Save();
}
Conclusion
Vous avez maintenant les billes pour implĂ©menter Ă votre façon un modèle gĂ©nĂ©rique de Repositories et du Unit Of Work. Ce code a Ă©tĂ© proposĂ© Ă titre d’exemple. Le Unit Of Work est lĂ pour rĂ©pondre Ă des besoins spĂ©cifiques qui ne seront pas forcĂ©ment les vĂ´tres. Le Unit Of Work propose une programmation transactionnelle, mais d’autres modèles existent. Utilisez le donc en connaissance de causes et uniquement s’il apporte une plus-value Ă vos projets.
Dans le développement, la généricité est une bonne chose mais peut conduire à une certaine complexité de code et pourrait ne pas répondre à tous les besoins de votre application.




article bien utile pour une implémentation de UnitOfWork existante que je voulais corriger
merci !
Tres bonne article, merci!
Bonjour, pourriez vous faire un example de test unitaire avec ce pattern
merci