Fermer

février 17, 2023

Premiers pas avec les génériques dans .NET

Premiers pas avec les génériques dans .NET


Les types génériques font partie des fondamentaux du langage C# et peuvent aider le développeur à gagner du temps en réutilisant le code. En savoir plus sur les types génériques et sur la création d’une API dans .NET 7 à l’aide du modèle de référentiel générique.

Les types génériques sont une fonctionnalité précieuse disponible en C# depuis les premières versions de .NET. Bien qu’ils fassent partie des principes fondamentaux du langage, de nombreux développeurs évitent de les utiliser, peut-être parce qu’ils ne savent pas exactement comment les utiliser ou peut-être même ne les connaissent-ils pas.

Dans cet article, vous en apprendrez un peu plus sur les génériques dans le contexte de .NET et comment les utiliser dans un scénario réel.

Que sont les génériques dans .NET ?

Les génériques dans le contexte .NET sont des classes, des structures, des interfaces et des méthodes avec des espaces réservés pour un ou plusieurs des types qu’ils stockent ou utilisent.

Imaginez que vous ayez besoin de créer une application qui effectuera l’enregistrement de nouveaux clients. Cependant, lors de l’enregistrement, il est nécessaire de sauvegarder ces informations dans d’autres bases de données comme une table d’historique – une autre table d’un fournisseur externe – et nous avons donc au moins trois insertions à faire dans chaque table. Pour ne pas répéter la même méthode et dupliquer le code, on peut créer une méthode qui accepte une classe générique, et ainsi les trois insertions pourront se faire avec la même méthode.

Parmi les avantages des génériques figurent la réduction du code répété, les gains de performances et la sécurité des types.

Avantages et inconvénients des génériques

Avantages:

  • Type de sécurité : Le type de données est vérifié pendant l’exécution, éliminant ainsi le besoin de créer du code supplémentaire à vérifier.
  • Moins d’écriture de code : Avec les types génériques, il est possible de réutiliser les classes et les méthodes, ce qui réduit le code à écrire.
  • Meilleure performance: Les types génériques fonctionnent généralement mieux lors du stockage et de la manipulation de types valeur.
  • Simplification du code généré dynamiquement : Dispense la génération de type lorsqu’il est généré dynamiquement. Il est donc possible d’utiliser des méthodes dynamiques légères au lieu de générer des assemblages entiers.

Les inconvénients:

  • Il n’y a aucun inconvénient à utiliser des génériques, mais il existe certaines limitations telles que les énumérations qui ne peuvent pas avoir de paramètres de type générique et le fait que .NET ne prend pas en charge les types génériques liés au contexte.

Quand utiliser les génériques

Les classes et méthodes génériques combinent la réutilisation, la sécurité des types et l’efficacité d’une manière que les alternatives non génériques ne peuvent pas. Donc, chaque fois que vous souhaitez utiliser l’un de ces avantages, envisagez d’utiliser des génériques.

Le modèle de référentiel générique

Dans les petits projets avec peu d’entités, il est courant de trouver un référentiel pour chacune des entités, d’effectuer des opérations CRUD pour chacune d’elles et de répéter le code plusieurs fois.

Cependant, imaginez un projet où il est nécessaire de mettre en place le CRUD de plusieurs entités. Outre la répétition du code, la maintenance de ce projet serait également coûteuse – après tout, il y aurait plusieurs classes à changer.

Avec le modèle de référentiel générique, nous éliminons ces problèmes en créant un référentiel unique qui peut être partagé avec autant d’entités que nécessaire.

Exemple pratique

Pour démontrer l’utilisation des génériques dans un exemple réel, nous allons créer une application .NET dans un scénario où il sera nécessaire d’implémenter un CRUD pour deux entités, une pour l’entité Produit et une autre pour l’entité Vendeur. Ainsi nous verrons en pratique comment il est possible de réutiliser du code grâce aux génériques.

Pour développer l’application, nous utiliserons .NET 7. Vous pouvez accéder aux code source ici.

Dépendances du projet

Vous devez ajouter les dépendances du projet, soit directement dans le code du projet « ProductCatalog.csproj »:

 <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>

ou en téléchargeant les packages NuGet :

Créer l’application

Commençons par créer la solution et le projet. Ainsi, dans Visual Studio :

  • Créer un nouveau projet
  • Choisissez ASP. API Web NET Core
  • Nommez-le (« ProductCatalog » est ma suggestion)
  • Choisissez .NET 7.0 (prise en charge à terme standard)
  • Décochez « Utiliser les contrôleurs »
  • Cliquez sur Créer

Créer les classes d’entités

Créons deux classes de modèles qui représenteront les entités Product et Seller. Alors, créez un nouveau dossier nommé « Models » et, à l’intérieur de celui-ci, créez les classes ci-dessous :

using System.ComponentModel.DataAnnotations;

namespace ProductCatalog.Models;
public class Product
{
    [Key]
    public Guid Id { get; set; }
    public string? Name { get; set; }
    public string? Description { get; set; }
    public string? Type { get; set; }
    public string? DisplayName { get; set; }
    public string? Brand { get; set; }
    public string? Category { get; set; }
    public bool Active { get; set; }
}
using System.ComponentModel.DataAnnotations;

namespace ProductCatalog.Models;
public class Seller
{
    [Key]
    public Guid Id { get; set; }
    public string? Name { get; set; }
}

Créer la classe de contexte

La classe de contexte sera utilisée pour ajouter les paramètres de la base de données qui dans ce cas seront SQLite. Créez donc un nouveau dossier appelé « Data » et, à l’intérieur de celui-ci, créez la classe ci-dessous :

using Microsoft.EntityFrameworkCore;
using ProductCatalog.Models;

namespace ProductCatalog.Data;
public class ProductCatalogContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder options) =>
       options.UseSqlite("DataSource = productCatalog; Cache=Shared");

    public DbSet<Product> Products { get; set; }
    public DbSet<Seller> Sellers { get; set; }
}

Créer le référentiel générique

Le référentiel contiendra les méthodes chargées d’exécuter les fonctions CRUD. Comme nous utiliserons le modèle de référentiel générique, nous n’aurons qu’une seule classe et une seule interface qui seront partagées par les entités Product et Seller.

Donc, dans le dossier « Data », créez un nouveau dossier appelé « Repository ». À l’intérieur, créez un nouveau dossier appelé « Interfaces », et à l’intérieur, créez l’interface ci-dessous :

namespace ProductCatalog.Data.Repository.Interfaces;
public interface IGenericRepository<T> where T : class
{
    IEnumerable<T> GetAll();
    Task<T> GetById(Guid id);
    Task Create(T entity);
    void Update(T entity);
    Task Delete(Guid id);
    Task Save();
}

💡 Notez que les méthodes attendent comme paramètre un type générique représenté en C# avec l’expression «  » et dans le cas des méthodes d’obtention, elles renvoient également des types génériques.

L’étape suivante consiste à créer la classe qui implémentera les méthodes d’interface. Ensuite, dans le dossier « Repository », créez la classe ci-dessous :

using Microsoft.EntityFrameworkCore;
using ProductCatalog.Data.Repository.Interfaces;

namespace ProductCatalog.Data.Repository;
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
    private readonly ProductCatalogContext _context;
    private readonly DbSet<T> _entities;

    public GenericRepository(ProductCatalogContext context)
    {
        _context = context;
        _entities = context.Set<T>();
    }

    public IEnumerable<T> GetAll() =>
        _entities.ToList();

    public async Task<T> GetById(Guid id) =>
         await _entities.FindAsync(id);

    public async Task Create(T entity) =>
        await _context.AddAsync(entity);

    public void Update(T entity)
    {
        _entities.Attach(entity);
        _context.Entry(entity).State = EntityState.Modified;
    }

    public async Task Delete(Guid id)
    {
        T existing = await _entities.FindAsync(id);
        _entities.Remove(existing);
    }

    public async Task Save() =>
      await _context.SaveChangesAsync();
}

💡 Notez que dans la classe ci-dessus, nous avons la variable globale « _entities » qui reçoit la valeur des collections de données, en fonction de l’entité passée en paramètre.

Nous avons également les méthodes qui effectuent des opérations CRUD via les méthodes d’extension EF Core telles que « FindAsync », « AddAsync », etc.

Ajouter les injections de dépendances

Pour ajouter des injections de dépendances, dans le fichier Program.cs, ajoutez les lignes de code suivantes juste en dessous de l’extrait de code « builder.Services.AddSwaggerGen() » :

builder.Services.AddScoped<ProductCatalogContext>();
builder.Services.AddTransient<IGenericRepository<Product>, GenericRepository<Product>>();
builder.Services.AddTransient<IGenericRepository<Seller>, GenericRepository<Seller>>();

Créer les points de terminaison de l’API

Vous trouverez ci-dessous le code de tous les points de terminaison de l’API, à la fois Product et Seller. Ensuite, toujours dans le fichier Program.cs, ajoutez le code ci-dessous avant l’extrait « app.Run() »:

#region Product API

app.MapGet("productCatalog/product/getAll", (IGenericRepository<Product> service) =>
{
    var products = service.GetAll();
    return Results.Ok(products);
})
.WithName("GetProductCatalog")
.WithOpenApi();

app.MapGet("productCatalog/product/getById", (IGenericRepository<Product> service, Guid id) =>
{
    var products = service.GetById(id);
    return Results.Ok(products);
})
.WithName("GetProductCatalogById")
.WithOpenApi();

app.MapPost("productCatalog/product/create", (IGenericRepository<Product> service, Product product) =>
{
    service.Create(product);
    service.Save();
    return Results.Ok();
})
.WithName("CreateProductCatalog")
.WithOpenApi();

app.MapPut("productCatalog/product/update", (IGenericRepository<Product> service, Product product) =>
{
    service.Update(product);
    service.Save();
    return Results.Ok();
})
.WithName("UpdateProductCatalog")
.WithOpenApi();

app.MapDelete("productCatalog/product/delete", (IGenericRepository<Product> service, Guid id) =>
{
    service.Delete(id);
    service.Save();
    return Results.Ok();
})
.WithName("DeleteProductCatalog")
.WithOpenApi();

#endregion

#region Seller API

app.MapGet("productCatalog/seller/getAll", (IGenericRepository<Seller> service) =>
{
    var products = service.GetAll();
    return Results.Ok(products);
})
.WithName("GetSeller")
.WithOpenApi();

app.MapGet("productCatalog/seller/getById", (IGenericRepository<Seller> service, Guid id) =>
{
    var products = service.GetById(id);
    return Results.Ok(products);
})
.WithName("GetSellerById")
.WithOpenApi();

app.MapPost("productCatalog/seller/create", (IGenericRepository<Seller> service, Seller seller) =>
{
    service.Create(seller);
    service.Save();
    return Results.Ok();
})
.WithName("CreateSeller")
.WithOpenApi();

app.MapPut("productCatalog/seller/update", (IGenericRepository<Seller> service, Seller seller) =>
{
    service.Update(seller);
    service.Save();
    return Results.Ok();
})
.WithName("UpdateSeller")
.WithOpenApi();

app.MapDelete("productCatalog/seller/delete", (IGenericRepository<Seller> service, Guid id) =>
{
    service.Delete(id);
    service.Save();
    return Results.Ok();
})
.WithName("DeleteSeller")
.WithOpenApi();

#endregion

💡 Notez que dans chaque point de terminaison lorsque l’interface « IGenericRepository » est déclarée, nous passons en argument l’entité correspondant au périmètre de l’API. Autrement dit, dans l’API produit, nous déclarons IGenericRepository<Product>et dans l’API du vendeur, IGenericRepository<Seller>. De cette façon, nous pouvons utiliser la même interface pour toute entité que notre code peut avoir, car il attend un type générique, quel qu’il soit.

Générer les migrations

Pour générer les migrations de base de données, nous devons exécuter les commandes EF Core. Pour cela, nous devons installer le CLI .NET outils. Sinon, les commandes entraîneront une erreur.

La première commande créera une migration appelée InitialModel et la seconde demandera à EF de créer une base de données et un schéma à partir de la migration.

Plus d’informations sur les migrations sont disponibles dans Documentation officielle de Microsoft.

Vous pouvez exécuter les commandes ci-dessous dans un terminal racine du projet.

dotnet ef migrations add InitialModel
dotnet ef database update

Vous pouvez également exécuter les commandes suivantes à partir de la console du gestionnaire de packages dans Visual Studio :

Add-Migration InitialModel
Update-Database

Testez l’application

Pour tester l’application, exécutez simplement le projet et effectuez les opérations CRUD. Le GIF ci-dessous illustre l’utilisation de l’interface Swagger pour exécuter les fonctions de création dans les deux API.

Essai d'application

Conclusion

À travers l’exemple enseigné dans l’article, nous pouvons voir en pratique comment l’utilisation de génériques peut être une excellente option lorsque l’objectif est de gagner du temps grâce à la réutilisation du code.

Pensez donc toujours à utiliser des génériques lorsque l’occasion se présente. Ce faisant, vous acquerrez tous les avantages des types génériques en plus de démontrer que vous vous souciez de créer du code réutilisable.

ASP.NET Core REPL : partagez des extraits de code, modifiez des démos sur place avec ASP.NET Core REPL




Source link