Partilhar via


Como usar o SDK do servidor back-end ASP.NET Core

Observação

Este produto foi retirado. Para obter uma substituição para projetos que usam o .NET 8 ou posterior, consulte a biblioteca Community Toolkit Datasync.

Este artigo mostra que você precisa configurar e usar o SDK do servidor back-end ASP.NET Core para produzir um servidor de sincronização de dados.

Plataformas suportadas

O servidor back-end ASP.NET Core suporta ASP.NET 6.0 ou posterior.

Os servidores de banco de dados devem atender aos seguintes critérios: ter um campo de tipo DateTime ou Timestamp armazenado com precisão de milissegundos. As implementações de repositório são fornecidas para Entity Framework Core e LiteDb.

Para obter suporte específico ao banco de dados, consulte as seguintes seções:

Criar um novo servidor de sincronização de dados

Um servidor de sincronização de dados usa os mecanismos normais do ASP.NET Core para criar o servidor. Consiste em três etapas:

  1. Crie um projeto de servidor ASP.NET 6.0 (ou posterior).
  2. Adicionar núcleo do Entity Framework
  3. Adicionar serviços de sincronização de dados

Para obter informações sobre como criar um serviço ASP.NET Core com o Entity Framework Core, consulte o tutorial.

Para habilitar os serviços de sincronização de dados, você precisa adicionar as seguintes bibliotecas NuGet:

Modifique o arquivo Program.cs. Adicione a seguinte linha abaixo de todas as outras definições de serviço:

builder.Services.AddDatasyncControllers();

Você também pode usar o modelo 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

O modelo inclui um modelo de exemplo e um controlador.

Criar um controlador de tabela para uma tabela SQL

O repositório padrão usa o Entity Framework Core. A criação de um controlador de tabela é um processo de três etapas:

  1. Crie uma classe de modelo para o modelo de dados.
  2. Adicione a classe de modelo ao DbContext do seu aplicativo.
  3. Crie uma nova classe TableController<T> para expor seu modelo.

Criar uma classe de modelo

Todas as classes de modelo devem implementar ITableData. Cada tipo de repositório tem uma classe abstrata que implementa ITableData. O repositório principal do Entity Framework 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; }
}

A interface ITableData fornece a ID do registro, juntamente com propriedades extras para lidar com serviços de sincronização de dados:

  • UpdatedAt (DateTimeOffset?) indica a data em que o registo foi atualizado pela última vez.
  • Version (byte[]) fornece um valor opaco que muda a cada gravação.
  • Deleted (bool) é verdadeiro se o registro estiver marcado para exclusão, mas ainda não tiver sido limpo.

A biblioteca de sincronização de dados mantém essas propriedades. Não modifique essas propriedades em seu próprio código.

Atualizar o DbContext

Cada modelo na base de dados deve ser registado no DbContext. Por exemplo:

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

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

Criar um controlador de tabela

Um controlador de mesa é um ApiControllerespecializado. Aqui está um controlador de tabela mínimo:

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

Observação

  • O controlador deve ter uma rota. Por convenção, as tabelas são expostas em um subcaminho de /tables, mas podem ser colocadas em qualquer lugar. Se você estiver usando bibliotecas de cliente anteriores à v5.0.0, a tabela deverá ser um subcaminho de /tables.
  • O controlador deve herdar de TableController<T>, onde <T> é uma implementação da implementação ITableData para seu tipo de repositório.
  • Atribua um repositório com base no mesmo tipo do seu modelo.

Implementando um repositório na memória

Você também pode usar um repositório na memória sem armazenamento persistente. Adicione um serviço singleton para o repositório em seu Program.cs:

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

Configure o controlador de tabela da seguinte forma:

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

Configurar opções do controlador de tabela

Você pode configurar certos aspetos do controlador usando TableControllerOptions:

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

As opções que você pode definir incluem:

  • PageSize (int, padrão: 100) é o número máximo de itens que uma operação de consulta retornou em uma única página.
  • MaxTop (int, padrão: 512000) é o número máximo de itens retornados em uma operação de consulta sem paginação.
  • EnableSoftDelete (bool, default: false) permite a exclusão suave, que marca os itens como excluídos em vez de excluí-los do banco de dados. A exclusão suave permite que os clientes atualizem seu cache offline, mas requer que os itens excluídos sejam removidos do banco de dados separadamente.
  • UnauthorizedStatusCode (int, padrão: 401 Não autorizado) é o código de status retornado quando o usuário não tem permissão para executar uma ação.

Configurar permissões de acesso

Por padrão, um usuário pode fazer o que quiser dentro de uma tabela - criar, ler, atualizar e excluir qualquer registro. Para obter um controle mais refinado sobre a autorização, crie uma classe que implemente IAccessControlProvider. O IAccessControlProvider usa três métodos para implementar a autorização:

  • GetDataView() retorna um lambda que limita o que o usuário conectado pode ver.
  • IsAuthorizedAsync() determina se o usuário conectado pode executar a ação na entidade específica que está sendo solicitada.
  • PreCommitHookAsync() ajusta qualquer entidade imediatamente antes de ser gravada no repositório.

Entre os três métodos, você pode lidar efetivamente com a maioria dos casos de controle de acesso. Se você precisar acessar o HttpContext, configurar um HttpContextAccessor.

Como exemplo, o seguinte implementa uma tabela pessoal, onde um usuário só pode ver seus próprios registros.

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

Os métodos são assíncronos no caso de você precisar fazer uma pesquisa de banco de dados extra para obter a resposta correta. Você pode implementar a interface IAccessControlProvider<T> no controlador, mas ainda precisa passar no IHttpContextAccessor para acessar o HttpContext de forma segura de thread.

Para usar esse provedor de controle de acesso, atualize seu TableController da seguinte maneira:

[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 quiser permitir o acesso não autenticado e autenticado a uma tabela, decore-a com [AllowAnonymous] em vez de [Authorize].

Configurar o registro em log

O registo é tratado através do o mecanismo de registo normal para ASP.NET Core. Atribua o objeto ILogger à propriedade 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;
    }
}

Monitorar alterações no repositório

Quando o repositório é alterado, você pode disparar fluxos de trabalho, registrar a resposta para o cliente ou fazer outro trabalho em um dos dois métodos:

Opção 1: Implementar um PostCommitHookAsync

A interface IAccessControlProvider<T> fornece um método PostCommitHookAsync(). O método PostCommitHookAsync() é chamado depois que os dados são gravados no repositório, mas antes de retornar os dados ao cliente. Deve-se ter cuidado para garantir que os dados que estão sendo retornados ao cliente não sejam alterados nesse método.

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

Use esta opção se estiver executando tarefas assíncronas como parte do gancho.

Opção 2: Usar o manipulador de eventos RepositoryUpdated

A classe base TableController<T> contém um manipulador de eventos que é chamado ao mesmo tempo que o método 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
    }
}

Habilitar a identidade do Serviço de Aplicativo do Azure

O servidor de sincronização de dados ASP.NET Core suporta ASP.NETde identidade principal ou qualquer outro esquema de autenticação e autorização que você deseje suportar. Para ajudar com atualizações de versões anteriores dos Aplicativos Móveis do Azure, também fornecemos um provedor de identidade que implementa Identidade do Serviço de Aplicativo do Azure. Para configurar a Identidade do Serviço de Aplicativo do Azure em seu aplicativo, edite seus Program.cs:

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

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

Suporte a bases de dados

O Entity Framework Core não configura a geração de valor para colunas de data/hora. (Consulte Geração de valor de data/hora). O repositório de Aplicativos Móveis do Azure para o Entity Framework Core atualiza automaticamente o campo UpdatedAt para você. No entanto, se o banco de dados for atualizado fora do repositório, você deverá providenciar a atualização dos campos UpdatedAt e Version.

Azure SQL

Crie um gatilho para cada entidade:

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

Você pode instalar esse gatilho usando uma migração ou imediatamente após EnsureCreated() para criar o banco de dados.

Azure Cosmos DB

O Azure Cosmos DB é um banco de dados NoSQL totalmente gerenciado para aplicativos de alto desempenho de qualquer tamanho ou escala. Consulte do Provedor do Azure Cosmos DB para obter informações sobre como usar o Azure Cosmos DB com o Entity Framework Core. Ao usar o Azure Cosmos DB com Aplicativos Móveis do Azure:

  1. Configure o Contêiner Cosmos com um índice composto que especifique os campos UpdatedAt e Id. Os índices compostos podem ser adicionados a um contêiner por meio do portal do Azure, ARM, Bicep, Terraform ou dentro do código. Aqui está um exemplo definição de recurso bíceps:

    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 você puxar um subconjunto de itens na tabela, certifique-se de especificar todas as propriedades envolvidas na consulta.

  2. Derive modelos da classe ETagEntityTableData:

    public class TodoItem : ETagEntityTableData
    {
        public string Title { get; set; }
        public bool Completed { get; set; }
    }
    
  3. Adicione um método OnModelCreating(ModelBuilder) ao DbContext. O driver do Cosmos DB para Entity Framework coloca todas as entidades no mesmo contêiner por padrão. No mínimo, você deve escolher uma chave de partição adequada e garantir que a propriedade EntityTag esteja marcada como a tag de simultaneidade. Por exemplo, o trecho a seguir armazena as entidades TodoItem em seu próprio contêiner com as configurações apropriadas para os Aplicativos Móveis do 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);
    }
    

O Azure Cosmos DB tem suporte no pacote NuGet Microsoft.AspNetCore.Datasync.EFCore desde a v5.0.11. Para obter mais informações, consulte os seguintes links:

PostgreSQL

Crie um gatilho para cada entidade:

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

Você pode instalar esse gatilho usando uma migração ou imediatamente após EnsureCreated() para criar o banco de dados.

SqLite

Advertência

Não use o SqLite para serviços de produção. O SqLite só é adequado para uso do lado do cliente na produção.

O SqLite não tem um campo de data/hora que suporte precisão de milissegundos. Como tal, não é adequado para nada, exceto para testes. Se você deseja usar o SqLite, certifique-se de implementar um conversor de valor e um comparador de valor em cada modelo para propriedades de data/hora. O método mais fácil para implementar conversores e comparadores de valor está no método OnModelCreating(ModelBuilder) do seu 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);
}

Instale um gatilho de atualização ao inicializar o banco de dados:

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

Certifique-se de que o método InstallUpdateTriggers seja chamado apenas uma vez durante a inicialização do banco de dados:

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

LiteDB

LiteDB é um banco de dados sem servidor entregue em uma única pequena DLL escrita em código gerenciado .NET C#. É uma solução de banco de dados NoSQL simples e rápida para aplicativos autônomos. Para usar o LiteDb com armazenamento persistente em disco:

  1. Instale o pacote Microsoft.AspNetCore.Datasync.LiteDb do NuGet.

  2. Adicione um singleton para o LiteDatabase ao Program.cs:

    const connectionString = builder.Configuration.GetValue<string>("LiteDb:ConnectionString");
    builder.Services.AddSingleton<LiteDatabase>(new LiteDatabase(connectionString));
    
  3. Derivar modelos do LiteDbTableData:

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

    Você pode usar qualquer um dos atributos BsonMapper fornecidos com o pacote NuGet LiteDb.

  4. Crie um controlador usando o LiteDbRepository:

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

Suporte OpenAPI

Você pode publicar a API definida por controladores de sincronização de dados usando NSwag ou Swashbuckle. Em ambos os casos, comece por configurar o serviço como faria normalmente para a biblioteca escolhida.

NSwag

Siga as instruções básicas para integração com o NSwag e modifique da seguinte maneira:

  1. Adicione pacotes ao seu projeto para oferecer suporte ao NSwag. Os seguintes pacotes são necessários:

  2. Adicione o seguinte à parte superior do ficheiro Program.cs:

    using Microsoft.AspNetCore.Datasync.NSwag;
    
  3. Adicione um serviço para gerar uma definição OpenAPI ao seu arquivo Program.cs:

    builder.Services.AddOpenApiDocument(options =>
    {
        options.AddDatasyncProcessors();
    });
    
  4. Habilite o middleware para servir o documento JSON gerado e a interface do usuário do Swagger, também em Program.cs:

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

Navegar até o ponto de extremidade /swagger do serviço Web permite que você navegue pela API. A definição de OpenAPI pode ser importada para outros serviços (como o Gerenciamento de API do Azure). Para obter mais informações sobre como configurar o NSwag, consulte Introdução ao NSwag e ao ASP.NET Core.

Swashbuckle

Siga as instruções básicas para a integração do Swashbuckle e modifique da seguinte forma:

  1. Adicione pacotes ao seu projeto para suportar o Swashbuckle. Os seguintes pacotes são necessários:

  2. Adicione um serviço para gerar uma definição OpenAPI ao seu arquivo Program.cs:

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

    Observação

    O método AddDatasyncControllers() usa um Assembly opcional que corresponde ao assembly que contém os controladores de tabela. O parâmetro Assembly só é necessário se os controladores de tabela estiverem em um projeto diferente do serviço.

  3. Habilite o middleware para servir o documento JSON gerado e a interface do usuário do Swagger, também em Program.cs:

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

Com essa configuração, a navegação até a raiz do serviço Web permite que você navegue pela API. A definição de OpenAPI pode ser importada para outros serviços (como o Gerenciamento de API do Azure). Para obter mais informações sobre como configurar o Swashbuckle, consulte Introdução ao Swashbuckle e ao ASP.NET Core.

Limitações

A edição ASP.NET Core das bibliotecas de serviço implementa OData v4 para a operação de lista. Quando o servidor está sendo executado no modo de compatibilidade com versões anteriores, a filtragem em uma substring não é suportada.