Clean Architecture .NET API - Partie 1 - Création, structure et base

Tuto : Vous souhaitez mettre en place une architecture propre et maintenable sur votre API .NET Core ? Vous êtes au bon endroit ! À la fin de cet article, vous aurez une API REST avec une architecture clean
alexJournet.jpg
Alexandre JOURNETMis à jour le 3 Nov 2023
Clean Architecture .NET API - Partie 1 - Création, structure et base - tuto

Vous souhaitez mettre en place une architecture propre et maintenable sur votre APIUne API est un programme permettant à deux applications distinctes de communiquer entre elles et d’échanger des données. .NET Core ? Vous êtes au bon endroit !

À la fin de cet article, vous aurez une API RESTREST (REpresentational State Transfer) est un style d'architecture logicielle qui fonctionne sous un certain nombre de contraintes. avec une architecture clean !

C’est parti ! 🎉

Commençons par structurer notre projet.

Partie 1 - Création, structure et base

Création du projet

Nous allons initialiser notre projet avec l’API Web ASP.NET.NET est le principal framework de l'univers Microsoft. Core, pour commencer avec un exemple de contrôleur. 

Clean Architecture .NET API - Partie 1 - Création, structure et base - Créer un projet

Vous nommez votre projet comme vous le souhaitez : 

Clean Architecture .NET API - Partie 1 - Création, structure et base - Configurer nouveau projet

J’ai choisi le .NET 6 la dernière version LTS au moment de créer cet article.

(NB1 : la différence principale avec les versions précédentes est le regroupement des fichiers Startup.cs et Program.cs en un seul fichier afin de simplifier le lancement d’une application)

(NB2 : Si vous choisissez une version inférieure à .NET6, il y aura certaines différences au niveau des injections de dépendances, je spécifierai pour chaque version.) 

Clean Architecture .NET API - Partie 1 - Création, structure et base - informations supplémentaires

À partir de ce point, nous avons notre projet prêt à être lancé. Nous pouvons commencer la mise en place de la structure !

Structure du projet

Nous allons créer 4 (ou plus si besoin) nouveaux projets Bibliothèque de classe :

  • Un projet API contenant tous nos contrôleurs
  • Un projet Applications contenant toute la logique de l’application
  • Un projet Infrastructure contenant tous les liens avec la base de données
  • Un projet Domain contenant tous nos modèles, DTO, etc.

Nous aurons donc l’architecture suivante : 

Clean Architecture .NET API - Partie 1 - Création, structure et base - architecture exemple

Commençons par le projet Domain :

Dans celui-ci, nous allons créer plusieurs dossiers :

  • Base : Contient l’entité de base, BaseEntity

  • Models : Contient tous nos modèles qui dérive de BaseEntity

  • DTO : Contient tous nos DTO

    Les suivants sont facultatifs :

  • Request : Contient tous nos records qui vont servir dans les requêtes

  • Query : Contient des objets qui permettront de mapper directement des résultats de requêtes SQLLangage permettant de communiquer avec une base de données. dans ceux-ci

NB : Les dossiers Request et Query ne sont pas obligatoires. C’est seulement dans le cas d’une grosse application, on préfère les séparer des DTO pour plus de lisibilité.

Dans le projet Infrastructure :

  • Base : Contient le BaseRepository ainsi que son interface pour définir toutes les méthodes communes à tous nos services
  • Database : Contient notre DbContext, appelé ici CoreDbContext et nos configurations (Pour plus de lisibilité dans le DbContext)
  • Repository : Contient tous nos repositories

NB : Le dossier Migrations d’EntityFramework sera créé automatiquement lors de notre première migration

Dans le projet Applications :

  • Base : Contient le BaseService ainsi que son interface pour définir toutes les méthodes communes à tous nos services
  • Services : Contient tous nos services

On obtient le schéma de solution ci-dessous, qui nous permet de séparer chaque couche de notre application : 

Clean Architecture .NET API - Partie 1 - Création, structure et base - schéma de solution

Passons à l’implémentation de nos fichiers de base !

Implémentation des fichiers de base

On va commencer par créer notre entité par défaut :

BaseEntity.cs Projet Domain, dossier Base

public abstract class BaseEntity
{
        public int Id { get; set; }
}

NB: Pour une application déjà existante, on peut se passer du BaseEntity et utiliser “class” à la place dans les templates ci-dessous.

Grâce à celle-ci, nous allons pouvoir implémenter nos interfaces de base pour les repositories et les services :

IBaseRepository.cs → Projet Infrastructure, dossier Base

public interface IBaseRepository<T> where T : BaseEntity
{
        Task<T> GetById(int id);
        Task<List<T>> GetAllAsync();
        Task<List<T>> GetAllAsync(Expression<Func<T, bool>> where);
}

(On pourra rajouter d’autres méthodes génériques par la suite, comme par exemple la sauvegarde, la mise à jour, la suppression, la récupération via une requête SQL native, etc.)

IBaseService.cs → Projet ApplicationC'est un programme conçu pour effectuer une ou plusieurs tâches. Réaliser des applications, c'est notre cœur de métier chez AXOPEN !, dossier Base

public interface IBaseService<T> where T : BaseEntity
{
        Task<T> GetById(int id);
        Task<List<T>> GetAllAsync();
        Task<List<T>> GetAllAsync(Expression<Func<T, bool>> where);
}

(Même remarque que ci-dessus, on pourra avoir des méthodes métiers génériques, par exemple, récupérer l’utilisateur courant)

J’ai seulement implémenté quelques fonctions qui vont nous permettre de récupérer des données.

On pourra aussi utiliser le pattern Specification, qui nous permettra d’ajouter une autre couche d’abstraction pour la récupération de données, pour spécifier les données que nous souhaitons.

Mais cela sera l’objet d’un prochain article !

Reprenons nos créations avec le dbContext, que j’ai nommé CoreDbContext :

CoreDbContext.cs → Projet Infrastructure, dossier Database

public class CoreDbContext : DbContext
{

        public CoreDbContext(DbContextOptions<CoreDbContext> options) : base(options)
        { }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
        }
}

Par la suite on va pouvoir implémenter nos BaseRepository et BaseService :

BaseRepository.cs → Projet Infrastructure, dossier Base

public class BaseRepository<TEntity, TContext> : IBaseRepository<TEntity> 
        where TEntity : BaseEntity
        where TContext : CoreDbContext
{
        protected readonly TContext _dbContext;

        public BaseRepository(TContext context)
        {
            _dbContext = context;
        }

        public async Task<TEntity> GetById(int id)
        {
            try
            {
                return await _dbContext.Set<TEntity>().FindAsync(id);
            }
            catch (Exception ex)
            {
                throw new Exception($"Impossible de récupérer l'entité: {ex.Message}");
            }
        }

        public async Task<List<TEntity>> GetAllAsync()
        {
            try
            {
                return await _dbContext.Set<TEntity>().AsNoTracking().ToListAsync();
            }
            catch (Exception ex)
            {
                throw new Exception($"Impossible de récupérer les entités: {ex.Message}");
            }
        }

        public async Task<List<TEntity>> GetAllAsync(Expression<Func<TEntity, bool>> where)
        {
            try
            {
                return await _dbContext.Set<TEntity>().Where(where).AsNoTracking().ToListAsync();
            }
            catch (Exception ex)
            {
                throw new Exception($"Impossible de récupérer les entités: {ex.Message}");
            }
        }
}

BaseService.cs → Projet Application, dossier Base

public class BaseService<TRepository, TEntity> : IBaseService<TEntity>
        where TRepository : IBaseRepository<TEntity>
        where TEntity : BaseEntity
{

        protected readonly TRepository _repository;

        public BaseService(TRepository repository)
        {
            _repository = repository;
        }
        public async Task<TEntity> GetById(int id)
        {
            return await _repository.GetById(id);
        }

        public async Task<List<TEntity>> GetAllAsync()
        {
            return await _repository.GetAllAsync();
        }

        public async Task<List<TEntity>> GetAllAsync(Expression<Func<TEntity, bool>> where)
        {
            return await _repository.GetAllAsync(where);
        }
}

On a fini tout ce qui est initialisation des nos fichiers de base ! Ils seront utiles et alimentés tout au long de votre projet !

On peut maintenant commencer à monter notre API !

RDV dans cet article pour continuer ce tuto !