ASP.NET Core Blazor avec Entity Framework Core (EF Core)

Remarque

Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 8 de cet article.

Important

Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.

Pour la version actuelle, consultez la version .NET 8 de cet article.

Cet article explique comment utiliser Entity Framework Core (EF Core) dans les applications Blazor côté serveur.

Blazor côté serveur est un framework d’application avec état. L’application maintient une connexion continue au serveur, et l’état de l’utilisateur est conservé dans la mémoire du serveur dans un circuit. Des données conservées dans des instances de service d’injection de dépendances qui sont limitées au circuit constituent un exemple d’état utilisateur. Le modèle d’application unique fourni par Blazor nécessite une approche spéciale pour utiliser Entity Framework Core.

Remarque

Cet article aborde EF Core dans les applications Blazor côté serveur. Les applications Blazor WebAssembly s’exécutent dans un bac à sable (sandbox) WebAssembly qui empêche la plupart des connexions de base de données directes. L’exécution d’EF Core dans Blazor WebAssembly n’entre pas dans le cadre de cet article.

Cette aide s’applique aux composants qui adoptent le rendu côté serveur interactif (SSR interactif) dans une application web Blazor.

Cette aide s’applique au projet Server d’une solution Blazor WebAssembly hébergée ou d’une application Blazor Server.

Exemple d’application

L’exemple d’application a été généré comme référence pour les applications Blazor côté serveur qui utilisent EF Core. L’exemple d’application inclut une grille avec des opérations de tri et de filtrage, de suppression, d’ajout et de mise à jour. L’exemple illustre l’utilisation d’EF Core pour gérer l’accès concurrentiel optimiste.

Voir ou télécharger un exemple de code (Comment télécharger) : sélectionnez le dossier qui correspond à la version de .NET que vous adoptez. Dans le dossier de la version, accédez à l’exemple nommé BlazorWebAppEFCore.

Voir ou télécharger un exemple de code (Comment télécharger) : sélectionnez le dossier qui correspond à la version de .NET que vous adoptez. Dans le dossier de la version, accédez à l’exemple nommé BlazorServerEFCoreSample.

L’exemple utilise une base de données SQLite locale afin qu’elle puisse être utilisée sur n’importe quelle plateforme. L’exemple configure également la journalisation de la base de données pour afficher les requêtes SQL qui sont générées. Cette fonctionnalité est configurée dans appsettings.Development.json :

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}
{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}
{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}
{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}
{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}

Les composants de grille, d’ajout et d’affichage utilisent le modèle « contexte par opération », où un contexte est créé pour chaque opération. Le composant d’édition utilise le modèle « contexte par composant », où un contexte est créé pour chaque composant.

Remarque

Certains des exemples de code de cette rubrique nécessitent des espaces de noms et des services qui ne sont pas affichés. Pour inspecter le code entièrement opérationnel, ce qui inclut les directives @using et @inject requises pour des exemples Razor, consultez l’exemple d’application.

Accès aux bases de données

EF Core s’appuie sur un DbContext comme moyen de configurer l’accès à la base de données et d’agir comme une unité de travail. EF Core fournit l’extension AddDbContext pour les applications ASP.NET Core qui inscrit le contexte en tant que service délimité par défaut. Dans les applications Blazor côté serveur, les inscriptions de service délimité peuvent être problématiques, car l’instance est partagée entre les composants du circuit de l’utilisateur. DbContext n’est pas thread-safe et n’est pas conçu pour une utilisation simultanée. Les durées de vie existantes sont inappropriées pour les raisons suivantes :

  • Singleton partage l’état entre tous les utilisateurs de l’application et conduit à une utilisation simultanée inappropriée.
  • Délimité (valeur par défaut) pose un problème similaire entre les composants pour le même utilisateur.
  • Temporaire entraîne la création d’une instance par requête. Toutefois, comme les composants peuvent être de longue durée, il en résulte un contexte d’une plus longue durée que prévu.

Les recommandations suivantes sont conçues pour fournir une approche cohérente de l’utilisation d’EF Core dans les applications Blazor côté serveur.

  • Par défaut, envisagez d’utiliser un contexte par opération. Le contexte est conçu pour une instanciation rapide et à faible charge :

    using var context = new MyContext();
    
    return await context.MyEntities.ToListAsync();
    
  • Utilisez un indicateur pour empêcher plusieurs opérations simultanées :

    if (Loading)
    {
        return;
    }
    
    try
    {
        Loading = true;
    
        ...
    }
    finally
    {
        Loading = false;
    }
    

    Placez les opérations après la ligne Loading = true; dans le bloc try.

    La logique de chargement ne nécessite pas de verrouillage des enregistrements de base de données, car la cohérence de thread n’est pas un problème. La logique de chargement est utilisée pour désactiver les contrôles d’interface utilisateur afin que les utilisateurs ne sélectionnent pas par inadvertance des boutons ou ne mettent pas à jour les champs pendant la récupération de données.

  • S’il est possible que plusieurs threads accèdent au même bloc de code, injectez une fabrique et créez une nouvelle instance par opération. Autrement, l’injection et l’utilisation du contexte sont généralement suffisantes.

  • Pour les opérations de plus longue durée qui tirent parti du suivi des modifications ou du contrôle d’accès concurrentiel, d’EF Core, limitez le contexte à la durée de vie du composant.

Nouvelles instances DbContext

Le moyen le plus rapide de créer une instance DbContext consiste à utiliser new. Toutefois, certains scénarios nécessitent la résolution de dépendances supplémentaires :

L’approche recommandée pour créer un DbContext avec des dépendances consiste à utiliser une fabrique. EF Core 5.0 ou version ultérieure fournit une fabrique intégrée pour la création de contextes.

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace BlazorServerDbContextExample.Data
{
    public class DbContextFactory<TContext> 
        : IDbContextFactory<TContext> where TContext : DbContext
    {
        private readonly IServiceProvider provider;

        public DbContextFactory(IServiceProvider provider)
        {
            this.provider = provider ?? throw new ArgumentNullException(
                $"{nameof(provider)}: You must configure an instance of " +
                "IServiceProvider");
        }

        public TContext CreateDbContext() => 
            ActivatorUtilities.CreateInstance<TContext>(provider);
    }
}

Dans la fabrique précédente :

L’exemple suivant configure SQLite et active la journalisation des données. Le code utilise une méthode d’extension (AddDbContextFactory) afin de configurer la fabrique de base de données pour l’injection de dépendances et de fournir des options par défaut :

builder.Services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
builder.Services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
builder.Services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));

La fabrique est injectée dans les composants et est utilisée pour créer des instances DbContext.

Sur la page d'accueil de l’exemple d’application, IDbContextFactory<ContactContext> est injecté dans le composant :

@inject IDbContextFactory<ContactContext> DbFactory

Un DbContext est créé à l’aide de la fabrique (DbFactory) pour supprimer un contact dans la méthode DeleteContactAsync :

private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();
    Filters.Loading = true;

    if (Wrapper is not null && context.Contacts is not null)
    {
        var contact = await context.Contacts
            .FirstAsync(c => c.Id == Wrapper.DeleteRequestId);

        if (contact is not null)
        {
            context.Contacts?.Remove(contact);
            await context.SaveChangesAsync();
        }
    }

    Filters.Loading = false;

    await ReloadAsync();
}
private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();
    Filters.Loading = true;

    if (Wrapper is not null && context.Contacts is not null)
    {
        var contact = await context.Contacts
            .FirstAsync(c => c.Id == Wrapper.DeleteRequestId);

        if (contact is not null)
        {
            context.Contacts?.Remove(contact);
            await context.SaveChangesAsync();
        }
    }

    Filters.Loading = false;

    await ReloadAsync();
}
private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();
    Filters.Loading = true;

    if (Wrapper is not null && context.Contacts is not null)
    {
        var contact = await context.Contacts
            .FirstAsync(c => c.Id == Wrapper.DeleteRequestId);

        if (contact is not null)
        {
            context.Contacts?.Remove(contact);
            await context.SaveChangesAsync();
        }
    }

    Filters.Loading = false;

    await ReloadAsync();
}
private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();

    Filters.Loading = true;

    var contact = await context.Contacts.FirstAsync(
        c => c.Id == Wrapper.DeleteRequestId);

    if (contact != null)
    {
        context.Contacts.Remove(contact);
        await context.SaveChangesAsync();
    }

    Filters.Loading = false;

    await ReloadAsync();
}
private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();

    Filters.Loading = true;

    var contact = await context.Contacts.FirstAsync(
        c => c.Id == Wrapper.DeleteRequestId);

    if (contact != null)
    {
        context.Contacts.Remove(contact);
        await context.SaveChangesAsync();
    }

    Filters.Loading = false;

    await ReloadAsync();
}

Remarque

Filters est un IContactFilters injecté et Wrapper est une référence de composant au composant GridWrapper. Consultez le composant Home (Components/Pages/Home.razor) dans l’exemple d’application.

Remarque

Filters est un IContactFilters injecté et Wrapper est une référence de composant au composant GridWrapper. Consultez le composant Index (Pages/Index.razor) dans l’exemple d’application.

Délimiter à la durée de vie du composant

Vous pouvez souhaiter créer un DbContext qui existe pendant toute la durée de vie d’un composant. Cela vous permet de l’utiliser comme unité de travail et de tirer parti des fonctionnalités intégrées, telles que le suivi des modifications et la résolution de l’accès concurrentiel.

Vous pouvez utiliser la fabrique pour créer un contexte et le suivre pendant la durée de vie du composant. Commencez par implémenter IDisposable et injectez la fabrique comme indiqué dans le composant EditContact (Components/Pages/EditContact.razor) :

Vous pouvez utiliser la fabrique pour créer un contexte et le suivre pendant la durée de vie du composant. Commencez par implémenter IDisposable et injectez la fabrique comme indiqué dans le composant EditContact (Pages/EditContact.razor) :

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

L’exemple d’application garantit que le contexte est supprimé lorsque le composant est supprimé :

public void Dispose()
{
    Context?.Dispose();
}
public void Dispose()
{
    Context?.Dispose();
}
public void Dispose()
{
    Context?.Dispose();
}
public void Dispose()
{
    Context?.Dispose();
}
public void Dispose()
{
    Context?.Dispose();
}

Enfin, OnInitializedAsync est remplacé pour créer un nouveau contexte. Dans l’exemple d’application, OnInitializedAsync charge le contact dans la même méthode :

protected override async Task OnInitializedAsync()
{
    Busy = true;

    try
    {
        Context = DbFactory.CreateDbContext();

        if (Context is not null && Context.Contacts is not null)
        {
            var contact = await Context.Contacts.SingleOrDefaultAsync(c => c.Id == ContactId);

            if (contact is not null)
            {
                Contact = contact;
            }
        }
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}
protected override async Task OnInitializedAsync()
{
    Busy = true;

    try
    {
        Context = DbFactory.CreateDbContext();

        if (Context is not null && Context.Contacts is not null)
        {
            var contact = await Context.Contacts.SingleOrDefaultAsync(c => c.Id == ContactId);

            if (contact is not null)
            {
                Contact = contact;
            }
        }
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}
protected override async Task OnInitializedAsync()
{
    Busy = true;

    try
    {
        Context = DbFactory.CreateDbContext();

        if (Context is not null && Context.Contacts is not null)
        {
            var contact = await Context.Contacts.SingleOrDefaultAsync(c => c.Id == ContactId);

            if (contact is not null)
            {
                Contact = contact;
            }
        }
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}
protected override async Task OnInitializedAsync()
{
    Busy = true;

    try
    {
        Context = DbFactory.CreateDbContext();
        Contact = await Context.Contacts
            .SingleOrDefaultAsync(c => c.Id == ContactId);
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}
protected override async Task OnInitializedAsync()
{
    Busy = true;

    try
    {
        Context = DbFactory.CreateDbContext();
        Contact = await Context.Contacts
            .SingleOrDefaultAsync(c => c.Id == ContactId);
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}

Dans l'exemple précédent :

  • Lorsque Busy est défini sur true, les opérations asynchrones peuvent commencer. Lorsque la valeur false est réaffectée à Busy, les opérations asynchrones doivent être terminées.
  • Placez une logique de gestion des erreurs supplémentaire dans un bloc catch.

Activer la journalisation des données sensibles

EnableSensitiveDataLogging inclut les données d’application dans les messages d’exception et la journalisation d’infrastructure. Les données journalisées peuvent inclure les valeurs attribuées aux propriétés des instances d’entité et les valeurs de paramètres pour les commandes envoyées à la base de données. La journalisation des données avec EnableSensitiveDataLogging présente un risque de sécurité, car elle peut exposer des mots de passe et d’autres Informations d’identification personnelle (PII) lors de la journalisation d’instructions SQL exécutées sur la base de données.

Nous vous recommandons d’activer EnableSensitiveDataLogging uniquement pour le développement et le test :

#if DEBUG
    services.AddDbContextFactory<ContactContext>(opt =>
        opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db")
        .EnableSensitiveDataLogging());
#else
    services.AddDbContextFactory<ContactContext>(opt =>
        opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
#endif

Ressources supplémentaires