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:
- o SQL do Azure e o SQL Server
- do Azure Cosmos DB
- postgreSQL
- do
SqLite - LiteDb
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:
- Crie um projeto de servidor ASP.NET 6.0 (ou posterior).
- Adicionar o Entity Framework Core
- 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:
- Crie uma classe de modelo para o modelo de dados.
- Adicione a classe de modelo à
DbContext
do aplicativo. - 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 ApiController
especializado. 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 deITableData
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:
Configure o Contêiner do Cosmos com um índice composto que especifica os campos
UpdatedAt
eId
. Í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.
Derivar modelos da classe
ETagEntityTableData
:public class TodoItem : ETagEntityTableData { public string Title { get; set; } public bool Completed { get; set; } }
Adicione um método
OnModelCreating(ModelBuilder)
aoDbContext
. 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 propriedadeEntityTag
seja marcada como a marca de simultaneidade. Por exemplo, o snippet a seguir armazena as entidadesTodoItem
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:
- de exemplo do Cosmos DB.
- documentação do provedor do Azure Cosmos DB do EF Core.
- documentação da política de índice do Cosmos DB.
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:
Instale o pacote
Microsoft.AspNetCore.Datasync.LiteDb
do NuGet.Adicione um singleton para o
LiteDatabase
aoProgram.cs
:const connectionString = builder.Configuration.GetValue<string>("LiteDb:ConnectionString"); builder.Services.AddSingleton<LiteDatabase>(new LiteDatabase(connectionString));
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.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:
Adicione pacotes ao seu projeto para dar suporte ao NSwag. Os seguintes pacotes são necessários:
- NSwag.AspNetCore .
- Microsoft.AspNetCore.Datasync.NSwag.
Adicione o seguinte à parte superior do arquivo
Program.cs
:using Microsoft.AspNetCore.Datasync.NSwag;
Adicione um serviço para gerar uma definição de OpenAPI ao arquivo
Program.cs
:builder.Services.AddOpenApiDocument(options => { options.AddDatasyncProcessors(); });
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:
Adicione pacotes ao projeto para dar suporte ao Swashbuckle. Os seguintes pacotes são necessários:
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 umAssembly
opcional que corresponde ao assembly que contém os controladores de tabela. O parâmetroAssembly
só será necessário se os controladores de tabela estiverem em um projeto diferente do serviço.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.