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用に提供されます。
特定のデータベースのサポートについては、次のセクションを参照してください。
- Azure SQL と SQL Server の
- Azure Cosmos DB の
- PostgreSQL
- SqLite
- LiteDb
新しいデータ同期サーバーを作成する
データ同期サーバーは、通常の ASP.NET Core メカニズムを使用してサーバーを作成します。 これは、次の 3 つの手順で構成されます。
- 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 が使用されます。 テーブル コントローラーの作成は、次の 3 つの手順で行います。
- データ モデルのモデル クラスを作成します。
- アプリケーションの
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) は、1 つのページで返されるクエリ操作のアイテムの最大数です。 -
MaxTop
(int
、既定値: 512000) は、ページングなしでクエリ操作で返される項目の最大数です。 -
EnableSoftDelete
(bool
、既定値: false) を使用すると、論理的な削除が有効になり、アイテムがデータベースから削除されるのではなく削除済みとしてマークされます。 論理的な削除を使用すると、クライアントはオフライン キャッシュを更新できますが、削除された項目はデータベースから個別に削除する必要があります。 -
UnauthorizedStatusCode
(int
、既定値: 401 Unauthorized) は、ユーザーがアクションを実行できない場合に返される状態コードです。
アクセス許可を構成する
既定では、ユーザーはテーブル内のエンティティに対して任意の操作 (レコードの作成、読み取り、更新、削除) を実行できます。 承認をより細かく制御するために、IAccessControlProvider
を実装するクラスを作成します。
IAccessControlProvider
では、次の 3 つの方法を使用して承認を実装します。
-
GetDataView()
は、接続されているユーザーに表示される内容を制限するラムダを返します。 -
IsAuthorizedAsync()
は、接続されているユーザーが、要求されている特定のエンティティに対してアクションを実行できるかどうかを判断します。 -
PreCommitHookAsync()
は、リポジトリに書き込まれる直前にエンティティを調整します。
3 つの方法の間で、ほとんどのアクセス制御ケースを効果的に処理できます。
たとえば、次の例では、ユーザーが自分のレコードのみを表示できる個人用テーブルを実装しています。
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>
インターフェイスはコントローラーに実装できますが、スレッド セーフな方法で HttpContext
にアクセスするには、IHttpContextAccessor
を渡す必要があります。
このアクセス制御プロバイダーを使用するには、次のように 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);
}
}
テーブルへの認証されていないアクセスと認証されたアクセスの両方を許可する場合は、[Authorize]
ではなく [AllowAnonymous]
で装飾します。
ログ記録の構成
ログ記録は、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;
}
}
リポジトリの変更を監視する
リポジトリが変更されると、ワークフローのトリガー、クライアントへの応答のログ記録、または次の 2 つの方法のいずれかで他の作業を実行できます。
オプション 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 App Service ID を有効にする
ASP.NET Core データ同期サーバーは、コア IDProgram.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 Mobile Apps リポジトリでは、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 データベースです。 Entity Framework Core での Azure Cosmos DB の使用については、Azure Cosmos DB プロバイダーの の
UpdatedAt
フィールドとId
フィールドを指定する複合インデックスを使用して Cosmos コンテナーを設定します。 複合インデックスは、Azure portal、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 Mobile Apps の適切な設定を使用して、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); }
Azure Cosmos DB は、v5.0.11 以降の Microsoft.AspNetCore.Datasync.EFCore
NuGet パッケージでサポートされています。 詳細については、次のリンクを参照してください。
- cosmos DB サンプルを
します。 - EF Core Azure Cosmos DB プロバイダー ドキュメントを
します。 - 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
メソッドがデータベースの初期化中に 1 回だけ呼び出されるようにします。
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
パッケージをインストールします。Program.cs
にLiteDatabase
のシングルトンを追加します。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
NSwag 統合の基本的な手順に従い、次のように変更します。
NSwag をサポートするパッケージをプロジェクトに追加します。 次のパッケージが必要です。
- NSwag.AspNetCoreを
します。 - microsoft.AspNetCore.Datasync.NSwagを
します。
- NSwag.AspNetCoreを
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 Management など) にインポートできます。 NSwag の構成の詳細については、「NSwag と ASP.NET Coreの概要」を参照してください。
Swashbuckle
Swashbuckle 統合の基本的な手順に従い、次のように変更します。
Swashbuckle をサポートするパッケージをプロジェクトに追加します。 次のパッケージが必要です。
- Swashbuckle.AspNetCoreを
します。 - Swashbuckle.AspNetCore.Newtonsoft.
- microsoft.AspNetCore.Datasync.Swashbuckleを
します。
- Swashbuckle.AspNetCoreを
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 Management など) にインポートできます。 Swashbuckle の構成の詳細については、「Swashbuckle と ASP.NET Coreの概要」を参照してください。
制限
サービス ライブラリの ASP.NET Core エディションでは、リスト操作用の OData v4 が実装されます。 サーバーが下位互換性モードで実行されている場合、部分文字列のフィルター処理はサポートされていません。