Compartilhar via


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

Nota

Este produto está desativado. Para obter uma substituição para projetos que usam o .NET 8 ou posterior, consulte a biblioteca datasync do Kit de Ferramentas da Comunidade .

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

Plataformas com suporte

O servidor de back-end do ASP.NET Core dá suporte a ASP.NET 6.0 ou posterior.

Os servidores de banco de dados devem atender aos critérios a seguir com um campo de tipo DateTime ou Timestamp armazenado com precisão de milissegundos. As implementações do 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. Ele consiste em três etapas:

  1. Crie um projeto de servidor ASP.NET 6.0 (ou posterior).
  2. Adicionar o Entity Framework Core
  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 tutorial.

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

  • Microsoft.AspNetCore.Datasync
  • Microsoft.AspNetCore.Datasync.EFCore para tabelas baseadas no Entity Framework Core.
  • Microsoft.AspNetCore.Datasync.InMemory para tabelas na memória.

Modifique o arquivo de Program.cs. Adicione a seguinte linha em 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 à DbContext do 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 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; }
}

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

  • UpdatedAt (DateTimeOffset?) fornece a data em que o registro foi atualizado pela última vez.
  • Version (byte[]) fornece um valor opaco que muda em cada gravação.
  • Deleted (bool) será verdadeiro se o registro estiver marcado para exclusão, mas ainda não estiver 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 no banco de dados deve ser registrado 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 tabela é 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);
    }
}

Nota

  • 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 clientes anteriores à v5.0.0, a tabela deverá ser um subcaminho de /tables.
  • O controlador deve herdar de TableController<T>, em que <T> é uma implementação da implementação de ITableData para o tipo de repositório.
  • Atribua um repositório com base no mesmo tipo que o 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 maneira:

[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 determinados aspectos 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, padrão: false) permite a exclusão reversível, que marca os itens como excluídos em vez de excluí-los do banco de dados. A exclusão reversível permite que os clientes atualizem seu cache offline, mas exige que os itens excluídos sejam limpos 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 para entidades 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 de acesso ao HttpContext, configurar umHttpContextAccessor.

Como exemplo, o seguinte implementa uma tabela pessoal, em que 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 caso você precise fazer uma pesquisa extra de banco de dados para obter a resposta correta. Você pode implementar a interface IAccessControlProvider<T> no controlador, mas ainda precisa passar o IHttpContextAccessor para acessar o HttpContext de maneira segura de thread.

Para usar esse provedor de controle de acesso, atualize o 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 você quiser permitir acesso não autenticado e autenticado a uma tabela, decore-o com [AllowAnonymous] em vez de [Authorize].

Configurar o registro em log

O registro em log é tratado por meio o mecanismo de registro em log 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 no 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 Th PostCommitHookAsync() é chamado depois que os dados são gravados no repositório, mas antes de retornar os dados para o cliente. É necessário ter cuidado para garantir que os dados 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 essa opção se você 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 do ASP.NET Core dá suporte a ASP.NET Core Identityou a qualquer outro esquema de autenticação e autorização que você deseja dar suporte. Para ajudar com atualizações de versões anteriores dos Aplicativos Móveis do Azure, também fornecemos um provedor de identidade que implementa de Identidade do Serviço de Aplicativo do Azure. Para configurar a Identidade do Serviço de Aplicativo do Azure em seu aplicativo, edite seu 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 ao banco de dados

O Entity Framework Core não configura a geração de valor para colunas de data/hora. (Consulte de 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á organizar 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 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 os Aplicativos Móveis do Azure:

  1. Configure o Contêiner do Cosmos com um índice composto que especifica os campos UpdatedAt e Id. Índices compostos podem ser adicionados a um contêiner por meio do portal do Azure, ARM, Bicep, Terraform ou dentro do código. Veja um exemplo definição de recurso do 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 você efetuar pull de um subconjunto de itens na tabela, especifique todas as propriedades envolvidas na consulta.

  2. Derivar 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 o 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 seja marcada como a marca de simultaneidade. Por exemplo, o snippet 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 Microsoft.AspNetCore.Datasync.EFCore pacote NuGet desde v5.0.11. Para obter mais informações, examine 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

Aviso

Não use 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 dê suporte à precisão de milissegundos. Dessa forma, ele não é adequado para nada, exceto para testes. Se você quiser 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 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);
        }
    }
}

Verifique se o método InstallUpdateTriggers só é chamado 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 DLL pequena escrita no código gerenciado do .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 no 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. Criar 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 ao OpenAPI

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

NSwag

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

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

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

    using Microsoft.AspNetCore.Datasync.NSwag;
    
  3. Adicione um serviço para gerar uma definição de OpenAPI ao 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 ASP.NET Core.

Swashbuckle

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

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

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

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

    Nota

    O método AddDatasyncControllers() usa um Assembly opcional que corresponde ao assembly que contém os controladores de tabela. O parâmetro Assembly só será 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, navegar 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 ASP.NET Core.

Limitações

A edição ASP.NET Core das bibliotecas de serviço implementa o OData v4 para a operação de lista. Quando o servidor está em execução no modo de compatibilidade com versões anteriores, não há suporte para filtragem em uma subcadeia de caracteres.