如何使用 ASP.NET Core 后端服务器 SDK
注意
此产品已停用。 有关使用 .NET 8 或更高版本的项目的替换,请参阅 Community Toolkit Datasync 库。
本文介绍如何配置和使用 ASP.NET Core 后端服务器 SDK 生成数据同步服务器。
支持的平台
ASP.NET Core 后端服务器支持 ASP.NET 6.0 或更高版本。
数据库服务器必须满足以下条件,DateTime
或 Timestamp
类型字段,该字段存储的精度为毫秒。 存储库实现适用于 Entity Framework Core 和 LiteDb。
有关特定数据库支持,请参阅以下部分:
创建新的数据同步服务器
数据同步服务器使用正常的 ASP.NET 核心机制来创建服务器。 它由三个步骤组成:
- 创建 ASP.NET 6.0(或更高版本)服务器项目。
- 添加 Entity Framework Core
- 添加数据同步服务
有关使用 Entity Framework Core 创建 ASP.NET Core 服务的信息,请参阅教程
若要启用数据同步服务,需要添加以下 NuGet 库:
- Microsoft.AspNetCore.Datasync
- 基于 Entity Framework Core 表的 Microsoft.AspNetCore.Datasync.EFCore。
- Microsoft.AspNetCore.Datasync.InMemory 内存中表。
修改 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。 创建表控制器的过程有三个步骤:
- 为数据模型创建模型类。
- 将模型类添加到应用程序的
DbContext
。 - 创建新的
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,以及用于处理数据同步服务的额外属性:
-
UpdatedAt
(DateTimeOffset?
)提供上次更新记录的日期。 -
Version
(byte[]
)提供一个不透明的值,用于更改每次写入。 - 如果记录标记为要删除但尚未清除,则
Deleted
(bool
)为 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 };
}
}
可以设置的选项包括:
-
PageSize
(int
,默认值:100)是查询操作在单个页面中返回的最大项数。 -
MaxTop
(int
,默认值:512000)是在查询操作中返回的最大项数,而不进行分页。 -
EnableSoftDelete
(bool
,默认值:false)启用软删除,这会将项标记为已删除,而不是从数据库中删除它们。 软删除允许客户端更新其脱机缓存,但要求从数据库单独清除已删除的项。 -
UnauthorizedStatusCode
(int
,默认值: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
字段。 但是,如果数据库在存储库外部更新,则必须安排要更新 UpdatedAt
和 Version
字段。
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 移动应用配合使用时:
使用复合索引设置 Cosmos 容器,该索引指定
UpdatedAt
和Id
字段。 可以通过 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' } ] ] } } } }
如果拉取表中的项子集,请确保指定查询中涉及的所有属性。
从
ETagEntityTableData
类派生模型:public class TodoItem : ETagEntityTableData { public string Title { get; set; } public bool Completed { get; set; } }
向
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,请确保在日期/时间属性的每个模型中实现值转换器和值比较器。 实现值转换器和比较器的最简单方法是 DbContext
的 OnModelCreating(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 与磁盘上的持久存储配合使用,请执行以下操作:
从 NuGet 安装
Microsoft.AspNetCore.Datasync.LiteDb
包。将
LiteDatabase
的单一实例添加到Program.cs
:const connectionString = builder.Configuration.GetValue<string>("LiteDb:ConnectionString"); builder.Services.AddSingleton<LiteDatabase>(new LiteDatabase(connectionString));
从
LiteDbTableData
派生模型:public class TodoItem : LiteDbTableData { public string Title { get; set; } public bool Completed { get; set; } }
可以使用随 LiteDb NuGet 包一起提供的任何
BsonMapper
属性。使用
LiteDbRepository
创建控制器:[Route("tables/[controller]")] public class TodoItemController : TableController<TodoItem> { public TodoItemController(LiteDatabase db) : base() { Repository = new LiteDbRepository<TodoItem>(db, "todoitems"); } }
OpenAPI 支持
可以使用 NSwag 或 Swashbuckle发布数据同步控制器定义的 API。 在这两种情况下,首先设置服务,就像你通常为所选库一样。
NSwag
按照 NSwag 集成的基本说明进行操作,然后按如下所示进行修改:
将包添加到项目以支持 NSwag。 需要以下包:
将以下内容添加到
Program.cs
文件的顶部:using Microsoft.AspNetCore.Datasync.NSwag;
向
Program.cs
文件添加服务以生成 OpenAPI 定义:builder.Services.AddOpenApiDocument(options => { options.AddDatasyncProcessors(); });
启用中间件以提供生成的 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 集成的基本说明进行操作,然后按如下所示进行修改:
将包添加到项目以支持 Swashbuckle。 需要以下包:
向
Program.cs
文件添加服务以生成 OpenAPI 定义:builder.Services.AddSwaggerGen(options => { options.AddDatasyncControllers(); }); builder.Services.AddSwaggerGenNewtonsoftSupport();
注意
AddDatasyncControllers()
方法采用与包含表控制器的程序集对应的可选Assembly
。 仅当表控制器位于服务的不同项目中时,才需要Assembly
参数。启用中间件以提供生成的 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。 在向后兼容模式下运行服务器时,不支持对子字符串进行筛选。