.Net

Generic Repository, Unit Of Work et Entity Framework

Sep 5, 2019

Samuel Hedouin

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.

3 Commentaires

  1. Xavier Averbouch

    article bien utile pour une implémentation de UnitOfWork existante que je voulais corriger
    merci !

    Réponse
  2. Dimitri

    Tres bonne article, merci!

    Réponse
  3. hich

    Bonjour, pourriez vous faire un example de test unitaire avec ce pattern
    merci

    Réponse

Soumettre un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Découvrez nos autres articles

Databricks AI Summit 2025

Databricks AI Summit 2025

Après le Snowflake Summit, Databricks a pris le relais au Data + AI Summit 2025 avec une évolution notable. La plateforme ne se limite plus à l’ingénierie ou à la science des données : elle se positionne désormais comme un système d’activation intelligent, où modèles,...

lire plus
Vous connaissiez Snowflake… mais avez-vous vu les nouveautés annoncées lors du Summit 2025 ?

Vous connaissiez Snowflake… mais avez-vous vu les nouveautés annoncées lors du Summit 2025 ?

On le savait : Snowflake, ce n’est pas juste un entrepôt de données cloud. Mais au Summit 2025, on a assisté à une transformation : la plateforme devient un véritable système intelligent de données. IA générative intégrée, ingestion temps réel simplifiée, compute...

lire plus
Aller au contenu principal