Compartir a través de


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:

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:

  1. Cree un proyecto de servidor de ASP.NET 6.0 (o posterior).
  2. Adición de Entity Framework Core
  3. 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:

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:

  1. Cree una clase de modelo para el modelo de datos.
  2. Agregue la clase de modelo al DbContext de la aplicación.
  3. 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 ApiControllerespecializado. 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 de ITableData 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:

  1. Configure el contenedor de Cosmos con un índice compuesto que especifique los campos UpdatedAt y Id. 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.

  2. Derive modelos de la clase ETagEntityTableData:

    public class TodoItem : ETagEntityTableData
    {
        public string Title { get; set; }
        public bool Completed { get; set; }
    }
    
  3. Agregue un método OnModelCreating(ModelBuilder) al DbContext. 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 propiedad EntityTag esté marcada como etiqueta de simultaneidad. Por ejemplo, el fragmento de código siguiente almacena las entidades TodoItem 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:

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:

  1. Instale el paquete Microsoft.AspNetCore.Datasync.LiteDb desde NuGet.

  2. Agregue un singleton para el LiteDatabase al Program.cs:

    const connectionString = builder.Configuration.GetValue<string>("LiteDb:ConnectionString");
    builder.Services.AddSingleton<LiteDatabase>(new LiteDatabase(connectionString));
    
  3. 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.

  4. 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:

  1. Agregue paquetes al proyecto para admitir NSwag. Se requieren los siguientes paquetes:

  2. Agregue lo siguiente a la parte superior del archivo de Program.cs:

    using Microsoft.AspNetCore.Datasync.NSwag;
    
  3. Agregue un servicio para generar una definición de OpenAPI en el archivo Program.cs:

    builder.Services.AddOpenApiDocument(options =>
    {
        options.AddDatasyncProcessors();
    });
    
  4. 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:

  1. Agregue paquetes al proyecto para admitir Swashbuckle. Se requieren los siguientes paquetes:

  2. 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 un Assembly opcional que corresponde al ensamblado que contiene los controladores de tabla. El parámetro Assembly solo es necesario si los controladores de tabla están en un proyecto diferente al servicio.

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