Fermer

avril 29, 2023

Construire une API CRUD avec Dapper

Construire une API CRUD avec Dapper


Dapper est aujourd’hui le micro-ORM le plus connu grâce à ses excellentes performances. Découvrez comment créer une application .NET et utiliser Dapper pour conserver les données.

Les ORM (Object Relational Mappers) facilitent grandement le développement d’applications nécessitant des connexions et de la persistance dans des bases de données. Après tout, les ORM disposent de plusieurs ressources qui, en plus des objets de mappage, peuvent générer la structure complète de la base de données avec seulement quelques commandes.

Cependant, un inconvénient est que les ORM traditionnels ne sont pas très performants. Pour résoudre ce problème, il existe des micro-ORM qui sont axés sur la performance car ils disposent de peu de ressources par rapport aux ORM classiques. L’un des micro-ORM les plus connus est Dapper.

Dans cet article, nous apprendrons plus de détails sur Dapper et comment implémenter un CRUD complet dans une application .NET avec ce Micro-ORM hautes performances.

Qu’est-ce qu’un Micro-ORM ?

Un ORM (encore une fois, c’est Object-Relational Mapper) est utilisé pour créer des objets de code client qui reflètent une base de données relationnelle et fournissent plusieurs fonctionnalités puissantes telles que la mise en cache d’objets et les requêtes.

Un micro-ORM a l’essence d’un ORM mais avec moins de fonctionnalités qu’un ORM complet.

Pimpant est un micro-ORM open source pour .NET développé par Marc Gravel et Sam Saffron en 2011 tout en essayant de résoudre les problèmes de performances de Stack Overflow. Vous pouvez consulter l’histoire de Dapper écrite par Sam lui-même : Comment j’ai appris à ne plus m’inquiéter et à écrire mon propre ORM.

Les résultats de performance obtenus par Dapper l’ont rendu populaire parmi ses concurrents – après tout, il a résolu les problèmes de performances sur l’un des sites les plus consultés au monde : Stack Overflow. Ce succès est dû à la façon dont il a été construit et à la façon dont il supprime la complexité lors de la création de requêtes, ce qui rend extrêmement simple l’exécution, le paramétrage et le mappage d’objets via des requêtes SQL.

Dapper contre EF Core

Entity Framework Core (EF Core) est un ORM complet développé et maintenu par Microsoft. Bien qu’il existe une grande variété d’options disponibles dans EF Core concernant Dapper, cela ne signifie pas qu’il est meilleur que Dapper. EF Core et Dapper présentent tous deux des avantages et des inconvénients. L’important est d’utiliser chacun dans les scénarios où ils sont les plus appropriés.

Il est courant de trouver des cas où Dapper est utilisé conjointement avec EF Core. Comme prévu, cette union nécessite un grand nombre de ressources applicatives, mais l’avantage de cette union de ressources est qu’elle permet la création d’excellentes applications, composées du meilleur de la technologie moderne.

Quand utiliser Dapper

Sans aucun doute, le plus grand avantage de l’utilisation de Dapper est le gain de performances. Dans les scénarios où des performances élevées sont requises, Dapper peut être le meilleur choix. Cependant, il faut considérer que lors de l’utilisation de Dapper, une période de développement plus longue sera nécessaire, car il est nécessaire d’écrire les requêtes SQL qui seront exécutées par Dapper. De plus, il n’a pas, du moins nativement, l’option de migrations automatiques comme dans EF Core, par exemple, nécessitant que les créations/modifications de bases de données et de tables soient effectuées via des scripts SQL.

Persistance des données avec Dapper

Pour démontrer l’utilisation de Dapper dans une application .NET, créons une API minimale qui exécutera les opérations CRUD (créer, lire, mettre à jour et supprimer) dans une base de données MySQL.

Vous pouvez accéder au le code source du projet ici.

Création du projet

Donc, pour générer le projet dans Visual Studio :

  • Créer un nouveau projet
  • Choisissez l’API Web ASP.NET Core
  • Nom (FreelanceProjectControl est ma suggestion)
  • Choisissez .NET 6 LTS
  • Décochez « Utiliser les contrôleurs »
  • Cliquez sur Créer

Dépendances du projet

Le projet aura les dépendances suivantes qui peuvent être ajoutées au fichier .csproj :

<PackageReference Include="MySql.Data" Version="8.0.31" />
<PackageReference Include="Dapper" Version="2.0.123" />

Ou via NugetPackage :

Création des classes de modèle

Commençons par créer les classes de modèle qui contiendront les entités de la base de données. Une classe sera utilisée pour stocker la valeur de la chaîne de connexion, et l’autre pour représenter l’entité « Projet », qui dans le contexte de l’article est un projet indépendant.

Créez donc un nouveau dossier appelé « Modèles » et à l’intérieur, créez les classes ci-dessous :

namespace FreelanceProjectControl.Models;
public class ConnectionString
{
    public string? ProjectConnection { get; set; }
}
namespace FreelanceProjectControl.Models;
public class Project
{
    public string? Id { get; set; }
    public string? Customer { get; set; }  
    public string? Name { get; set; }
    public int WorkedHours { get; set; }
    public decimal FlatRateAmount { get; set; }
    public decimal HourlyRateAmount { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
    public bool Active { get; set; }
}

Création de la classe de référentiel

La classe du référentiel contiendra la connexion à la base de données ainsi que les méthodes chargées d’effectuer le CRUD (Create, Read, Update et Delete).

Créez donc un nouveau dossier appelé « Data » et créez à l’intérieur la classe ci-dessous :

using Dapper;
using FreelanceProjectControl.Models;
using Microsoft.Extensions.Options;
using MySql.Data.MySqlClient;
using System.Data;

namespace FreelanceProjectControl.Data;
public class ProjectRepository
{
    private readonly IDbConnection _dbConnection;

    public ProjectRepository(IOptions<ConnectionString> connectionString)
    {
        _dbConnection = new MySqlConnection(connectionString.Value.ProjectConnection);
    }

    public async Task<List<Project>> GetAll()
    {
        try
        {
            _dbConnection?.Open();

            string query = @"select id, name, customer, workedHours, flatRateAmount, hourlyRateAmount, startDate, endDate, active from project";
            
            var projects = await _dbConnection.QueryAsync<Project>(query);
            return projects.ToList();
        }
        catch (Exception)
        {
            return new List<Project>();
        }
        finally
        {
            _dbConnection?.Close();
        }
    }

    public async Task<Project?> GetById(string id)
    {
        try
        {
            _dbConnection?.Open();

            string query = $@"select id, name, customer, workedHours, flatRateAmount, hourlyRateAmount, startDate, endDate, active from project where id = '{id}'";
            
            var customer = await _dbConnection.QueryAsync<Project>(query, id);
            return customer.FirstOrDefault();
        }
        catch (Exception)
        {
            return null;
        }
        finally
        {
            _dbConnection?.Close();
        }
    }

    public async Task<bool> Create(Project project)
    {
        try
        {
            _dbConnection?.Open();

            string query = @"insert into project(id, name, customer, workedHours, flatRateAmount, hourlyRateAmount, startDate, endDate, active) 
                             values(@Id, @Customer, @Name, @WorkedHours, @FlatRateAmount, @HourlyRateAmount, @StartDate, @EndDate, @Active)";

            await _dbConnection.ExecuteAsync(query, project);
            return true;
        }
        catch (Exception)
        {
            return false;
        }
        finally
        {
            _dbConnection?.Close();
        }
    }

    public async Task<bool> Update(Project project)
    {
        try
        {
            _dbConnection?.Open();

            string selectQuery = $@"select * from project where id = '{project.Id}'";

            var entity = await _dbConnection.QueryAsync<Project>(selectQuery, project.Id);

            if (entity is null)
                return false;

            string updateQuery = @"update project set name = @Name, customer = @Customer, workedHours = @WorkedHours, flatRateAmount = @FlatRateAmount, 
                                   hourlyRateAmount = @HourlyRateAmount, startDate = @StartDate, endDate = @EndDate, active = @Active 
                                   where id = @Id";

            await _dbConnection.ExecuteAsync(updateQuery, project);
            return true;
        }
        catch (Exception)
        {
            return false;
        }
        finally
        {
            _dbConnection?.Close();
        }
    }

    public async Task<bool> Delete(string id)
    {
        try
        {
            _dbConnection?.Open();

            string selectQuery = $@"select * from project where id = '{id}'";

            var entity = await _dbConnection.QueryAsync<Project>(selectQuery, id);

            if (entity is null)
                return false;

            string query = $@"delete from project where id = '{id}'";

            await _dbConnection.ExecuteAsync(query);
            return true;
        }
        catch (Exception)
        {
            return false;
        }
        finally
        {
            _dbConnection?.Close();
        }
    }
}

Notez que dans le code ci-dessus, nous créons une instance d’une connexion MySQL et attribuons sa valeur au _dbConnection variable dans le constructeur de la classe. Cette variable nous permet d’exécuter toutes les méthodes de persistance disponibles dans MySQL. Chaque méthode est composée d’une chaîne dans laquelle le code SQL est intégré et transmis en tant que paramètre aux méthodes d’espace de noms Dapper.

Nous devons également ajouter la configuration de la classe de référentiel pour que l’injection de dépendances se produise, donc dans le fichier Program.cs ajoutez le code suivant :

builder.Services.AddSingleton<ProjectRepository>();

Création de la classe de service

La classe de service contiendra l’appel aux méthodes de référentiel que nous avons créées précédemment. Il aura également une interface pour s’assurer que toutes les méthodes sont mises en œuvre.

Créez donc un nouveau dossier appelé « Services » et à l’intérieur, créez l’interface et la classe ci-dessous :

  • IProjectService (interface)
using FreelanceProjectControl.Models;

namespace FreelanceProjectControl.Services
{
    public interface IProjectService
    {
        Task<List<Project>> GetAllProjects();
        Task<Project> GetProjectById(string id);
        Task<bool> CreateProject(Project project);
        Task<bool> UpdateProject(Project project);
        Task<bool> DeleteProject(string id);
    }
}
using FreelanceProjectControl.Data;
using FreelanceProjectControl.Models;

namespace FreelanceProjectControl.Services;
public class ProjectService : IProjectService
{
    private readonly ProjectRepository _repository;

    public ProjectService(ProjectRepository repository)
    {
        _repository = repository;
    }

    public async Task<List<Project>> GetAllProjects() =>
    await _repository.GetAll();

    public async Task<Project> GetProjectById(string id) =>
        await _repository.GetById(id);

    public async Task<bool> CreateProject(Project project) =>
        await _repository.Create(project);

    public async Task<bool> UpdateProject(Project project) =>
        await _repository.Update(project);

    public async Task<bool> DeleteProject(string id) =>
        await _repository.Delete(id);
}

Pour ajouter l’injection de dépendance de classe de service, dans le fichier Program.cs, ajoutez le code suivant :

builder.Services.AddTransient<IProjectService, ProjectService>();

Configuration de la base de données

La base de données utilisée dans cet exemple sera MySQL, donc pour que l’application fonctionne, il est nécessaire que MySQL soit installé localement. Vous pouvez utiliser une autre base de données, mais vous devrez modifier certains paramètres qui ne sont pas traités dans cet article.

Pour créer la base de données qui sera utilisée dans l’application, vous pouvez utiliser la commande SQL ci-dessous :

start transaction;
create database dbprojects;

Et pour créer la table, utilisez la commande suivante :

use dbprojects;
create table `project` (
  `id` varchar(36) NOT NULL DEFAULT 'uuid()',
  `name` varchar(50) NOT NULL,
  `customer` varchar(250) NOT NULL DEFAULT 'uuid()',
  `workedHours` integer NOT NULL,
  `flatRateAmount` decimal NULL,
  `hourlyRateAmount` decimal NULL,
  `startDate` datetime NOT NULL,
  `endDate` datetime NULL,
  `active` bool NOT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Une autre configuration consiste à créer la chaîne de connexion. Puis dans le fichier appsettings.json ajoutez le code suivant :

  "ConnectionStrings": {
     "ProjectConnection": "host=localhost; port=YOUR MYSQL PORT; database=dbprojects; user=YOUR MYSQL USER; password=YOUR MYSQL PASSWORD;"
   },

Et dans le fichier Program.cs ajoutez le code ci-dessous. Il chargera la valeur de la chaîne de connexion au démarrage de l’application :

builder.Services.Configure<ConnectionString>(builder.Configuration.GetSection("ConnectionStrings"));

Création des points de terminaison de l’API

Enfin, il ne reste plus qu’à créer les endpoints API qui serviront à exécuter les actions CRUD. Ainsi, dans le fichier Program.cs, sous le code app.UseHttpsRedirection(); ajouter le code suivant :

#region Project API

app.MapGet("/Project/Get", async (IProjectService service) =>
{
    var result = await service.GetAllProjects();
    return result.Any() ? Results.Ok(result) : Results.NotFound("No records found");
})
.WithName("GetAllProjects");

app.MapGet("/Project/GetById", async (IProjectService service, string id) =>
{
    var result = await service.GetProjectById(id);
    return result is not null ? Results.Ok(result) : Results.NotFound($"No record found - id: {id}");
})
.WithName("GetProjectById");

app.MapPost("/Project/Create", async (IProjectService service, Project project) =>
{
    bool result = await service.CreateProject(project);
    return result ? Results.Ok() : Results.BadRequest("Error creating new Project Entity");
})
.WithName("CreateProject");

app.MapPut("/Project/Update", async (IProjectService service, Project project) =>
{
    bool result = await service.UpdateProject(project);
    return result ? Results.Ok() : Results.NotFound($"No record found - id: {project.Id}");
})
.WithName("UpdateProject");

app.MapDelete("/Project/Delete", async (IProjectService service, string id) =>
{
    bool result = await service.DeleteProject(id);
    return result ? Results.Ok() : Results.NotFound($"No record found - id: {id}");
})
.WithName("DeleteProject");

#endregion

Notez que dans le code ci-dessus, nous créons les points de terminaison qui appellent la classe de service. Comme nous utilisons le modèle Minimal APIs, tout le code est simple et sans cérémonie.

Tester l’application

Pour tester l’application, il suffit d’exécuter le projet et, si tout est correct, une fenêtre avec l’interface Swagger s’ouvrira dans le navigateur. Le GIF ci-dessous montre la fonction de création et de recherche de données, juste pour confirmer que l’application est fonctionnelle.

Test fanfaron

Conclusion

En effet, Dapper est un excellent outil de persistance des données en .NET et possède des performances exceptionnelles par rapport aux autres micro-ORMS. Dans cet article, nous avons vu comment créer une application avec Dapper en utilisant MySQL comme base de données, car le but de l’article était de démontrer l’utilisation de Dapper certains détails n’étaient pas couverts comme l’ajout de logs et de validations de champs. N’hésitez pas à créer ces ressources et à améliorer le projet.




Source link