如何使用 ASP.NET Core 后端服务器 SDK

注意

此产品已停用。 有关使用 .NET 8 或更高版本的项目的替换,请参阅 Community Toolkit Datasync 库

本文介绍如何配置和使用 ASP.NET Core 后端服务器 SDK 生成数据同步服务器。

支持的平台

ASP.NET Core 后端服务器支持 ASP.NET 6.0 或更高版本。

数据库服务器必须满足以下条件,DateTimeTimestamp 类型字段,该字段存储的精度为毫秒。 存储库实现适用于 Entity Framework CoreLiteDb

有关特定数据库支持,请参阅以下部分:

创建新的数据同步服务器

数据同步服务器使用正常的 ASP.NET 核心机制来创建服务器。 它由三个步骤组成:

  1. 创建 ASP.NET 6.0(或更高版本)服务器项目。
  2. 添加 Entity Framework Core
  3. 添加数据同步服务

有关使用 Entity Framework Core 创建 ASP.NET Core 服务的信息,请参阅教程

若要启用数据同步服务,需要添加以下 NuGet 库:

修改 Program.cs 文件。 在所有其他服务定义下添加以下行:

builder.Services.AddDatasyncControllers();

还可以使用 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

该模板包括示例模型和控制器。

为 SQL 表创建表控制器

默认存储库使用 Entity Framework Core。 创建表控制器的过程有三个步骤:

  1. 为数据模型创建模型类。
  2. 将模型类添加到应用程序的 DbContext
  3. 创建新的 TableController<T> 类来公开模型。

创建模型类

所有模型类都必须实现 ITableData。 每个存储库类型都有一个实现 ITableData的抽象类。 Entity Framework Core 存储库使用 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; }
}

ITableData 接口提供记录的 ID,以及用于处理数据同步服务的额外属性:

  • UpdatedAtDateTimeOffset?)提供上次更新记录的日期。
  • Versionbyte[])提供一个不透明的值,用于更改每次写入。
  • 如果记录标记为要删除但尚未清除,则 Deletedbool)为 true。

数据同步库维护这些属性。 请勿在自己的代码中修改这些属性。

更新 DbContext

数据库中的每个模型都必须在 DbContext中注册。 例如:

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

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

创建表控制器

表控制器是专用 ApiController。 下面是最小表控制器:

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

注意

  • 控制器必须具有路由。 按照约定,表在 /tables的子路径上公开,但它们可以放置在任何位置。 如果使用的是早于 v5.0.0 的客户端库,则该表必须是 /tables的子路径。
  • 控制器必须继承自 TableController<T>,其中 <T> 是存储库类型的 ITableData 实现的实现。
  • 根据模型所在的同一类型分配存储库。

实现内存中存储库

还可以使用没有永久性存储的内存中存储库。 在 Program.cs中添加存储库的单一实例服务:

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

按如下所示设置表控制器:

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

配置表控制器选项

可以使用 TableControllerOptions配置控制器的某些方面:

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

可以设置的选项包括:

  • PageSizeint,默认值:100)是查询操作在单个页面中返回的最大项数。
  • MaxTopint,默认值:512000)是在查询操作中返回的最大项数,而不进行分页。
  • EnableSoftDeletebool,默认值:false)启用软删除,这会将项标记为已删除,而不是从数据库中删除它们。 软删除允许客户端更新其脱机缓存,但要求从数据库单独清除已删除的项。
  • UnauthorizedStatusCodeint,默认值:401 未授权)是不允许用户执行操作时返回的状态代码。

配置访问权限

默认情况下,用户可以对表中的实体执行任何操作 - 创建、读取、更新和删除任何记录。 若要更精细地控制授权,请创建实现 IAccessControlProvider的类。 IAccessControlProvider 使用三种方法来实现授权:

  • GetDataView() 返回一个 lambda,用于限制连接的用户可以看到的内容。
  • IsAuthorizedAsync() 确定已连接用户是否可以对所请求的特定实体执行操作。
  • PreCommitHookAsync() 在写入存储库之前立即调整任何实体。

在三种方法之间,可以有效地处理大多数访问控制案例。 如果需要访问 HttpContext配置 HttpContextAccessor

例如,以下示例实现个人表,其中用户只能查看自己的记录。

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

如果需要执行额外的数据库查找以获取正确的答案,则方法是异步的。 可以在控制器上实现 IAccessControlProvider<T> 接口,但仍需传入 IHttpContextAccessor 以线程安全方式访问 HttpContext

若要使用此访问控制提供程序,请更新 TableController,如下所示:

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

如果要允许未经身份验证和经过身份验证的访问表,请使用 [AllowAnonymous] 修饰它,而不是 [Authorize]

配置日志记录

日志记录通过 ASP.NET Core 的常规日志记录机制 进行处理。 将 ILogger 对象分配给 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;
    }
}

监视存储库更改

更改存储库后,可以触发工作流、将响应记录到客户端,或在以下两种方法之一中执行其他工作:

选项 1:实现 PostCommitHookAsync

IAccessControlProvider<T> 接口提供 PostCommitHookAsync() 方法。 在将数据写入存储库之后,但在将数据返回到客户端之前,将调用 PostCommitHookAsync() 方法。 必须注意以确保此方法中不会更改要返回到客户端的数据。

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

如果要作为挂钩的一部分运行异步任务,请使用此选项。

选项 2:使用 RepositoryUpdated 事件处理程序

TableController<T> 基类包含与 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
    }
}

启用 Azure 应用服务标识

ASP.NET 核心数据同步服务器支持 ASP.NET Core Identity,或者想要支持的任何其他身份验证和授权方案。 为了帮助升级早期版本的 Azure 移动应用,我们还提供一个标识提供者,用于实现 Azure 应用服务标识。 若要在应用程序中配置 Azure 应用服务标识,请编辑 Program.cs

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

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

数据库支持

Entity Framework Core 不会为日期/时间列设置值生成。 (请参阅 日期/时间值生成)。 Entity Framework Core 的 Azure 移动应用存储库会自动更新 UpdatedAt 字段。 但是,如果数据库在存储库外部更新,则必须安排要更新 UpdatedAtVersion 字段。

Azure SQL

为每个实体创建触发器:

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

可以使用迁移或 EnsureCreated() 后立即安装此触发器来创建数据库。

Azure Cosmos DB

Azure Cosmos DB 是一个完全托管的 NoSQL 数据库,适用于任何大小或规模的高性能应用程序。 有关将 Azure Cosmos DB 与 Entity Framework Core 配合使用的信息,请参阅 Azure Cosmos DB 提供程序。 将 Azure Cosmos DB 与 Azure 移动应用配合使用时:

  1. 使用复合索引设置 Cosmos 容器,该索引指定 UpdatedAtId 字段。 可以通过 Azure 门户、ARM、Bicep、Terraform 或代码将复合索引添加到容器。 下面是 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'
                            }
                        ]
                    ]
                }
            }
        }
    }
    

    如果拉取表中的项子集,请确保指定查询中涉及的所有属性。

  2. ETagEntityTableData 类派生模型:

    public class TodoItem : ETagEntityTableData
    {
        public string Title { get; set; }
        public bool Completed { get; set; }
    }
    
  3. DbContext添加 OnModelCreating(ModelBuilder) 方法。 默认情况下,Entity Framework 的 Cosmos DB 驱动程序将所有实体放入同一容器中。 至少必须选取适当的分区键,并确保 EntityTag 属性标记为并发标记。 例如,以下代码片段使用适用于 Azure 移动应用的相应设置将 TodoItem 实体存储在其自己的容器中:

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

自 v5.0.11 起,Microsoft.AspNetCore.Datasync.EFCore NuGet 包支持 Azure Cosmos DB。 有关详细信息,请查看以下链接:

PostgreSQL

为每个实体创建触发器:

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

可以使用迁移或 EnsureCreated() 后立即安装此触发器来创建数据库。

SqLite

警告

请勿将 SqLite 用于生产服务。 SqLite 仅适用于生产中的客户端使用情况。

SqLite 没有支持毫秒准确性的日期/时间字段。 因此,除了测试之外,它不适合任何内容。 如果要使用 SqLite,请确保在日期/时间属性的每个模型中实现值转换器和值比较器。 实现值转换器和比较器的最简单方法是 DbContextOnModelCreating(ModelBuilder) 方法:

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

初始化数据库时安装更新触发器:

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

确保在数据库初始化期间只调用 InstallUpdateTriggers 方法一次:

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

LiteDB

LiteDB 是在以 .NET C# 托管代码编写的单个小型 DLL 中传递的无服务器数据库。 它是适用于独立应用程序的简单且快速的 NoSQL 数据库解决方案。 若要将 LiteDb 与磁盘上的持久存储配合使用,请执行以下操作:

  1. 从 NuGet 安装 Microsoft.AspNetCore.Datasync.LiteDb 包。

  2. LiteDatabase 的单一实例添加到 Program.cs

    const connectionString = builder.Configuration.GetValue<string>("LiteDb:ConnectionString");
    builder.Services.AddSingleton<LiteDatabase>(new LiteDatabase(connectionString));
    
  3. LiteDbTableData派生模型:

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

    可以使用随 LiteDb NuGet 包一起提供的任何 BsonMapper 属性。

  4. 使用 LiteDbRepository创建控制器:

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

OpenAPI 支持

可以使用 NSwagSwashbuckle发布数据同步控制器定义的 API。 在这两种情况下,首先设置服务,就像你通常为所选库一样。

NSwag

按照 NSwag 集成的基本说明进行操作,然后按如下所示进行修改:

  1. 将包添加到项目以支持 NSwag。 需要以下包:

  2. 将以下内容添加到 Program.cs 文件的顶部:

    using Microsoft.AspNetCore.Datasync.NSwag;
    
  3. Program.cs 文件添加服务以生成 OpenAPI 定义:

    builder.Services.AddOpenApiDocument(options =>
    {
        options.AddDatasyncProcessors();
    });
    
  4. 启用中间件以提供生成的 JSON 文档和 Swagger UI,也可以在 Program.cs中:

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

通过浏览到 Web 服务的 /swagger 终结点,可以浏览 API。 然后,OpenAPI 定义可以导入到其他服务(如 Azure API 管理) 中。 有关配置 NSwag 的详细信息,请参阅 NSwag 入门和 ASP.NET Core

Swashbuckle

按照 Swashbuckle 集成的基本说明进行操作,然后按如下所示进行修改:

  1. 将包添加到项目以支持 Swashbuckle。 需要以下包:

  2. Program.cs 文件添加服务以生成 OpenAPI 定义:

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

    注意

    AddDatasyncControllers() 方法采用与包含表控制器的程序集对应的可选 Assembly。 仅当表控制器位于服务的不同项目中时,才需要 Assembly 参数。

  3. 启用中间件以提供生成的 JSON 文档和 Swagger UI,也可以在 Program.cs中:

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

使用此配置,浏览到 Web 服务的根目录可以浏览 API。 然后,OpenAPI 定义可以导入到其他服务(如 Azure API 管理) 中。 有关配置 Swashbuckle 的详细信息,请参阅 Swashbuckle 入门和 ASP.NET Core

局限性

服务库的 ASP.NET Core 版本为列表操作实现 OData v4。 在向后兼容模式下运行服务器时,不支持对子字符串进行筛选。