Partager via


Comment utiliser le Kit de développement logiciel (SDK) du serveur principal ASP.NET Core

Cet article vous montre que vous devez configurer et utiliser le Kit de développement logiciel (SDK) du serveur principal ASP.NET Core pour produire un serveur de synchronisation de données.

Plateformes prises en charge

Le serveur principal ASP.NET Core prend en charge ASP.NET 6.0 ou version ultérieure.

Les serveurs de base de données doivent respecter les critères suivants : un DateTime champ de Timestamp type stocké avec précision en millisecondes. Les implémentations de référentiel sont fournies pour Entity Framework Core et LiteDb.

Pour obtenir une prise en charge spécifique de la base de données, consultez les sections suivantes :

Créer un serveur de synchronisation de données

Un serveur de synchronisation de données utilise les mécanismes standard ASP.NET Core pour créer le serveur. Il se compose de trois étapes :

  1. Créez un projet serveur ASP.NET 6.0 (ou version ultérieure).
  2. Ajouter Entity Framework Core
  3. Ajouter des services de synchronisation de données

Pour plus d’informations sur la création d’un service ASP.NET Core avec Entity Framework Core, consultez le tutoriel.

Pour activer les services de synchronisation des données, vous devez ajouter les bibliothèques NuGet suivantes :

Modifiez le fichier Program.cs. Ajoutez la ligne suivante sous toutes les autres définitions de service :

builder.Services.AddDatasyncControllers();

Vous pouvez également utiliser le modèle ASP.NET Core datasync-server :

# This only needs to be done once
dotnet new -i Microsoft.AspNetCore.Datasync.Template.CSharp
mkdir My.Datasync.Server
cd My.Datasync.Server
dotnet new datasync-server

Le modèle inclut un exemple de modèle et de contrôleur.

Créer un contrôleur de table pour une table SQL

Le référentiel par défaut utilise Entity Framework Core. La création d’un contrôleur de table est un processus en trois étapes :

  1. Créez une classe de modèle pour le modèle de données.
  2. Ajoutez la classe de modèle à votre DbContext application.
  3. Créez une TableController<T> classe pour exposer votre modèle.

Créer une classe de modèle

Toutes les classes de modèle doivent implémenter ITableData. Chaque type de référentiel a une classe abstraite qui implémente ITableData. Le référentiel Entity Framework Core utilise EntityTableData:

public class TodoItem : EntityTableData
{
    /// <summary>
    /// Text of the Todo Item
    /// </summary>
    public string Text { get; set; }

    /// <summary>
    /// Is the item complete?
    /// </summary>
    public bool Complete { get; set; }
}

L’interface ITableData fournit l’ID de l’enregistrement, ainsi que des propriétés supplémentaires pour la gestion des services de synchronisation des données :

  • UpdatedAt (DateTimeOffset?) indique la date à laquelle l’enregistrement a été mis à jour pour la dernière fois.
  • Version (byte[]) fournit une valeur opaque qui change sur chaque écriture.
  • Deleted (bool) a la valeur true si l’enregistrement est marqué pour suppression, mais pas encore vidé.

La bibliothèque de synchronisation des données gère ces propriétés. Ne modifiez pas ces propriétés dans votre propre code.

Mettre à jour le DbContext

Chaque modèle de la base de données doit être inscrit dans le DbContext. Par exemple :

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }

    public DbSet<TodoItem> TodoItems { get; set; }
}

Créer un contrôleur de table

Un contrôleur de table est un contrôleur de table spécialisé ApiController. Voici un contrôleur de table minimal :

[Route("tables/[controller]")]
public class TodoItemController : TableController<TodoItem>
{
    public TodoItemController(AppDbContext context) : base()
    {
        Repository = new EntityTableRepository<TodoItem>(context);
    }
}

Remarque

  • Le contrôleur doit avoir un itinéraire. Par convention, les tables sont exposées sur un sous-chemin de /tables, mais elles peuvent être placées n’importe où. Si vous utilisez des bibliothèques clientes antérieures à la version 5.0.0, la table doit être un sous-chemin d’accès /tables.
  • Le contrôleur doit hériter, TableController<T><T> est une implémentation de l’implémentation ITableData pour votre type de référentiel.
  • Affectez un référentiel basé sur le même type que votre modèle.

Implémentation d’un référentiel en mémoire

Vous pouvez également utiliser un référentiel en mémoire sans stockage persistant. Ajoutez un service singleton pour le référentiel dans votre Program.cs:

IEnumerable<Model> seedData = GenerateSeedData();
builder.Services.AddSingleton<IRepository<Model>>(new InMemoryRepository<Model>(seedData));

Configurez votre contrôleur de table comme suit :

[Route("tables/[controller]")]
public class ModelController : TableController<Model>
{
    public MovieController(IRepository<Model> repository) : base(repository)
    {
    }
}

Configurer les options du contrôleur de table

Vous pouvez configurer certains aspects du contrôleur à l’aide TableControllerOptionsde :

[Route("tables/[controller]")]
public class MoodelController : TableController<Model>
{
    public ModelController(IRepository<Model> repository) : base(repository)
    {
        Options = new TableControllerOptions { PageSize = 25 };
    }
}

Les options que vous pouvez définir sont les suivantes :

  • PageSize (intpar défaut : 100) correspond au nombre maximal d’éléments retournés par une opération de requête dans une seule page.
  • MaxTop (intpar défaut : 512000) est le nombre maximal d’éléments retournés dans une opération de requête sans pagination.
  • EnableSoftDelete (boolpar défaut : false) active la suppression réversible, qui marque les éléments comme supprimés au lieu de les supprimer de la base de données. La suppression réversible permet aux clients de mettre à jour leur cache hors connexion, mais nécessite que les éléments supprimés soient vidés de la base de données séparément.
  • UnauthorizedStatusCode (intpar défaut : 401 Non autorisé) est le code d’état retourné lorsque l’utilisateur n’est pas autorisé à effectuer une action.

Configurer les autorisations d’accès

Par défaut, un utilisateur peut effectuer tout ce qu’il souhaite pour les entités d’une table : créer, lire, mettre à jour et supprimer n’importe quel enregistrement. Pour un contrôle plus précis sur l’autorisation, créez une classe qui implémente IAccessControlProvider. Les IAccessControlProvider trois méthodes permettent d’implémenter l’autorisation :

  • GetDataView() retourne une expression lambda qui limite ce que l’utilisateur connecté peut voir.
  • IsAuthorizedAsync() détermine si l’utilisateur connecté peut effectuer l’action sur l’entité spécifique demandée.
  • PreCommitHookAsync() ajuste n’importe quelle entité immédiatement avant d’être écrite dans le référentiel.

Entre les trois méthodes, vous pouvez gérer efficacement la plupart des cas de contrôle d’accès. Si vous avez besoin d’accéder à l’objet HttpContext, configurez un HttpContextAccessor.

Par exemple, l’exemple suivant implémente une table personnelle, où un utilisateur ne peut voir que ses propres enregistrements.

public class PrivateAccessControlProvider<T>: IAccessControlProvider<T>
    where T : ITableData
    where T : IUserId
{
    private readonly IHttpContextAccessor _accessor;

    public PrivateAccessControlProvider(IHttpContextAccessor accessor)
    {
        _accessor = accessor;
    }

    private string UserId { get => _accessor.HttpContext.User?.Identity?.Name; }

    public Expression<Func<T,bool>> GetDataView()
    {
      return (UserId == null)
        ? _ => false
        : model => model.UserId == UserId;
    }

    public Task<bool> IsAuthorizedAsync(TableOperation op, T entity, CancellationToken token = default)
    {
        if (op == TableOperation.Create || op == TableOperation.Query)
        {
            return Task.FromResult(true);
        }
        else
        {
            return Task.FromResult(entity?.UserId != null && entity?.UserId == UserId);
        }
    }

    public virtual Task PreCommitHookAsync(TableOperation operation, T entity, CancellationToken token = default)
    {
        entity.UserId == UserId;
        return Task.CompletedTask;
    }
}

Les méthodes sont asynchrones si vous devez effectuer une recherche de base de données supplémentaire pour obtenir la réponse correcte. Vous pouvez implémenter l’interface IAccessControlProvider<T> sur le contrôleur, mais vous devez toujours passer l’accès IHttpContextAccessorHttpContext de manière sécurisée au thread.

Pour utiliser ce fournisseur de contrôle d’accès, mettez à jour vos TableController éléments comme suit :

[Authorize]
[Route("tables/[controller]")]
public class ModelController : TableController<Model>
{
    public ModelsController(AppDbContext context, IHttpContextAccessor accessor) : base()
    {
        AccessControlProvider = new PrivateAccessControlProvider<Model>(accessor);
        Repository = new EntityTableRepository<Model>(context);
    }
}

Si vous souhaitez autoriser l’accès non authentifié et authentifié à une table, décorez-le au [AllowAnonymous] lieu de [Authorize].

Configuration de la journalisation

La journalisation est gérée par le biais du mécanisme de journalisation normal pour ASP.NET Core. Affectez l’objet ILogger à la Logger propriété :

[Authorize]
[Route("tables/[controller]")]
public class ModelController : TableController<Model>
{
    public ModelController(AppDbContext context, Ilogger<ModelController> logger) : base()
    {
        Repository = new EntityTableRepository<Model>(context);
        Logger = logger;
    }
}

Surveiller les modifications apportées au référentiel

Lorsque le référentiel est modifié, vous pouvez déclencher des flux de travail, consigner la réponse au client ou effectuer d’autres tâches dans l’une des deux méthodes suivantes :

Option 1 : Implémenter un PostCommitHookAsync

L’interface IAccessControlProvider<T> fournit une PostCommitHookAsync() méthode. La méthode Th PostCommitHookAsync() est appelée une fois les données écrites dans le référentiel, mais avant de renvoyer les données au client. Vous devez veiller à ce que les données retournées au client ne soient pas modifiées dans cette méthode.

public class MyAccessControlProvider<T> : AccessControlProvider<T> where T : ITableData
{
    public override async Task PostCommitHookAsync(TableOperation op, T entity, CancellationToken cancellationToken = default)
    {
        // Do any work you need to here.
        // Make sure you await any asynchronous operations.
    }
}

Utilisez cette option si vous exécutez des tâches asynchrones dans le cadre du hook.

Option 2 : Utiliser le gestionnaire d’événements RepositoryUpdated

La TableController<T> classe de base contient un gestionnaire d’événements appelé en même temps que la PostCommitHookAsync() méthode.

[Authorize]
[Route(tables/[controller])]
public class ModelController : TableController<Model>
{
    public ModelController(AppDbContext context) : base()
    {
        Repository = new EntityTableRepository<Model>(context);
        RepositoryUpdated += OnRepositoryUpdated;
    }

    internal void OnRepositoryUpdated(object sender, RepositoryUpdatedEventArgs e) 
    {
        // The RepositoryUpdatedEventArgs contains Operation, Entity, EntityName
    }
}

Activer l’identité Azure App Service

Le serveur de synchronisation de données ASP.NET Core prend en charge ASP.NET Identité principale, ou tout autre schéma d’authentification et d’autorisation que vous souhaitez prendre en charge. Pour faciliter les mises à niveau à partir des versions antérieures d’Azure Mobile Apps, nous fournissons également un fournisseur d’identité qui implémente Azure App Service Identity. Pour configurer Azure App Service Identity dans votre application, modifiez vos Program.cséléments suivants :

builder.Services.AddAuthentication(AzureAppServiceAuthentication.AuthenticationScheme)
  .AddAzureAppServiceAuthentication(options => options.ForceEnable = true);

// Then later, after you have created the app
app.UseAuthentication();
app.UseAuthorization();

Prise en charge de la base de données

Entity Framework Core ne configure pas la génération de valeur pour les colonnes de date/heure. (Voir Génération de valeur de date/heure). Le dépôt Azure Mobile Apps pour Entity Framework Core met automatiquement à jour le UpdatedAt champ pour vous. Toutefois, si votre base de données est mise à jour en dehors du référentiel, vous devez organiser la mise à jour des champs et Version des UpdatedAt champs.

Azure SQL

Créez un déclencheur pour chaque entité :

CREATE OR ALTER TRIGGER [dbo].[TodoItems_UpdatedAt] ON [dbo].[TodoItems]
    AFTER INSERT, UPDATE
AS
BEGIN
    SET NOCOUNT ON;
    UPDATE 
        [dbo].[TodoItems] 
    SET 
        [UpdatedAt] = GETUTCDATE() 
    WHERE 
        [Id] IN (SELECT [Id] FROM INSERTED);
END

Vous pouvez installer ce déclencheur à l’aide d’une migration ou immédiatement après EnsureCreated() la création de la base de données.

Azure Cosmos DB

Azure Cosmos DB est une base de données NoSQL entièrement managée pour les applications hautes performances de toute taille ou échelle. Pour plus d’informations sur l’utilisation d’Azure Cosmos DB avec Entity Framework Core, consultez le fournisseur Azure Cosmos DB. Lors de l’utilisation d’Azure Cosmos DB avec Azure Mobile Apps :

  1. Configurez le conteneur Cosmos avec un index composite qui spécifie les champs et Id les UpdatedAt champs. Les index composites peuvent être ajoutés à un conteneur via les Portail Azure, ARM, Bicep, Terraform ou dans le code. Voici un exemple de définition de ressource bicep :

    resource cosmosContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-04-15' = {
        name: 'TodoItems'
        parent: cosmosDatabase
        properties: {
            resource: {
                id: 'TodoItems'
                partitionKey: {
                    paths: [
                        '/Id'
                    ]
                    kind: 'Hash'
                }
                indexingPolicy: {
                    indexingMode: 'consistent'
                    automatic: true
                    includedPaths: [
                        {
                            path: '/*'
                        }
                    ]
                    excludedPaths: [
                        {
                            path: '/"_etag"/?'
                        }
                    ]
                    compositeIndexes: [
                        [
                            {
                                path: '/UpdatedAt'
                                order: 'ascending'
                            }
                            {
                                path: '/Id'
                                order: 'ascending'
                            }
                        ]
                    ]
                }
            }
        }
    }
    

    Si vous extrayez un sous-ensemble d’éléments dans la table, veillez à spécifier toutes les propriétés impliquées dans la requête.

  2. Dérivez des modèles de la ETagEntityTableData classe :

    public class TodoItem : ETagEntityTableData
    {
        public string Title { get; set; }
        public bool Completed { get; set; }
    }
    
  3. Ajoutez une OnModelCreating(ModelBuilder) méthode au DbContext. Le pilote Cosmos DB pour Entity Framework place toutes les entités dans le même conteneur par défaut. Au minimum, vous devez choisir une clé de partition appropriée et vous assurer que la EntityTag propriété est marquée comme balise d’accès concurrentiel. Par exemple, l’extrait de code suivant stocke les TodoItem entités dans leur propre conteneur avec les paramètres appropriés pour Azure Mobile Apps :

    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.Entity<TodoItem>(builder =>
        {
            // Store this model in a specific container.
            builder.ToContainer("TodoItems");
            // Do not include a discriminator for the model in the partition key.
            builder.HasNoDiscriminator();
            // Set the partition key to the Id of the record.
            builder.HasPartitionKey(model => model.Id);
            // Set the concurrency tag to the EntityTag property.
            builder.Property(model => model.EntityTag).IsETagConcurrency();
        });
        base.OnModelCreating(builder);
    }
    

Azure Cosmos DB est pris en charge dans le Microsoft.AspNetCore.Datasync.EFCore package NuGet depuis la version 5.0.11. Pour plus d’informations, consultez les liens suivants :

PostgreSQL

Créez un déclencheur pour chaque entité :

CREATE OR REPLACE FUNCTION todoitems_datasync() RETURNS trigger AS $$
BEGIN
    NEW."UpdatedAt" = NOW() AT TIME ZONE 'UTC';
    NEW."Version" = convert_to(gen_random_uuid()::text, 'UTF8');
    RETURN NEW
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE TRIGGER
    todoitems_datasync
BEFORE INSERT OR UPDATE ON
    "TodoItems"
FOR EACH ROW EXECUTE PROCEDURE
    todoitems_datasync();

Vous pouvez installer ce déclencheur à l’aide d’une migration ou immédiatement après EnsureCreated() la création de la base de données.

Sqlite

Avertissement

N’utilisez pas SqLite pour les services de production. SqLite convient uniquement à l’utilisation côté client en production.

SqLite n’a pas de champ de date/heure qui prend en charge la précision de millisecondes. Par conséquent, il n’est pas adapté à quoi que ce soit à l’exception des tests. Si vous souhaitez utiliser SqLite, veillez à implémenter un convertisseur de valeurs et un comparateur de valeurs sur chaque modèle pour les propriétés de date/heure. La méthode la plus simple pour implémenter des convertisseurs de valeur et des comparateurs se trouve dans la OnModelCreating(ModelBuilder) méthode de votre DbContext:

protected override void OnModelCreating(ModelBuilder builder)
{
    var timestampProps = builder.Model.GetEntityTypes().SelectMany(t => t.GetProperties())
        .Where(p => p.ClrType == typeof(byte[]) && p.ValueGenerated == ValueGenerated.OnAddOrUpdate);
    var converter = new ValueConverter<byte[], string>(
        v => Encoding.UTF8.GetString(v),
        v => Encoding.UTF8.GetBytes(v)
    );
    foreach (var property in timestampProps)
    {
        property.SetValueConverter(converter);
        property.SetDefaultValueSql("STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')");
    }
    base.OnModelCreating(builder);
}

Installez un déclencheur de mise à jour lorsque vous initialisez la base de données :

internal static void InstallUpdateTriggers(DbContext context)
{
    foreach (var table in context.Model.GetEntityTypes())
    {
        var props = table.GetProperties().Where(prop => prop.ClrType == typeof(byte[]) && prop.ValueGenerated == ValueGenerated.OnAddOrUpdate);
        foreach (var property in props)
        {
            var sql = $@"
                CREATE TRIGGER s_{table.GetTableName()}_{prop.Name}_UPDATE AFTER UPDATE ON {table.GetTableName()}
                BEGIN
                    UPDATE {table.GetTableName()}
                    SET {prop.Name} = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
                    WHERE rowid = NEW.rowid;
                END
            ";
            context.Database.ExecuteSqlRaw(sql);
        }
    }
}

Vérifiez que la méthode n’est appelée qu’une seule fois pendant l’initialisation InstallUpdateTriggers de la base de données :

public void InitializeDatabase(DbContext context)
{
    bool created = context.Database.EnsureCreated();
    if (created && context.Database.IsSqlite())
    {
        InstallUpdateTriggers(context);
    }
    context.Database.SaveChanges();
}

LiteDB

LiteDB est une base de données serverless fournie dans une seule petite DLL écrite en code managé C# .NET. Il s’agit d’une solution de base de données NoSQL simple et rapide pour les applications autonomes. Pour utiliser LiteDb avec stockage persistant sur disque :

  1. Installez le package Microsoft.AspNetCore.Datasync.LiteDb à partir de NuGet.

  2. Ajoutez un singleton pour le LiteDatabaseProgram.cs:

    const connectionString = builder.Configuration.GetValue<string>("LiteDb:ConnectionString");
    builder.Services.AddSingleton<LiteDatabase>(new LiteDatabase(connectionString));
    
  3. Dériver des modèles à partir de :LiteDbTableData

    public class TodoItem : LiteDbTableData
    {
        public string Title { get; set; }
        public bool Completed { get; set; }
    }
    

    Vous pouvez utiliser l’un des BsonMapper attributs fournis avec le package NuGet LiteDb.

  4. Créez un contrôleur à l’aide des LiteDbRepositorypoints suivants :

    [Route("tables/[controller]")]
    public class TodoItemController : TableController<TodoItem>
    {
        public TodoItemController(LiteDatabase db) : base()
        {
            Repository = new LiteDbRepository<TodoItem>(db, "todoitems");
        }
    }
    

Prise en charge d’OpenAPI

Vous pouvez publier l’API définie par les contrôleurs de synchronisation de données à l’aide de NSwag ou de Swashbuckle. Dans les deux cas, commencez par configurer le service comme vous le feriez normalement pour la bibliothèque choisie.

NSwag

Suivez les instructions de base pour l’intégration de NSwag, puis modifiez comme suit :

  1. Ajoutez des packages à votre projet pour prendre en charge NSwag. Les packages suivants sont requis :

  2. Ajoutez ce qui suit en haut de votre Program.cs fichier :

    using Microsoft.AspNetCore.Datasync.NSwag;
    
  3. Ajoutez un service pour générer une définition OpenAPI à votre Program.cs fichier :

    builder.Services.AddOpenApiDocument(options =>
    {
        options.AddDatasyncProcessors();
    });
    
  4. Activez l’intergiciel pour traiter le document JSON généré et l’interface utilisateur de Swagger, également dans Program.cs :

    if (app.Environment.IsDevelopment())
    {
        app.UseOpenApi();
        app.UseSwaggerUI3();
    }
    

La navigation vers le /swagger point de terminaison du service web vous permet de parcourir l’API. La définition OpenAPI peut ensuite être importée dans d’autres services (par exemple, Azure Gestion des API). Pour plus d’informations sur la configuration de NSwag, consultez Prise en main de NSwag et ASP.NET Core.

Swashbuckle

Suivez les instructions de base pour l’intégration de Swashbuckle, puis modifiez comme suit :

  1. Ajoutez des packages à votre projet pour prendre en charge Swashbuckle. Les packages suivants sont requis :

  2. Ajoutez un service pour générer une définition OpenAPI à votre Program.cs fichier :

    builder.Services.AddSwaggerGen(options => 
    {
        options.AddDatasyncControllers();
    });
    builder.Services.AddSwaggerGenNewtonsoftSupport();
    

    Remarque

    La AddDatasyncControllers() méthode prend une option qui Assembly correspond à l’assembly qui contient vos contrôleurs de table. Le Assembly paramètre est obligatoire uniquement si vos contrôleurs de table se trouvent dans un projet différent du service.

  3. Activez l’intergiciel pour traiter le document JSON généré et l’interface utilisateur de Swagger, également dans Program.cs :

    if (app.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI(options => 
        {
            options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");
            options.RoutePrefix = string.Empty;
        });
    }
    

Avec cette configuration, la navigation à la racine du service web vous permet de parcourir l’API. La définition OpenAPI peut ensuite être importée dans d’autres services (par exemple, Azure Gestion des API). Pour plus d’informations sur la configuration de Swashbuckle, consultez Prise en main de Swashbuckle et ASP.NET Core.

Limites

L’édition ASP.NET Core des bibliothèques de services implémente OData v4 pour l’opération de liste. Lorsque le serveur s’exécute en mode de compatibilité descendante, le filtrage sur une sous-chaîne n’est pas pris en charge.