Bagikan melalui


Cara menggunakan SDK server backend ASP.NET Core

Nota

Produk ini dihentikan. Untuk pengganti proyek yang menggunakan .NET 8 atau yang lebih baru, lihat pustaka Community Toolkit Datasync.

Artikel ini menunjukkan Anda harus mengonfigurasi dan menggunakan SDK server backend ASP.NET Core untuk menghasilkan server sinkronisasi data.

Platform yang didukung

Server backend ASP.NET Core mendukung ASP.NET 6.0 atau yang lebih baru.

Server database harus memenuhi kriteria berikut memiliki bidang jenis DateTime atau Timestamp yang disimpan dengan akurasi milidetik. Implementasi repositori disediakan untuk Inti Kerangka Kerja Entitas dan LiteDb.

Untuk dukungan database tertentu, lihat bagian berikut ini:

Membuat server sinkronisasi data baru

Server sinkronisasi data menggunakan mekanisme normal ASP.NET Core untuk membuat server. Ini terdiri dari tiga langkah:

  1. Buat proyek server ASP.NET 6.0 (atau yang lebih baru).
  2. Menambahkan Entity Framework Core
  3. Menambahkan Layanan sinkronisasi data

Untuk informasi tentang membuat layanan ASP.NET Core dengan Entity Framework Core, lihat tutorial.

Untuk mengaktifkan layanan sinkronisasi data, Anda perlu menambahkan pustaka NuGet berikut:

Ubah file Program.cs. Tambahkan baris berikut di bawah semua definisi layanan lainnya:

builder.Services.AddDatasyncControllers();

Anda juga dapat menggunakan templat datasync-server ASP.NET Core:

# 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

Templat mencakup model sampel dan pengontrol.

Membuat pengontrol tabel untuk tabel SQL

Repositori default menggunakan Entity Framework Core. Membuat pengontrol tabel adalah proses tiga langkah:

  1. Buat kelas model untuk model data.
  2. Tambahkan kelas model ke DbContext untuk aplikasi Anda.
  3. Buat kelas TableController<T> baru untuk mengekspos model Anda.

Membuat kelas model

Semua kelas model harus menerapkan ITableData. Setiap jenis repositori memiliki kelas abstrak yang mengimplementasikan ITableData. Repositori Entity Framework Core menggunakan 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; }
}

Antarmuka ITableData menyediakan ID rekaman, bersama dengan properti tambahan untuk menangani layanan sinkronisasi data:

  • UpdatedAt (DateTimeOffset?) menyediakan tanggal rekaman terakhir diperbarui.
  • Version (byte[]) memberikan nilai buram yang berubah pada setiap tulisan.
  • Deleted (bool) benar jika rekaman ditandai untuk dihapus tetapi belum dihapus menyeluruh.

Pustaka Sinkronisasi data mempertahankan properti ini. Jangan ubah properti ini dalam kode Anda sendiri.

Memperbarui DbContext

Setiap model dalam database harus terdaftar di DbContext. Misalnya:

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

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

Membuat pengontrol tabel

Pengontrol tabel adalah ApiControllerkhusus . Berikut adalah pengontrol tabel minimal:

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

Nota

  • Pengontrol harus memiliki rute. Menurut konvensi, tabel diekspos pada subpath /tables, tetapi dapat ditempatkan di mana saja. Jika Anda menggunakan pustaka klien yang lebih lama dari v5.0.0, maka tabel harus merupakan subjalur /tables.
  • Pengontrol harus mewarisi dari TableController<T>, di mana <T> adalah implementasi implementasi ITableData untuk jenis repositori Anda.
  • Tetapkan repositori berdasarkan jenis yang sama dengan model Anda.

Menerapkan repositori dalam memori

Anda juga dapat menggunakan repositori dalam memori tanpa penyimpanan persisten. Tambahkan layanan singleton untuk repositori di Program.csAnda:

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

Siapkan pengontrol tabel Anda sebagai berikut:

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

Mengonfigurasi opsi pengontrol tabel

Anda dapat mengonfigurasi aspek pengontrol tertentu menggunakan TableControllerOptions:

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

Opsi yang dapat Anda atur meliputi:

  • PageSize (int, default: 100) adalah jumlah maksimum item yang dikembalikan operasi kueri dalam satu halaman.
  • MaxTop (int, default: 512000) adalah jumlah maksimum item yang dikembalikan dalam operasi kueri tanpa penomoran halaman.
  • EnableSoftDelete (bool, default: false) memungkinkan penghapusan sementara, yang menandai item sebagai dihapus alih-alih menghapusnya dari database. Penghapusan sementara memungkinkan klien memperbarui cache offline mereka, tetapi mengharuskan item yang dihapus dihapus menyeluruh dari database secara terpisah.
  • UnauthorizedStatusCode (int, default: 401 Tidak Sah) adalah kode status yang dikembalikan ketika pengguna tidak diizinkan untuk melakukan tindakan.

Mengonfigurasi izin akses

Secara default, pengguna dapat melakukan apa pun yang ingin mereka entitaskan dalam tabel - membuat, membaca, memperbarui, dan menghapus rekaman apa pun. Untuk kontrol yang lebih halus atas otorisasi, buat kelas yang mengimplementasikan IAccessControlProvider. IAccessControlProvider menggunakan tiga metode untuk menerapkan otorisasi:

  • GetDataView() mengembalikan lambda yang membatasi apa yang dapat dilihat pengguna yang terhubung.
  • IsAuthorizedAsync() menentukan apakah pengguna yang terhubung dapat melakukan tindakan pada entitas tertentu yang diminta.
  • PreCommitHookAsync() menyesuaikan entitas apa pun segera sebelum ditulis ke repositori.

Di antara ketiga metode tersebut, Anda dapat menangani sebagian besar kasus kontrol akses secara efektif. Jika Anda memerlukan akses ke HttpContext, mengonfigurasi HttpContextAccessor.

Sebagai contoh, berikut ini mengimplementasikan tabel pribadi, di mana pengguna hanya dapat melihat rekaman mereka sendiri.

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

Metode ini asinkron jika Anda perlu melakukan pencarian database tambahan untuk mendapatkan jawaban yang benar. Anda dapat mengimplementasikan antarmuka IAccessControlProvider<T> pada pengontrol, tetapi Anda masih harus meneruskan IHttpContextAccessor untuk mengakses HttpContext dengan cara yang aman di utas.

Untuk menggunakan penyedia kontrol akses ini, perbarui TableController Anda sebagai berikut:

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

Jika Anda ingin mengizinkan akses yang tidak diautentikasi dan diautentikasi ke tabel, hiasi dengan [AllowAnonymous] alih-alih [Authorize].

Mengonfigurasi pengelogan

Pengelogan ditangani melalui mekanisme pengelogan normal untuk ASP.NET Core. Tetapkan objek ILogger ke properti 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;
    }
}

Memantau perubahan repositori

Saat repositori diubah, Anda dapat memicu alur kerja, mencatat respons ke klien, atau melakukan pekerjaan lain dalam salah satu dari dua metode:

Opsi 1: Menerapkan PostCommitHookAsync

Antarmuka IAccessControlProvider<T> menyediakan metode PostCommitHookAsync(). Metode Th PostCommitHookAsync() dipanggil setelah data ditulis ke repositori tetapi sebelum mengembalikan data ke klien. Perawatan harus dilakukan untuk memastikan bahwa data yang dikembalikan ke klien tidak diubah dalam metode ini.

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

Gunakan opsi ini jika Anda menjalankan tugas asinkron sebagai bagian dari hook.

Opsi 2: Gunakan penanganan aktivitas RepositoryUpdated

Kelas dasar TableController<T> berisi penanganan aktivitas yang dipanggil secara bersamaan dengan metode 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
    }
}

Mengaktifkan Identitas Azure App Service

Server sinkronisasi data ASP.NET Core mendukung ASP.NET Core Identity, atau skema autentikasi dan otorisasi lain yang ingin Anda dukung. Untuk membantu peningkatan dari versi Azure Mobile Apps sebelumnya, kami juga menyediakan idP yang menerapkan Azure App Service Identity. Untuk mengonfigurasi Azure App Service Identity di aplikasi Anda, edit Program.csAnda :

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

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

Dukungan Database

Entity Framework Core tidak menyiapkan pembuatan nilai untuk kolom tanggal/waktu. (Lihat pembuatan nilai tanggal/waktu). Repositori Azure Mobile Apps untuk Entity Framework Core secara otomatis memperbarui bidang UpdatedAt untuk Anda. Namun, jika database Anda diperbarui di luar repositori, Anda harus mengatur bidang UpdatedAt dan Version untuk diperbarui.

Azure SQL

Buat pemicu untuk setiap entitas:

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

Anda dapat menginstal pemicu ini menggunakan migrasi atau segera setelah EnsureCreated() untuk membuat database.

Azure Cosmos DB

Azure Cosmos DB adalah database NoSQL yang dikelola sepenuhnya untuk aplikasi berkinerja tinggi dengan ukuran atau skala apa pun. Lihat Penyedia Azure Cosmos DB untuk informasi tentang penggunaan Azure Cosmos DB dengan Entity Framework Core. Saat menggunakan Azure Cosmos DB dengan Azure Mobile Apps:

  1. Siapkan Kontainer Cosmos dengan indeks komposit yang menentukan bidang UpdatedAt dan Id. Indeks komposit dapat ditambahkan ke kontainer melalui portal Microsoft Azure, ARM, Bicep, Terraform, atau dalam kode. Berikut adalah contoh definisi sumber daya 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'
                            }
                        ]
                    ]
                }
            }
        }
    }
    

    Jika Anda menarik subset item dalam tabel, pastikan Anda menentukan semua properti yang terlibat dalam kueri.

  2. Dapatkan model dari kelas ETagEntityTableData:

    public class TodoItem : ETagEntityTableData
    {
        public string Title { get; set; }
        public bool Completed { get; set; }
    }
    
  3. Tambahkan metode OnModelCreating(ModelBuilder) ke DbContext. Driver Cosmos DB untuk Kerangka Kerja Entitas menempatkan semua entitas ke dalam kontainer yang sama secara default. Minimal, Anda harus memilih kunci partisi yang sesuai dan memastikan properti EntityTag ditandai sebagai tag konkurensi. Misalnya, cuplikan berikut menyimpan entitas TodoItem dalam kontainer mereka sendiri dengan pengaturan yang sesuai untuk Azure Mobile Apps:

    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 didukung dalam paket nuGet Microsoft.AspNetCore.Datasync.EFCore sejak v5.0.11. Untuk informasi selengkapnya, tinjau tautan berikut:

PostgreSQL

Buat pemicu untuk setiap entitas:

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

Anda dapat menginstal pemicu ini menggunakan migrasi atau segera setelah EnsureCreated() untuk membuat database.

SqLite

Peringatan

Jangan gunakan SqLite untuk layanan produksi. SqLite hanya cocok untuk penggunaan sisi klien dalam produksi.

SqLite tidak memiliki bidang tanggal/waktu yang mendukung akurasi milidetik. Dengan demikian, itu tidak cocok untuk apa pun kecuali untuk pengujian. Jika Anda ingin menggunakan SqLite, pastikan Anda menerapkan pengonversi nilai dan pembanding nilai pada setiap model untuk properti tanggal/waktu. Metode termudah untuk mengimplementasikan pengonversi nilai dan pembanding ada dalam metode OnModelCreating(ModelBuilder)DbContextAnda :

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

Instal pemicu pembaruan saat Anda menginisialisasi database:

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

Pastikan bahwa metode InstallUpdateTriggers hanya dipanggil sekali selama inisialisasi database:

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

LiteDB

LiteDB adalah database tanpa server yang dikirimkan dalam satu DLL kecil yang ditulis dalam kode terkelola .NET C#. Ini adalah solusi database NoSQL yang sederhana dan cepat untuk aplikasi yang berdiri sendiri. Untuk menggunakan LiteDb dengan penyimpanan persisten pada disk:

  1. Instal paket Microsoft.AspNetCore.Datasync.LiteDb dari NuGet.

  2. Tambahkan singleton untuk LiteDatabase ke Program.cs:

    const connectionString = builder.Configuration.GetValue<string>("LiteDb:ConnectionString");
    builder.Services.AddSingleton<LiteDatabase>(new LiteDatabase(connectionString));
    
  3. Memperoleh model dari LiteDbTableData:

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

    Anda dapat menggunakan salah satu atribut BsonMapper yang disediakan dengan paket LiteDb NuGet.

  4. Buat pengontrol menggunakan LiteDbRepository:

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

Dukungan OpenAPI

Anda dapat menerbitkan API yang ditentukan oleh pengontrol sinkronisasi data menggunakan NSwag atau Swashbuckle. Dalam kedua kasus, mulailah dengan menyiapkan layanan seperti yang biasanya Anda lakukan untuk pustaka yang dipilih.

NSwag

Ikuti instruksi dasar untuk integrasi NSwag, lalu ubah sebagai berikut:

  1. Tambahkan paket ke proyek Anda untuk mendukung NSwag. Paket berikut diperlukan:

  2. Tambahkan yang berikut ini ke bagian atas file Program.cs Anda:

    using Microsoft.AspNetCore.Datasync.NSwag;
    
  3. Tambahkan layanan untuk menghasilkan definisi OpenAPI ke file Program.cs Anda:

    builder.Services.AddOpenApiDocument(options =>
    {
        options.AddDatasyncProcessors();
    });
    
  4. Aktifkan middleware untuk melayani dokumen JSON yang dihasilkan dan antarmuka pengguna Swagger, juga di Program.cs:

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

Menelusuri ke titik akhir /swagger layanan web memungkinkan Anda menelusuri API. Definisi OpenAPI kemudian dapat diimpor ke layanan lain (seperti Azure API Management). Untuk informasi selengkapnya tentang mengonfigurasi NSwag, lihat Mulai menggunakan NSwag dan ASP.NET Core.

Swashbuckle

Ikuti instruksi dasar untuk integrasi Swashbuckle, lalu ubah sebagai berikut:

  1. Tambahkan paket ke proyek Anda untuk mendukung Swashbuckle. Paket berikut diperlukan:

  2. Tambahkan layanan untuk menghasilkan definisi OpenAPI ke file Program.cs Anda:

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

    Nota

    Metode AddDatasyncControllers() mengambil Assembly opsional yang sesuai dengan rakitan yang berisi pengontrol tabel Anda. Parameter Assembly hanya diperlukan jika pengontrol tabel Anda berada dalam proyek yang berbeda dengan layanan.

  3. Aktifkan middleware untuk melayani dokumen JSON yang dihasilkan dan antarmuka pengguna Swagger, juga di Program.cs:

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

Dengan konfigurasi ini, menelusuri ke akar layanan web memungkinkan Anda menelusuri API. Definisi OpenAPI kemudian dapat diimpor ke layanan lain (seperti Azure API Management). Untuk informasi selengkapnya tentang mengonfigurasi Swashbuckle, lihat Mulai menggunakan Swashbuckle dan ASP.NET Core.

Keterbatasan

Edisi ASP.NET Core dari pustaka layanan mengimplementasikan OData v4 untuk operasi daftar. Saat server berjalan dalam mode kompatibilitas mundur, pemfilteran pada substring tidak didukung.