Uso del SDK del servidor back-end de ASP.NET Core
Nota
Este producto se retira. Para obtener un reemplazo de proyectos con .NET 8 o posterior, consulte la biblioteca datasync de Community Toolkit.
En este artículo se muestra que tiene que configurar y usar el SDK del servidor back-end de ASP.NET Core para generar un servidor de sincronización de datos.
Plataformas admitidas
El servidor back-end de ASP.NET Core admite ASP.NET 6.0 o posterior.
Los servidores de bases de datos deben cumplir los siguientes criterios tienen un campo de tipo DateTime
o Timestamp
que se almacena con precisión de milisegundos. Las implementaciones de repositorio se proporcionan para Entity Framework Core y LiteDb.
Para obtener compatibilidad específica con bases de datos, consulte las secciones siguientes:
- Azure SQL y SQL Server
- de Azure Cosmos DB
- postgreSQL
- sqLite
- LiteDb
Creación de un nuevo servidor de sincronización de datos
Un servidor de sincronización de datos usa los mecanismos normales de ASP.NET Core para crear el servidor. Consta de tres pasos:
- Cree un proyecto de servidor de ASP.NET 6.0 (o posterior).
- Adición de Entity Framework Core
- Agregar servicios de sincronización de datos
Para obtener información sobre cómo crear un servicio de ASP.NET Core con Entity Framework Core, consulte el tutorial.
Para habilitar los servicios de sincronización de datos, debe agregar las siguientes bibliotecas de NuGet:
- Microsoft.AspNetCore.Datasync
- microsoft.AspNetCore.Datasync.EFCore para tablas basadas en Entity Framework Core.
- microsoft.AspNetCore.Datasync.InMemory para tablas en memoria.
Modifique el archivo Program.cs
. Agregue la siguiente línea en todas las demás definiciones de servicio:
builder.Services.AddDatasyncControllers();
También puede usar la plantilla datasync-server
de ASP.NET Core:
# 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
La plantilla incluye un modelo de ejemplo y un controlador.
Creación de un controlador de tabla para una tabla SQL
El repositorio predeterminado usa Entity Framework Core. La creación de un controlador de tabla es un proceso de tres pasos:
- Cree una clase de modelo para el modelo de datos.
- Agregue la clase de modelo al
DbContext
de la aplicación. - Cree una nueva clase
TableController<T>
para exponer el modelo.
Creación de una clase de modelo
Todas las clases de modelo deben implementar ITableData
. Cada tipo de repositorio tiene una clase abstracta que implementa ITableData
. El repositorio de 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; }
}
La interfaz ITableData
proporciona el identificador del registro, junto con propiedades adicionales para controlar los servicios de sincronización de datos:
-
UpdatedAt
(DateTimeOffset?
) proporciona la fecha en que se actualizó por última vez el registro. -
Version
(byte[]
) proporciona un valor opaco que cambia en cada escritura. -
Deleted
(bool
) es true si el registro está marcado para su eliminación, pero aún no se ha purgado.
La biblioteca de sincronización de datos mantiene estas propiedades. No modifique estas propiedades en su propio código.
Actualizar el DbContext
Cada modelo de la base de datos debe registrarse en el DbContext
. Por ejemplo:
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public DbSet<TodoItem> TodoItems { get; set; }
}
Creación de un controlador de tabla
Un controlador de tabla es un ApiController
especializado. Este es un controlador de tabla mínimo:
[Route("tables/[controller]")]
public class TodoItemController : TableController<TodoItem>
{
public TodoItemController(AppDbContext context) : base()
{
Repository = new EntityTableRepository<TodoItem>(context);
}
}
Nota
- El controlador debe tener una ruta. Por convención, las tablas se exponen en una subruta de
/tables
, pero se pueden colocar en cualquier lugar. Si usa bibliotecas cliente anteriores a v5.0.0, la tabla debe ser una subruta de/tables
. - El controlador debe heredar de
TableController<T>
, donde<T>
es una implementación de la implementación deITableData
para el tipo de repositorio. - Asigne un repositorio basado en el mismo tipo que el modelo.
Implementación de un repositorio en memoria
También puede usar un repositorio en memoria sin almacenamiento persistente. Agregue un servicio singleton para el repositorio en el Program.cs
:
IEnumerable<Model> seedData = GenerateSeedData();
builder.Services.AddSingleton<IRepository<Model>>(new InMemoryRepository<Model>(seedData));
Configure el controlador de tabla de la siguiente manera:
[Route("tables/[controller]")]
public class ModelController : TableController<Model>
{
public MovieController(IRepository<Model> repository) : base(repository)
{
}
}
Configuración de las opciones del controlador de tabla
Puede configurar determinados aspectos del controlador mediante TableControllerOptions
:
[Route("tables/[controller]")]
public class MoodelController : TableController<Model>
{
public ModelController(IRepository<Model> repository) : base(repository)
{
Options = new TableControllerOptions { PageSize = 25 };
}
}
Las opciones que puede establecer incluyen:
-
PageSize
(int
, valor predeterminado: 100) es el número máximo de elementos que devuelve una operación de consulta en una sola página. -
MaxTop
(int
, valor predeterminado: 512000) es el número máximo de elementos devueltos en una operación de consulta sin paginación. -
EnableSoftDelete
(bool
, valor predeterminado: false) habilita la eliminación temporal, que marca los elementos como eliminados en lugar de eliminarlos de la base de datos. La eliminación temporal permite a los clientes actualizar su caché sin conexión, pero requiere que los elementos eliminados se purguen de la base de datos por separado. -
UnauthorizedStatusCode
(int
, valor predeterminado: 401 No autorizado) es el código de estado devuelto cuando el usuario no puede realizar una acción.
Configuración de permisos de acceso
De forma predeterminada, un usuario puede hacer todo lo que desee para las entidades dentro de una tabla: crear, leer, actualizar y eliminar cualquier registro. Para un control más específico sobre la autorización, cree una clase que implemente IAccessControlProvider
. El IAccessControlProvider
usa tres métodos para implementar la autorización:
-
GetDataView()
devuelve una expresión lambda que limita lo que puede ver el usuario conectado. -
IsAuthorizedAsync()
determina si el usuario conectado puede realizar la acción en la entidad específica que se solicita. -
PreCommitHookAsync()
ajusta cualquier entidad inmediatamente antes de escribirse en el repositorio.
Entre los tres métodos, puede controlar eficazmente la mayoría de los casos de control de acceso. Si necesita acceso a la HttpContext
, configure unHttpContextAccessor .
Por ejemplo, lo siguiente implementa una tabla personal, donde un usuario solo puede ver sus propios 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;
}
}
Los métodos son asincrónicos en caso de que necesite realizar una búsqueda de base de datos adicional para obtener la respuesta correcta. Puede implementar la interfaz de IAccessControlProvider<T>
en el controlador, pero todavía tiene que pasar el IHttpContextAccessor
para acceder a la HttpContext
de una manera segura para subprocesos.
Para usar este proveedor de control de acceso, actualice el TableController
de la siguiente manera:
[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 desea permitir el acceso no autenticado y autenticado a una tabla, decora con [AllowAnonymous]
en lugar de [Authorize]
.
Configuración del registro
El registro se controla a través de el mecanismo de registro normal para ASP.NET Core. Asigne el objeto ILogger
a la propiedad 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;
}
}
Supervisión de los cambios del repositorio
Cuando se cambia el repositorio, puede desencadenar flujos de trabajo, registrar la respuesta al cliente o realizar otro trabajo en uno de estos dos métodos:
Opción 1: Implementar un PostCommitHookAsync
La interfaz IAccessControlProvider<T>
proporciona un método PostCommitHookAsync()
. Th PostCommitHookAsync()
método se llama después de escribir los datos en el repositorio, pero antes de devolver los datos al cliente. Se debe tener cuidado para asegurarse de que los datos que se devuelven al cliente no cambian en este 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 opción si ejecuta tareas asincrónicas como parte del enlace.
Opción 2: Usar el controlador de eventos RepositoryUpdated
La clase base TableController<T>
contiene un controlador de eventos al mismo tiempo que el 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
}
}
Habilitación de Azure App Service Identity
El servidor de sincronización de datos de ASP.NET Core admite ASP.NET Core Identity, o cualquier otro esquema de autenticación y autorización que desee admitir. Para ayudar con las actualizaciones de versiones anteriores de Azure Mobile Apps, también proporcionamos un proveedor de identidades que implementa Azure App Service Identity. Para configurar Azure App Service Identity en la aplicación, edite el Program.cs
:
builder.Services.AddAuthentication(AzureAppServiceAuthentication.AuthenticationScheme)
.AddAzureAppServiceAuthentication(options => options.ForceEnable = true);
// Then later, after you have created the app
app.UseAuthentication();
app.UseAuthorization();
Compatibilidad con bases de datos
Entity Framework Core no configura la generación de valores para las columnas de fecha y hora. (Consulte generación de valores de fecha y hora). El repositorio de Azure Mobile Apps para Entity Framework Core actualiza automáticamente el campo UpdatedAt
automáticamente. Sin embargo, si la base de datos se actualiza fuera del repositorio, debe organizar los campos UpdatedAt
y Version
que se actualizarán.
Azure SQL
Cree un desencadenador para cada entidad:
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
Puede instalar este desencadenador mediante una migración o inmediatamente después de EnsureCreated()
para crear la base de datos.
Azure Cosmos DB
Azure Cosmos DB es una base de datos NoSQL totalmente administrada para aplicaciones de alto rendimiento de cualquier tamaño o escala. Consulte proveedor de Azure Cosmos DB para obtener información sobre el uso de Azure Cosmos DB con Entity Framework Core. Al usar Azure Cosmos DB con Azure Mobile Apps:
Configure el contenedor de Cosmos con un índice compuesto que especifique los campos
UpdatedAt
yId
. Los índices compuestos se pueden agregar a un contenedor a través de Azure Portal, ARM, Bicep, Terraform o dentro del código. Este es un ejemplo definición de recursos 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 extrae un subconjunto de elementos de la tabla, asegúrese de especificar todas las propiedades implicadas en la consulta.
Derive modelos de la clase
ETagEntityTableData
:public class TodoItem : ETagEntityTableData { public string Title { get; set; } public bool Completed { get; set; } }
Agregue un método
OnModelCreating(ModelBuilder)
alDbContext
. El controlador de Cosmos DB para Entity Framework coloca todas las entidades en el mismo contenedor de forma predeterminada. Como mínimo, debe elegir una clave de partición adecuada y asegurarse de que la propiedadEntityTag
esté marcada como etiqueta de simultaneidad. Por ejemplo, el fragmento de código siguiente almacena las entidadesTodoItem
en su propio contenedor con la configuración adecuada para 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 se admite en el paquete NuGet de Microsoft.AspNetCore.Datasync.EFCore
desde v5.0.11. Para obtener más información, revise los vínculos siguientes:
- ejemplo de Cosmos DB.
- documentación de del proveedor de Azure Cosmos DB de EF Core.
- documentación de la directiva de índice de Cosmos DB.
PostgreSQL
Cree un desencadenador para cada entidad:
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();
Puede instalar este desencadenador mediante una migración o inmediatamente después de EnsureCreated()
para crear la base de datos.
SqLite
Advertencia
No use SqLite para los servicios de producción. SqLite solo es adecuado para el uso del lado cliente en producción.
SqLite no tiene un campo de fecha y hora que admita la precisión de milisegundos. Por lo tanto, no es adecuado para nada excepto para las pruebas. Si desea usar SqLite, asegúrese de implementar un convertidor de valores y un comparador de valores en cada modelo para las propiedades de fecha y hora. El método más sencillo para implementar convertidores de valores y comparadores está en el método OnModelCreating(ModelBuilder)
de su 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 un desencadenador de actualización al inicializar la base de datos:
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);
}
}
}
Asegúrese de que solo se llama al método InstallUpdateTriggers
una vez durante la inicialización de la base de datos:
public void InitializeDatabase(DbContext context)
{
bool created = context.Database.EnsureCreated();
if (created && context.Database.IsSqlite())
{
InstallUpdateTriggers(context);
}
context.Database.SaveChanges();
}
LiteDB
liteDB es una base de datos sin servidor que se entrega en un único archivo DLL pequeño escrito en código administrado de C# de .NET. Es una solución de base de datos NoSQL sencilla y rápida para aplicaciones independientes. Para usar LiteDb con almacenamiento persistente en disco:
Instale el paquete
Microsoft.AspNetCore.Datasync.LiteDb
desde NuGet.Agregue un singleton para el
LiteDatabase
alProgram.cs
:const connectionString = builder.Configuration.GetValue<string>("LiteDb:ConnectionString"); builder.Services.AddSingleton<LiteDatabase>(new LiteDatabase(connectionString));
Derive modelos de la
LiteDbTableData
:public class TodoItem : LiteDbTableData { public string Title { get; set; } public bool Completed { get; set; } }
Puede usar cualquiera de los atributos de
BsonMapper
que se proporcionan con el paquete NuGet LiteDb.Cree un controlador mediante el
LiteDbRepository
:[Route("tables/[controller]")] public class TodoItemController : TableController<TodoItem> { public TodoItemController(LiteDatabase db) : base() { Repository = new LiteDbRepository<TodoItem>(db, "todoitems"); } }
Compatibilidad con OpenAPI
Puede publicar la API definida por los controladores de sincronización de datos mediante NSwag o Swashbuckle. En ambos casos, empiece por configurar el servicio como lo haría normalmente para la biblioteca elegida.
NSwag
Siga las instrucciones básicas para la integración de NSwag y, a continuación, modifique como se indica a continuación:
Agregue paquetes al proyecto para admitir NSwag. Se requieren los siguientes paquetes:
Agregue lo siguiente a la parte superior del archivo de
Program.cs
:using Microsoft.AspNetCore.Datasync.NSwag;
Agregue un servicio para generar una definición de OpenAPI en el archivo
Program.cs
:builder.Services.AddOpenApiDocument(options => { options.AddDatasyncProcessors(); });
Habilite el middleware para atender el documento JSON generado y la interfaz de usuario de Swagger, también en
Program.cs
:if (app.Environment.IsDevelopment()) { app.UseOpenApi(); app.UseSwaggerUI3(); }
La exploración al punto de conexión de /swagger
del servicio web le permite examinar la API. La definición de OpenAPI se puede importar a otros servicios (como Azure API Management). Para obtener más información sobre cómo configurar NSwag, consulte Introducción a NSwag y ASP.NET Core.
Swashbuckle
Siga las instrucciones básicas para la integración de Swashbuckle y, a continuación, modifique como se indica a continuación:
Agregue paquetes al proyecto para admitir Swashbuckle. Se requieren los siguientes paquetes:
Agregue un servicio para generar una definición de OpenAPI en el archivo
Program.cs
:builder.Services.AddSwaggerGen(options => { options.AddDatasyncControllers(); }); builder.Services.AddSwaggerGenNewtonsoftSupport();
Nota
El método
AddDatasyncControllers()
toma unAssembly
opcional que corresponde al ensamblado que contiene los controladores de tabla. El parámetroAssembly
solo es necesario si los controladores de tabla están en un proyecto diferente al servicio.Habilite el middleware para atender el documento JSON generado y la interfaz de usuario de Swagger, también en
Program.cs
:if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1"); options.RoutePrefix = string.Empty; }); }
Con esta configuración, la exploración a la raíz del servicio web le permite examinar la API. La definición de OpenAPI se puede importar a otros servicios (como Azure API Management). Para obtener más información sobre cómo configurar Swashbuckle, consulte Introducción a Swashbuckle y ASP.NET Core.
Limitaciones
La edición ASP.NET Core de las bibliotecas de servicios implementa OData v4 para la operación de lista. Cuando el servidor se ejecuta en modo de compatibilidad con versiones anteriores, no se admite el filtrado en una subcadena.