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

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

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

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

データベース サーバーは、ミリ秒精度で保存される DateTime 型か Timestamp 型のフィールドを持つ次の条件を満たす必要があります。 リポジトリの実装は、Entity Framework Core および LiteDb 用に提供されます。

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

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

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

  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 が使用されます。 テーブル コントローラーの作成は、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);
    }
}

Note

  • コントローラーはルートを持っている必要があります。 慣例により、テーブルは /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 未承認) は、ユーザーがアクションの実行を許可されていない場合に返される状態コードです。

アクセス許可のコンフィギュレーション

既定では、ユーザーはテーブル内のエンティティに対して必要なあらゆる操作を実行できます。つまり、レコードの作成、読み取り、更新、削除を行うことができます。 承認をより細かく制御するには、IAccessControlProvider を実装するクラスを作成します。 IAccessControlProvider では、次の 3 つの方法で承認を実装します。

  • GetDataView() は、接続されているユーザーに表示される内容を制限するラムダを返します。
  • IsAuthorizedAsync() は、接続されているユーザーが、要求されている特定のエンティティに対してアクションを実行できるかどうかを判断します。
  • PreCommitHookAsync() は、リポジトリに書き込まれる直前に、エンティティを調整します。

この 3 つの方法の間では、ほとんどのアクセス制御ケースを効果的に処理できます。 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> インターフェイスを実装できますが、スレッド セーフな方法で 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 データ同期サーバーは、ASP.NET Core ID、またはサポートするその他の認証および承認スキームをサポートします。 以前のバージョンの Azure Mobile Apps からのアップグレードを支援するために、Azure App Service ID を実装する 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 更新されます。 ただし、データベースがリポジトリの外部で更新される場合は、更新するフィールドとVersionフィールドをUpdatedAt配置する必要があります。

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. OnModelCreating(ModelBuilder) メソッドを DbContext に追加します。 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 パッケージでサポートされています。 詳しくは、次のリンクをご覧ください。

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

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

NSwag

NSwag を統合する基本的な手順に従った後、次のように変更します。

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

  2. Program.cs ファイルの先頭に以下を追加します。

    using Microsoft.AspNetCore.Datasync.NSwag;
    
  3. Program.cs ファイルに、OpenAPI 定義を生成するサービスを追加します。

    builder.Services.AddOpenApiDocument(options =>
    {
        options.AddDatasyncProcessors();
    });
    
  4. 同じく Program.cs で、生成された JSON ドキュメントと Swagger UI を提供するミドルウェアを有効にします。

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

    Note

    AddDatasyncControllers() メソッドは、オプションのパラメーターとして、テーブル コントローラーが格納されたアセンブリに対応する Assembly を受け取ります。 Assembly パラメーターは、テーブル コントローラーがサービスとは異なるプロジェクトに含まれている場合にのみ必要です。

  3. 同じく Program.cs で、生成された JSON ドキュメントと Swagger UI を提供するミドルウェアを有効にします。

    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 が実装されています。 サーバーが下位互換性モードで実行されている場合、substring のフィルター処理はサポートされません。