Поделиться через


Использование пакета SDK серверного сервера ASP.NET Core

Заметка

Этот продукт отставлен. Сведения о замене проектов с помощью .NET 8 или более поздней версии см. вбиблиотеке Community Toolkit Datasync.

В этой статье показано, как настроить и использовать пакет SDK серверного сервера ASP.NET Core для создания сервера синхронизации данных.

Поддерживаемые платформы

Сервер серверной части ASP.NET Core поддерживает ASP.NET 6.0 или более поздней версии.

Серверы баз данных должны соответствовать следующим критериям: поле типа DateTime или Timestamp, которое хранится с точностью миллисекунда. Реализации репозитория предоставляются для Entity Framework Core и LiteDb.

Сведения о поддержке конкретной базы данных см. в следующих разделах:

Создание нового сервера синхронизации данных

Сервер синхронизации данных использует обычные механизмы ASP.NET Core для создания сервера. Он состоит из трех шагов:

  1. Создайте серверный проект ASP.NET 6.0 (или более поздней версии).
  2. Добавление Entity Framework Core
  3. Добавление служб синхронизации данных

Сведения о создании службы ASP.NET Core с помощью Entity Framework Core см. в учебнике.

Чтобы включить службы синхронизации данных, необходимо добавить следующие библиотеки NuGet:

Измените файл Program.cs. Добавьте следующую строку во все остальные определения служб:

builder.Services.AddDatasyncControllers();

Вы также можете использовать шаблон datasync-server core ASP.NET:

# 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. Создание контроллера таблицы — это трехэтапный процесс:

  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 предоставляет идентификатор записи, а также дополнительные свойства для обработки служб синхронизации данных:

  • 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, но их можно разместить в любом месте. Если вы используете клиентские библиотеки раньше версии 5.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() возвращает лямбда-строку, которая ограничивает то, что может видеть подключенный пользователь.
  • 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 Core поддерживает 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 не настраивает создание значений для столбцов даты и времени. (См. создания значений даты и времени). Репозиторий мобильных приложений Azure для Entity Framework Core автоматически обновляет поле 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:

  1. Настройте контейнер Cosmos с составным индексом, указывающим поля UpdatedAt и Id. Составные индексы можно добавить в контейнер через портал Azure, ARM, Bicep, Terraform или в коде. Ниже приведен пример определения ресурсов bicep bice p:

    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. Драйвер Cosmos DB для Entity Framework помещает все сущности в один контейнер по умолчанию. Как минимум, необходимо выбрать подходящий ключ секции и убедиться, что свойство EntityTag помечается как тег параллелизма. Например, следующий фрагмент кода хранит сущности TodoItem в собственном контейнере с соответствующими параметрами для мобильных приложений Azure:

    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 поддерживается в пакете NuGet Microsoft.AspNetCore.Datasync.EFCore с версии 5.0.11. Дополнительные сведения см. по следующим ссылкам:

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, убедитесь, что вы реализуете преобразователь значений и средство сравнения значений для каждой модели для свойств даты и времени. Самый простой способ реализации преобразователей значений и сравнения — в методе OnModelCreating(ModelBuilder)DbContext:

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 — это бессерверная база данных, доставленная в одну небольшую библиотеку DLL, написанную в управляемом коде .NET C#. Это простое и быстрое решение базы данных NoSQL для автономных приложений. Чтобы использовать LiteDb с сохраняемым хранилищем на диске, выполните приведенные действия.

  1. Установите пакет Microsoft.AspNetCore.Datasync.LiteDb из NuGet.

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

    Вы можете использовать любой из BsonMapper атрибутов, предоставляемых пакетом NuGet LiteDb.

  4. Создайте контроллер с помощью LiteDbRepository:

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

Поддержка OpenAPI

Вы можете опубликовать API, определенный контроллерами синхронизации данных, с помощью NSwag или Swashbuckle. В обоих случаях сначала настройте службу, как правило, для выбранной библиотеки.

NSwag

Выполните основные инструкции по интеграции NSwag, а затем измените следующее:

  1. Добавьте пакеты в проект для поддержки NSwag. Требуются следующие пакеты:

  2. Добавьте следующее в начало файла Program.cs:

    using Microsoft.AspNetCore.Datasync.NSwag;
    
  3. Добавьте службу для создания определения OpenAPI в файл Program.cs:

    builder.Services.AddOpenApiDocument(options =>
    {
        options.AddDatasyncProcessors();
    });
    
  4. Включите ПО промежуточного слоя для обслуживания созданного документа JSON и пользовательского интерфейса Swagger, а также в Program.cs:

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

Просмотр /swagger конечной точки веб-службы позволяет просматривать API. Затем определение OpenAPI можно импортировать в другие службы (например, управление API Azure). Дополнительные сведения о настройке NSwag см. в статье Начало работы с NSwag и ASP.NET Core.

Swashbuckle

Выполните основные инструкции по интеграции Swashbuckle, а затем измените следующее:

  1. Добавьте пакеты в проект для поддержки Swashbuckle. Требуются следующие пакеты:

  2. Добавьте службу для создания определения OpenAPI в файл Program.cs:

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

    Заметка

    Метод AddDatasyncControllers() принимает необязательный Assembly, соответствующий сборке, содержащей контроллеры таблиц. Параметр Assembly требуется только в том случае, если контроллеры таблиц находятся в другом проекте службы.

  3. Включите ПО промежуточного слоя для обслуживания созданного документа JSON и пользовательского интерфейса Swagger, а также в Program.cs:

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

С помощью этой конфигурации просмотр в корне веб-службы позволяет просматривать API. Затем определение OpenAPI можно импортировать в другие службы (например, управление API Azure). Дополнительные сведения о настройке Swashbuckle см. в статье Начало работы с Swashbuckle и ASP.NET Core.

Ограничения

Выпуск ASP.NET Core библиотек служб реализует OData версии 4 для операции списка. Если сервер работает в режиме обратной совместимости, фильтрация по подстроке не поддерживается.