Condividi tramite


Come usare l'SDK del server back-end di ASP.NET Core

Nota

Questo prodotto viene ritirato. Per una sostituzione dei progetti che usano .NET 8 o versione successiva, vedere la libreria datasync di Community Toolkit.

Questo articolo illustra come configurare e usare l'SDK del server back-end di ASP.NET Core per produrre un server di sincronizzazione dati.

Piattaforme supportate

Il server back-end ASP.NET Core supporta ASP.NET 6.0 o versione successiva.

I server di database devono soddisfare i criteri seguenti includono un campo di tipo DateTime o Timestamp archiviato con accuratezza in millisecondi. Le implementazioni del repository vengono fornite per Entity Framework Core e LiteDb.

Per il supporto specifico del database, vedere le sezioni seguenti:

Creare un nuovo server di sincronizzazione dati

Un server di sincronizzazione dati usa i normali meccanismi ASP.NET Core per la creazione del server. È costituito da tre passaggi:

  1. Creare un progetto server ASP.NET 6.0 (o versione successiva).
  2. Aggiungere Entity Framework Core
  3. Aggiungere Servizi di sincronizzazione dati

Per informazioni sulla creazione di un servizio ASP.NET Core con Entity Framework Core, vedere 'esercitazione.

Per abilitare i servizi di sincronizzazione dati, è necessario aggiungere le librerie NuGet seguenti:

Modificare il file Program.cs. Aggiungere la riga seguente in tutte le altre definizioni di servizio:

builder.Services.AddDatasyncControllers();

È anche possibile usare il modello datasync-server core ASP.NET:

# 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

Il modello include un modello di esempio e un controller.

Creare un controller di tabella per una tabella SQL

Il repository predefinito usa Entity Framework Core. La creazione di un controller di tabella è un processo in tre passaggi:

  1. Creare una classe modello per il modello di dati.
  2. Aggiungere la classe del modello al DbContext per l'applicazione.
  3. Creare una nuova classe TableController<T> per esporre il modello.

Creare una classe modello

Tutte le classi di modello devono implementare ITableData. Ogni tipo di repository ha una classe astratta che implementa ITableData. Il repository Entity Framework Core usa 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'interfaccia ITableData fornisce l'ID del record, insieme alle proprietà aggiuntive per la gestione dei servizi di sincronizzazione dati:

  • UpdatedAt (DateTimeOffset?) fornisce la data dell'ultimo aggiornamento del record.
  • Version (byte[]) fornisce un valore opaco che cambia in ogni scrittura.
  • Deleted (bool) è true se il record è contrassegnato per l'eliminazione ma non è ancora stato eliminato.

La libreria di sincronizzazione dati gestisce queste proprietà. Non modificare queste proprietà nel proprio codice.

Aggiornare il DbContext

Ogni modello nel database deve essere registrato nel DbContext. Per esempio:

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

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

Creare un controller di tabella

Un controller di tabella è un ApiControllerspecializzato. Ecco un controller di tabella minimo:

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

Nota

  • Il controller deve avere una route. Per convenzione, le tabelle vengono esposte in un sottopercorso di /tables, ma possono essere posizionate ovunque. Se si usano librerie client precedenti alla versione 5.0.0, la tabella deve essere un sottopercorso di /tables.
  • Il controller deve ereditare da TableController<T>, dove <T> è un'implementazione dell'implementazione ITableData per il tipo di repository.
  • Assegnare un repository in base allo stesso tipo del modello.

Implementazione di un repository in memoria

È anche possibile usare un repository in memoria senza spazio di archiviazione permanente. Aggiungere un servizio singleton per il repository nel Program.cs:

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

Configurare il controller di tabella come segue:

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

Configurare le opzioni del controller di tabella

È possibile configurare alcuni aspetti del controller usando TableControllerOptions:

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

Le opzioni che è possibile impostare includono:

  • PageSize (int, valore predefinito: 100) è il numero massimo di elementi restituiti da un'operazione di query in una singola pagina.
  • MaxTop (int, valore predefinito: 512000) è il numero massimo di elementi restituiti in un'operazione di query senza paging.
  • EnableSoftDelete (bool, valore predefinito: false) abilita l'eliminazione temporanea, che contrassegna gli elementi come eliminati anziché eliminarli dal database. L'eliminazione temporanea consente ai client di aggiornare la cache offline, ma richiede che gli elementi eliminati vengano eliminati separatamente dal database.
  • UnauthorizedStatusCode (int, valore predefinito: 401 Non autorizzato) è il codice di stato restituito quando l'utente non è autorizzato a eseguire un'azione.

Configurare le autorizzazioni di accesso

Per impostazione predefinita, un utente può eseguire qualsiasi operazione che desidera eseguire per le entità all'interno di una tabella, ovvero creare, leggere, aggiornare ed eliminare qualsiasi record. Per un controllo più granulare sull'autorizzazione, creare una classe che implementi IAccessControlProvider. Il IAccessControlProvider usa tre metodi per implementare l'autorizzazione:

  • GetDataView() restituisce un'espressione lambda che limita ciò che l'utente connesso può visualizzare.
  • IsAuthorizedAsync() determina se l'utente connesso può eseguire l'azione sull'entità specifica richiesta.
  • PreCommitHookAsync() regola qualsiasi entità immediatamente prima di essere scritta nel repository.

Tra i tre metodi, è possibile gestire in modo efficace la maggior parte dei casi di controllo di accesso. Se è necessario accedere al HttpContext, configurare un oggetto HttpContextAccessor.

Ad esempio, il codice seguente implementa una tabella personale, in cui un utente può visualizzare solo i propri record.

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;
    }
}

I metodi sono asincroni nel caso in cui sia necessario eseguire una ricerca di database aggiuntiva per ottenere la risposta corretta. È possibile implementare l'interfaccia IAccessControlProvider<T> nel controller, ma è comunque necessario passare il IHttpContextAccessor per accedere al HttpContext in modo thread-safe.

Per usare questo provider di controllo di accesso, aggiornare il TableController come indicato di seguito:

[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);
    }
}

Se si vuole consentire l'accesso non autenticato e autenticato a una tabella, decorarlo con [AllowAnonymous] anziché [Authorize].

Configurare la registrazione

La registrazione viene gestita tramite il normale meccanismo di registrazione per ASP.NET Core. Assegnare l'oggetto ILogger alla proprietà 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;
    }
}

Monitorare le modifiche del repository

Quando il repository viene modificato, è possibile attivare i flussi di lavoro, registrare la risposta al client o eseguire altre operazioni in uno dei due metodi seguenti:

Opzione 1: Implementare un postCommitHookAsync

L'interfaccia IAccessControlProvider<T> fornisce un metodo PostCommitHookAsync(). Il metodo PostCommitHookAsync() viene chiamato dopo la scrittura dei dati nel repository, ma prima di restituire i dati al client. È necessario assicurarsi che i dati restituiti al client non vengano modificati in questo metodo.

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.
    }
}

Usare questa opzione se si eseguono attività asincrone come parte dell'hook.

Opzione 2: Usare il gestore eventi RepositoryUpdated

La classe base TableController<T> contiene un gestore eventi chiamato contemporaneamente al metodo 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
    }
}

Abilitare l'identità del servizio app di Azure

Il server di sincronizzazione dati core ASP.NET supporta ASP.NET Core Identityo qualsiasi altro schema di autenticazione e autorizzazione che si vuole supportare. Per facilitare gli aggiornamenti delle versioni precedenti di App per dispositivi mobili di Azure, viene fornito anche un provider di identità che implementa 'identità del servizio app di Azure. Per configurare l'identità del servizio app di Azure nell'applicazione, modificare il Program.cs:

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

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

Supporto del database

Entity Framework Core non configura la generazione di valori per le colonne di data/ora. Vedere generazione di valori di data/ora). Il repository di App per dispositivi mobili di Azure per Entity Framework Core aggiorna automaticamente il campo UpdatedAt. Tuttavia, se il database viene aggiornato all'esterno del repository, è necessario disporre per aggiornare i campi UpdatedAt e Version.

Azure SQL

Creare un trigger per ogni 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

È possibile installare questo trigger usando una migrazione o immediatamente dopo EnsureCreated() per creare il database.

Azure Cosmos DB

Azure Cosmos DB è un database NoSQL completamente gestito per applicazioni a prestazioni elevate di qualsiasi dimensione o scalabilità. Per informazioni sull'uso di Azure Cosmos DB con Entity Framework Core, vedere provider di Azure Cosmos DB. Quando si usa Azure Cosmos DB con app per dispositivi mobili di Azure:

  1. Configurare il contenitore Cosmos con un indice composito che specifica i campi UpdatedAt e Id. È possibile aggiungere indici compositi a un contenitore tramite il portale di Azure, ARM, Bicep, Terraform o all'interno del codice. Di seguito è riportato un esempio definizione di risorsa 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'
                            }
                        ]
                    ]
                }
            }
        }
    }
    

    Se si esegue il pull di un subset di elementi nella tabella, assicurarsi di specificare tutte le proprietà coinvolte nella query.

  2. Derivare modelli dalla classe ETagEntityTableData:

    public class TodoItem : ETagEntityTableData
    {
        public string Title { get; set; }
        public bool Completed { get; set; }
    }
    
  3. Aggiungere un metodo OnModelCreating(ModelBuilder) al DbContext. Il driver Cosmos DB per Entity Framework inserisce tutte le entità nello stesso contenitore per impostazione predefinita. È necessario selezionare almeno una chiave di partizione appropriata e assicurarsi che la proprietà EntityTag sia contrassegnata come tag di concorrenza. Ad esempio, il frammento di codice seguente archivia le TodoItem entità nel proprio contenitore con le impostazioni appropriate per App per dispositivi mobili di Azure:

    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 è supportato nel pacchetto NuGet Microsoft.AspNetCore.Datasync.EFCore dalla versione 5.0.11. Per altre informazioni, vedere i collegamenti seguenti:

PostgreSQL

Creare un trigger per ogni 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();

È possibile installare questo trigger usando una migrazione o immediatamente dopo EnsureCreated() per creare il database.

SqLite

Avvertimento

Non usare SqLite per i servizi di produzione. SqLite è adatto solo per l'utilizzo lato client nell'ambiente di produzione.

SqLite non dispone di un campo di data/ora che supporta l'accuratezza dei millisecondi. Di conseguenza, non è adatto ad alcun elemento, ad eccezione dei test. Se si vuole usare SqLite, assicurarsi di implementare un convertitore di valori e un operatore di confronto dei valori in ogni modello per le proprietà di data/ora. Il metodo più semplice per implementare convertitori di valori e comparer è nel metodo OnModelCreating(ModelBuilder) del 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);
}

Installare un trigger di aggiornamento quando si inizializza il database:

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);
        }
    }
}

Assicurarsi che il metodo InstallUpdateTriggers venga chiamato una sola volta durante l'inizializzazione del database:

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

LiteDB

LiteDB è un database serverless fornito in una singola DLL piccola scritta in codice gestito C# .NET. Si tratta di una soluzione di database NoSQL semplice e veloce per applicazioni autonome. Per usare LiteDb con l'archiviazione permanente su disco:

  1. Installare il pacchetto Microsoft.AspNetCore.Datasync.LiteDb da NuGet.

  2. Aggiungere un singleton per il LiteDatabase al Program.cs:

    const connectionString = builder.Configuration.GetValue<string>("LiteDb:ConnectionString");
    builder.Services.AddSingleton<LiteDatabase>(new LiteDatabase(connectionString));
    
  3. Derivare modelli dal LiteDbTableData:

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

    È possibile usare uno qualsiasi degli attributi BsonMapper forniti con il pacchetto NuGet LiteDb.

  4. Creare un controller usando il LiteDbRepository:

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

Supporto openAPI

È possibile pubblicare l'API definita dai controller di sincronizzazione dati usando NSwag o Swashbuckle. In entrambi i casi, iniziare configurando il servizio come normalmente si farebbe per la libreria scelta.

NSwag

Seguire le istruzioni di base per l'integrazione di NSwag, quindi modificare come segue:

  1. Aggiungere pacchetti al progetto per supportare NSwag. Sono necessari i pacchetti seguenti:

  2. Aggiungere quanto segue all'inizio del file Program.cs:

    using Microsoft.AspNetCore.Datasync.NSwag;
    
  3. Aggiungere un servizio per generare una definizione OpenAPI al file Program.cs:

    builder.Services.AddOpenApiDocument(options =>
    {
        options.AddDatasyncProcessors();
    });
    
  4. Abilitare il middleware per gestire il documento JSON generato e l'interfaccia utente di Swagger, anche in Program.cs:

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

L'esplorazione dell'endpoint /swagger del servizio Web consente di esplorare l'API. La definizione OpenAPI può quindi essere importata in altri servizi, ad esempio Gestione API di Azure. Per altre informazioni sulla configurazione di NSwag, vedere Introduzione a NSwag e ASP.NET Core.

Swashbuckle

Seguire le istruzioni di base per l'integrazione di Swashbuckle, quindi modificare come segue:

  1. Aggiungere pacchetti al progetto per supportare Swashbuckle. Sono necessari i pacchetti seguenti:

  2. Aggiungere un servizio per generare una definizione OpenAPI al file Program.cs:

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

    Nota

    Il metodo AddDatasyncControllers() accetta un Assembly facoltativo che corrisponde all'assembly che contiene i controller di tabella. Il parametro Assembly è necessario solo se i controller di tabella si trovano in un progetto diverso per il servizio.

  3. Abilitare il middleware per gestire il documento JSON generato e l'interfaccia utente di Swagger, anche in Program.cs:

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

Con questa configurazione, passare alla radice del servizio Web consente di esplorare l'API. La definizione OpenAPI può quindi essere importata in altri servizi, ad esempio Gestione API di Azure. Per altre informazioni sulla configurazione di Swashbuckle, vedere Introduzione a Swashbuckle e ASP.NET Core.

Limitazioni

L'edizione ASP.NET Core delle librerie di servizi implementa OData v4 per l'operazione di elenco. Quando il server è in esecuzione in modalità di compatibilità con le versioni precedenti, il filtro in base a una sottostringa non è supportato.