Comment utiliser le Kit de développement logiciel (SDK) du serveur principal ASP.NET Core
Note
Ce produit est mis hors service. Pour un remplacement des projets utilisant .NET 8 ou version ultérieure, consultez la bibliothèque Datasync Community Toolkit.
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 répondre aux critères suivants ont un champ de type DateTime
ou Timestamp
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 :
- Créez un projet serveur ASP.NET 6.0 (ou version ultérieure).
- Ajouter Entity Framework Core
- 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 didacticiel.
Pour activer les services de synchronisation des données, vous devez ajouter les bibliothèques NuGet suivantes :
- Microsoft.AspNetCore.Datasync
- Microsoft.AspNetCore.Datasync.EFCore pour les tables Entity Framework Core.
- Microsoft.AspNetCore.Datasync.InMemory pour les tables en mémoire.
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 :
- Créez une classe de modèle pour le modèle de données.
- Ajoutez la classe de modèle au
DbContext
de votre application. - Créez une classe
TableController<T>
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 de la dernière mise à jour de l’enregistrement. -
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 ApiController
spécialisé. 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);
}
}
Note
- Le contrôleur doit avoir un itinéraire. Par convention, les tables sont exposées sur un sous-chemin d'
/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'/tables
. - Le contrôleur doit hériter de
TableController<T>
, où<T>
est une implémentation de l’implémentationITableData
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 de TableControllerOptions
:
[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
(int
, valeur par défaut : 100) correspond au nombre maximal d’éléments retournés par une opération de requête dans une seule page. -
MaxTop
(int
, valeur par défaut : 512000) correspond au nombre maximal d’éléments retournés dans une opération de requête sans pagination. -
EnableSoftDelete
(bool
, valeur par 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
(int
, valeur par 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
. Le IAccessControlProvider
utilise trois méthodes pour 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 au HttpContext
, configurer unHttpContextAccessor .
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 le IHttpContextAccessor
pour accéder au HttpContext
de manière sécurisée de thread.
Pour utiliser ce fournisseur de contrôle d’accès, mettez à jour votre TableController
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 avec [AllowAnonymous]
au lieu de [Authorize]
.
Configurer la journalisation
La journalisation est gérée via le mécanisme de journalisation normal pour ASP.NET Core. Affectez l’objet ILogger
à la propriété Logger
:
[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 méthode PostCommitHookAsync()
. 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 classe de base TableController<T>
contient un gestionnaire d’événements appelé en même temps que la méthode PostCommitHookAsync()
.
[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 Core Identity, ou tout autre schéma d’authentification et d’autorisation que vous souhaitez prendre en charge. Pour faciliter les mises à niveau 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 votre Program.cs
:
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 des bases de données
Entity Framework Core ne configure pas la génération de valeur pour les colonnes de date/heure. (Consultez génération de valeur date/heure). Le dépôt Azure Mobile Apps pour Entity Framework Core met automatiquement à jour le champ UpdatedAt
pour vous. Toutefois, si votre base de données est mise à jour en dehors du référentiel, vous devez organiser les champs UpdatedAt
et Version
à mettre à jour.
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()
pour créer 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. Consultez fournisseur Azure Cosmos DB pour plus d’informations sur l’utilisation d’Azure Cosmos DB avec Entity Framework Core. Lors de l’utilisation d’Azure Cosmos DB avec Azure Mobile Apps :
Configurez le conteneur Cosmos avec un index composite qui spécifie les champs
UpdatedAt
etId
. Les index composites peuvent être ajoutés à un conteneur via le portail Azure, ARM, Bicep, Terraform ou dans le code. Voici un exemple 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.
Dérivez des modèles de la classe
ETagEntityTableData
:public class TodoItem : ETagEntityTableData { public string Title { get; set; } public bool Completed { get; set; } }
Ajoutez une méthode
OnModelCreating(ModelBuilder)
auDbContext
. 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 vérifier que la propriétéEntityTag
est marquée comme balise d’accès concurrentiel. Par exemple, l’extrait de code suivant stocke les entitésTodoItem
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 package NuGet Microsoft.AspNetCore.Datasync.EFCore
depuis la version 5.0.11. Pour plus d’informations, consultez les liens suivants :
- exemple Cosmos DB.
- documentation du fournisseur Azure Cosmos DB EF Core.
- documentation sur la stratégie d’index Cosmos DB.
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()
pour créer 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 méthode OnModelCreating(ModelBuilder)
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 InstallUpdateTriggers
n’est appelée qu’une seule fois lors de l’initialisation 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 :
Installez le package
Microsoft.AspNetCore.Datasync.LiteDb
à partir de NuGet.Ajoutez un singleton pour le
LiteDatabase
auProgram.cs
:const connectionString = builder.Configuration.GetValue<string>("LiteDb:ConnectionString"); builder.Services.AddSingleton<LiteDatabase>(new LiteDatabase(connectionString));
Dérivez des modèles du
LiteDbTableData
:public class TodoItem : LiteDbTableData { public string Title { get; set; } public bool Completed { get; set; } }
Vous pouvez utiliser l’un des attributs
BsonMapper
fournis avec le package NuGet LiteDb.Créez un contrôleur à l’aide de l'
LiteDbRepository
:[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 NSwag ou 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 :
Ajoutez des packages à votre projet pour prendre en charge NSwag. Les packages suivants sont requis :
Ajoutez ce qui suit en haut de votre fichier
Program.cs
:using Microsoft.AspNetCore.Datasync.NSwag;
Ajoutez un service pour générer une définition OpenAPI à votre fichier
Program.cs
:builder.Services.AddOpenApiDocument(options => { options.AddDatasyncProcessors(); });
Activez l’intergiciel pour servir le document JSON généré et l’interface utilisateur Swagger, également dans
Program.cs
:if (app.Environment.IsDevelopment()) { app.UseOpenApi(); app.UseSwaggerUI3(); }
La navigation vers le point de terminaison /swagger
du service web vous permet de parcourir l’API. La définition OpenAPI peut ensuite être importée dans d’autres services (par exemple, Gestion des API Azure). Pour plus d’informations sur la configuration de NSwag, consultez Bien démarrer avec NSwag et ASP.NET Core.
Swashbuckle
Suivez les instructions de base pour l’intégration de Swashbuckle, puis modifiez comme suit :
Ajoutez des packages à votre projet pour prendre en charge Swashbuckle. Les packages suivants sont requis :
Ajoutez un service pour générer une définition OpenAPI à votre fichier
Program.cs
:builder.Services.AddSwaggerGen(options => { options.AddDatasyncControllers(); }); builder.Services.AddSwaggerGenNewtonsoftSupport();
Note
La méthode
AddDatasyncControllers()
prend uneAssembly
facultative qui correspond à l’assembly qui contient vos contrôleurs de table. Le paramètreAssembly
n’est requis que si vos contrôleurs de table se trouvent dans un projet différent du service.Activez l’intergiciel pour servir le document JSON généré et l’interface utilisateur 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, Gestion des API Azure). Pour plus d’informations sur la configuration de Swashbuckle, consultez Bien démarrer avec Swashbuckle et ASP.NET Core.
Limitations
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.