次の方法で共有


ASP.NET Core バックエンド サーバー SDK の使用方法

手記

この製品は提供終了です。 .NET 8 以降を使用するプロジェクトの代わりに、Community Toolkit Datasync ライブラリを参照してください。

この記事では、ASP.NET Core バックエンド サーバー SDK を構成して使用して、データ同期サーバーを生成する必要があることを示します。

サポートされているプラットフォーム

ASP.NET Core バックエンド サーバーは、ASP.NET 6.0 以降をサポートしています。

データベース サーバーは、ミリ秒の精度で格納される DateTime または Timestamp 型フィールドを持つ次の条件を満たしている必要があります。 リポジトリの実装は、Entity Framework CoreLiteDb用に提供されます。

特定のデータベースのサポートについては、次のセクションを参照してください。

新しいデータ同期サーバーを作成する

データ同期サーバーは、通常の ASP.NET Core メカニズムを使用してサーバーを作成します。 これは、次の 3 つの手順で構成されます。

  1. ASP.NET 6.0 (またはそれ以降) のサーバー プロジェクトを作成します。
  2. Entity Framework Core の追加
  3. データ同期サービスの追加

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 つの手順で行います。

  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 と、データ同期サービスを処理するための追加のプロパティを提供します。

  • 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 つの方法の間で、ほとんどのアクセス制御ケースを効果的に処理できます。 にアクセスする必要がある場合は、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> インターフェイスはコントローラーに実装できますが、スレッド セーフな方法で 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 データ同期サーバーは、コア IDASP.NET、またはサポートするその他の認証および承認スキームをサポートします。 以前のバージョンの Azure Mobile Apps からのアップグレードを支援するために、Azure App Service Identity実装する ID プロバイダーも提供しています。 アプリケーションで Azure App Service ID を構成するには、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 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 プロバイダーの の を参照してください。 Azure Mobile Apps で Azure Cosmos DB を使用する場合:

  1. 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'
                            }
                        ]
                    ]
                }
            }
        }
    }
    

    テーブル内の項目のサブセットをプルする場合は、クエリに関連するすべてのプロパティを指定してください。

  2. ETagEntityTableData クラスからモデルを派生させます。

    public class TodoItem : ETagEntityTableData
    {
        public string Title { get; set; }
        public bool Completed { get; set; }
    }
    
  3. DbContextOnModelCreating(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 を使用する場合は、日付/時刻プロパティの各モデルに値コンバーターと値比較子を実装してください。 値コンバーターと比較子を実装する最も簡単な方法は、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 メソッドがデータベースの初期化中に 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 を使用するには:

  1. NuGet から Microsoft.AspNetCore.Datasync.LiteDb パッケージをインストールします。

  2. Program.csLiteDatabase のシングルトンを追加します。

    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 のサポート

NSwag または Swashbuckleを使用して、データ同期コントローラーによって定義された API を発行できます。 どちらの場合も、最初に、選択したライブラリの場合と同じようにサービスを設定します。

NSwag

NSwag 統合の基本的な手順に従い、次のように変更します。

  1. NSwag をサポートするパッケージをプロジェクトに追加します。 次のパッケージが必要です。

    • NSwag.AspNetCoreを します。
    • microsoft.AspNetCore.Datasync.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 Management など) にインポートできます。 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 Management など) にインポートできます。 Swashbuckle の構成の詳細については、「Swashbuckle と ASP.NET Coreの概要」を参照してください。

制限

サービス ライブラリの ASP.NET Core エディションでは、リスト操作用の OData v4 が実装されます。 サーバーが下位互換性モードで実行されている場合、部分文字列のフィルター処理はサポートされていません。