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

В этой статье показано, как настроить и использовать пакет 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();

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

  1. Создайте класс модели для модели данных.
  2. Добавьте класс модели в DbContext приложение.
  3. Создайте новый TableController<T> класс для предоставления модели.

Создание класса модели

Все классы модели должны реализовывать ITableData. Каждый тип репозитория имеет абстрактный класс, реализующий ITableData. Используется EntityTableDataрепозиторий Entity Framework Core:

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. Назначьте объект свойству ILoggerLogger :

[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() метод. Метод Th 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

Сервер синхронизации данных core ASP.NET поддерживает 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 :

    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 поддерживается в пакете Microsoft.AspNetCore.Datasync.EFCore NuGet с версии 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. Добавьте одноэлементный элемент для следующих вариантов LiteDatabaseProgram.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 можно импортировать в другие службы (например, Azure Управление API). Дополнительные сведения о настройке 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 можно импортировать в другие службы (например, Azure Управление API). Дополнительные сведения о настройке Swashbuckle см. в статье "Начало работы с Swashbuckle" и ASP.NET Core.

Ограничения

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