Использование клиентской библиотеки мобильных приложений Azure для .NET

В этом руководстве показано, как реализовать типичные сценарии с использованием клиентской библиотеки .NET для мобильных приложений Azure. Используйте клиентную библиотеку .NET в любом приложении .NET 6 или .NET Standard 2.0, включая MAUI, Xamarin и Windows (WPF, UWP и WinUI).

Если вы не знакомы с мобильными приложениями Azure, попробуйте сначала выполнить одно из кратких руководств.

Примечание.

В этой статье рассматриваются последние выпуски Microsoft Datasync Framework версии 6.0. Сведения о старых клиентах см. в документации по версии 4.2.0.

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

Клиентская библиотека .NET поддерживает любую платформу .NET Standard 2.0 или .NET 6, в том числе:

  • Платформа .NET MAUI для Android, iOS и Windows.
  • Уровень API Android 21 и более поздних версий (Xamarin и Android для .NET).
  • iOS версии 12.0 и более поздних версий (Xamarin и iOS для .NET).
  • универсальная платформа Windows сборки 19041 и более поздних версий.
  • Windows Presentation Framework (WPF).
  • Пакет SDK для приложений Windows (WinUI 3).
  • Xamarin.Forms

Кроме того, были созданы примеры для Avalonia и Uno Platform. Пример TodoApp содержит пример каждой тестовой платформы.

Настройка и необходимые компоненты

Добавьте следующие библиотеки из NuGet:

Если используется проект платформы (например, .NET MAUI), необходимо добавить библиотеки в проект платформы и любой общий проект.

Создание клиента службы

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

var options = new DatasyncClientOptions 
{
    // Options set here
};
var client = new DatasyncClient("MOBILE_APP_URL", options);

В приведенном выше коде замените MOBILE_APP_URL URL-адрес серверной части ASP.NET Core. Клиент должен быть создан как одинарный. При использовании поставщика проверки подлинности его можно настроить следующим образом:

var options = new DatasyncClientOptions 
{
    // Options set here
};
var client = new DatasyncClient("MOBILE_APP_URL", authProvider, options);

Дополнительные сведения о поставщике проверки подлинности приведены далее в этом документе.

Параметры

Полный (по умолчанию) набор параметров можно создать следующим образом:

var options = new DatasyncClientOptions
{
    HttpPipeline = new HttpMessageHandler[](),
    IdGenerator = (table) => Guid.NewGuid().ToString("N"),
    InstallationId = null,
    OfflineStore = null,
    ParallelOperations = 1,
    SerializerSettings = null,
    TableEndpointResolver = (table) => $"/tables/{tableName.ToLowerInvariant()}",
    UserAgent = $"Datasync/5.0 (/* Device information */)"
};

HttpPipeline

Как правило, HTTP-запрос выполняется путем передачи запроса через поставщика проверки подлинности (который добавляет Authorization заголовок для текущего пользователя, прошедшего проверку подлинности), перед отправкой запроса. При необходимости можно добавить дополнительные делегирующие обработчики. Каждый запрос проходит через делегирующие обработчики перед отправкой в службу. Делегирование обработчиков позволяет добавлять дополнительные заголовки, выполнять повторные попытки или предоставлять возможности ведения журнала.

Примеры делегирования обработчиков предоставляются для ведения журнала и добавления заголовков запросов далее в этой статье.

IdGenerator

При добавлении сущности в автономную таблицу он должен иметь идентификатор. Идентификатор создается, если он не указан. Этот IdGenerator параметр позволяет настроить созданный идентификатор. По умолчанию создается глобальный уникальный идентификатор. Например, следующий параметр создает строку, содержащую имя таблицы и GUID:

var options = new DatasyncClientOptions 
{
    IdGenerator = (table) => $"{table}-{Guid.NewGuid().ToString("D").ToUpperInvariant()}"
}

InstallationId

InstallationId Если задано, пользовательский заголовок X-ZUMO-INSTALLATION-ID отправляется с каждым запросом для идентификации сочетания приложения на определенном устройстве. Этот заголовок можно записать в журналы и определить количество отдельных установок для приложения. При использовании InstallationIdидентификатор должен храниться в постоянном хранилище на устройстве, чтобы можно было отслеживать уникальные установки.

Автономное хранилище

Он OfflineStore используется при настройке автономного доступа к данным. Дополнительные сведения см. в статье "Работа с автономными таблицами".

ParallelOperations

Часть процесса автономной синхронизации включает отправку очередных операций на удаленный сервер. При активации операции принудительной отправки операции отправляются в том порядке, в который они были получены. При необходимости можно использовать до восьми потоков для отправки этих операций. Параллельные операции используют больше ресурсов на клиенте и сервере для ускорения выполнения операции. Порядок, в котором операции, поступающие на сервер, не могут быть гарантированы при использовании нескольких потоков.

Сериализатор Параметры

Если вы изменили параметры сериализатора на сервере синхронизации данных, необходимо внести те же изменения SerializerSettings в клиент. Этот параметр позволяет указать собственные параметры сериализатора.

TableEndpointResolver

По соглашению таблицы находятся в удаленной службе по /tables/{tableName} пути (как указано Route атрибутом в коде сервера). Однако таблицы могут существовать в любом пути конечной точки. Это TableEndpointResolver функция, которая преобразует имя таблицы в путь для взаимодействия с удаленной службой.

Например, следующее изменяет предположение, чтобы все таблицы находились под /api:

var options = new DatasyncClientOptions
{
    TableEndpointResolver = (table) => $"/api/{table}"
};

UserAgent

Клиент синхронизации данных создает подходящее значение заголовка user-Agent на основе версии библиотеки. Некоторые разработчики чувствуют, что заголовок агента пользователя утечки сведений о клиенте. Свойство можно задать для любого допустимого значения заголовка UserAgent .

Работа с удаленными таблицами

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

Создание ссылки на удаленную таблицу

Чтобы создать ссылку на удаленную таблицу, используйте GetRemoteTable<T>:

IRemoteTable<TodoItem> remoteTable = client.GetRemoteTable();

Если вы хотите вернуть таблицу только для чтения, используйте версию IReadOnlyRemoteTable<T> :

IReadOnlyRemoteTable<TodoItem> remoteTable = client.GetRemoteTable();

Тип модели должен реализовать ITableData контракт из службы. Используйте DatasyncClientData для предоставления обязательных полей:

public class TodoItem : DatasyncClientData
{
    public string Title { get; set; }
    public bool IsComplete { get; set; }
}

Объект DatasyncClientData включает:

  • Id (string) — глобальный уникальный идентификатор элемента.
  • UpdatedAt (System.DataTimeOffset) — дата и время последнего обновления элемента.
  • Version (string) — непрозрачная строка, используемая для управления версиями.
  • Deleted (логическое значение) — если trueэлемент удаляется.

Служба поддерживает эти поля. Не изменяйте эти поля как часть клиентского приложения.

Модели можно аннотировать с помощью атрибутов Newtonsoft.JSON. Имя таблицы можно указать с помощью атрибута DataTable :

[DataTable("todoitem")]
public class MyTodoItemClass : DatasyncClientData
{
    public string Title { get; set; }
    public bool IsComplete { get; set; }
}

Кроме того, укажите имя таблицы в вызове GetRemoteTable() :

IRemoteTable<TodoItem> remoteTable = client.GetRemoteTable("todoitem");

Клиент использует путь /tables/{tablename} в качестве URI. Имя таблицы также является именем автономной таблицы в базе данных SQLite.

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

Помимо примитивных типов (int, float, string и т. д.), для моделей поддерживаются следующие типы:

  • System.DateTime — как строка даты и времени в формате ISO-8601 UTC с точностью мс.
  • System.DateTimeOffset — как строка даты и времени в формате ISO-8601 UTC с точностью мс.
  • System.Guid — отформатировано как 32 цифры, разделенные как дефисы.

Запрос данных с удаленного сервера

Удаленная таблица может использоваться с операторами LINQ-like, в том числе:

  • Фильтрация с .Where() помощью предложения.
  • Сортировка с различными .OrderBy() предложениями.
  • Выбор свойств с .Select()помощью .
  • Разбиение по страницам с .Skip() и .Take().

Подсчет элементов из запроса

Если вам нужно количество элементов, возвращаемых запросом, можно использовать .CountItemsAsync() в таблице или .LongCountAsync() запросе:

// Count items in a table.
long count = await remoteTable.CountItemsAsync();

// Count items in a query.
long count = await remoteTable.Where(m => m.Rating == "R").LongCountAsync();

Этот метод вызывает циклический переход к серверу. Вы также можете получить количество при заполнении списка (например, избегая дополнительной круговой поездки:

var enumerable = remoteTable.ToAsyncEnumerable() as AsyncPageable<T>;
var list = new List<T>();
long count = 0;
await foreach (var item in enumerable)
{
    count = enumerable.Count;
    list.Add(item);
}

Число будет заполнено после первого запроса на получение содержимого таблицы.

Возврат всех данных

Данные возвращаются с помощью IAsyncEnumerable:

var enumerable = remoteTable.ToAsyncEnumerable();
await foreach (var item in enumerable) 
{
    // Process each item
}

Используйте любое из следующих предложений конца для преобразования в IAsyncEnumerable<T> другую коллекцию:

T[] items = await remoteTable.ToArrayAsync();

Dictionary<string, T> items = await remoteTable.ToDictionaryAsync(t => t.Id);

HashSet<T> items = await remoteTable.ToHashSetAsync();

List<T> items = await remoteTable.ToListAsync();

За кулисами удаленная таблица обрабатывает разбиение результатов по страницам. Все элементы возвращаются независимо от количества запросов на стороне сервера для выполнения запроса. Эти элементы также доступны в результатах запроса (например, remoteTable.Where(m => m.Rating == "R")).

Платформа синхронизации данных также предоставляет ConcurrentObservableCollection<T> потокобезопасную коллекцию. Этот класс можно использовать в контексте приложений пользовательского интерфейса, которые обычно используются ObservableCollection<T> для управления списком (например, списки Xamarin Forms или MAUI). Вы можете очистить и загрузить ConcurrentObservableCollection<T> непосредственно из таблицы или запроса:

var collection = new ConcurrentObservableCollection<T>();
await remoteTable.ToObservableCollection(collection);

Использование .ToObservableCollection(collection) триггеров CollectionChanged события один раз для всей коллекции, а не для отдельных элементов, что приводит к более быстрому перерисовке времени.

Также ConcurrentObservableCollection<T> имеются изменения, управляемые предикатами:

// Add an item only if the identified item is missing.
bool modified = collection.AddIfMissing(t => t.Id == item.Id, item);

// Delete one or more item(s) based on a predicate
bool modified = collection.DeleteIf(t => t.Id == item.Id);

// Replace one or more item(s) based on a predicate
bool modified = collection.ReplaceIf(t => t.Id == item.Id, item);

Изменения на основе предиката можно использовать в обработчиках событий, когда индекс элемента не известен заранее.

Фильтрация данных

Предложение можно использовать .Where() для фильтрации данных. Например:

var items = await remoteTable.Where(x => !x.IsComplete).ToListAsync();

Фильтрация выполняется в службе до IAsyncEnumerable и на клиенте после IAsyncEnumerable. Например:

var items = (await remoteTable.Where(x => !x.IsComplete).ToListAsync()).Where(x => x.Title.StartsWith("The"));

Первое .Where() предложение (возвращаемое только неполные элементы) выполняется в службе, а второе .Where() предложение (начиная с "The") выполняется на клиенте.

Предложение Where поддерживает операции, которые будут преобразованы в подмножество OData. К этим операциям относятся:

  • операторы сравнения (==, !=, <, <=, >, >=);
  • арифметические операторы (+, -, /, *, %);
  • точность чисел (Math.Floor, Math.Ceiling);
  • Строковые функции (, , Substring, EqualsStartsWithReplaceIndexOf, , ) EndsWith(Lengthтолько порядковые и инвариантные региональные параметры)
  • свойства даты (Year, Month, Day, Hour, Minute, Second);
  • свойства доступа к объекту;
  • выражения, сочетающие в себе любые из этих операций.

Сортировка данных

Используйте .OrderBy(), .ThenBy().OrderByDescending()и .ThenByDescending() с методом доступа к свойствам для сортировки данных.

var items = await remoteTable.OrderBy(x => x.IsComplete).ThenBy(x => x.Title).ToListAsync();

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

var items = await remoteTable.ToListAsync().OrderBy(x => x.Title.ToLowerCase());

Выбор свойств

Вы можете вернуть подмножество данных из службы:

var items = await remoteTable.Select(x => new { x.Id, x.Title, x.IsComplete }).ToListAsync();

Возврат страницы данных

Вы можете вернуть подмножество набора данных с помощью .Skip() и .Take() реализовать разбиение по страницам:

var pageOfItems = await remoteTable.Skip(100).Take(10).ToListAsync();

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

Все описанные функции являются аддитивными, поэтому мы можем создавать из них цепочки. Каждый последующий цепной вызов все больше влияет на запрос. Еще один пример:

var query = todoTable
                .Where(todoItem => todoItem.Complete == false)
                .Select(todoItem => todoItem.Text)
                .Skip(3).
                .Take(3);
List<string> items = await query.ToListAsync();

Поиск удаленных данных по идентификатору

Функцию GetItemAsync можно использовать для поиска в базе данных объектов с определенным идентификатором.

TodoItem item = await remoteTable.GetItemAsync("37BBF396-11F0-4B39-85C8-B319C729AF6D");

Если элемент, который вы пытаетесь извлечь, был обратимо удален, необходимо использовать includeDeleted параметр:

// The following code will throw a DatasyncClientException if the item is soft-deleted.
TodoItem item = await remoteTable.GetItemAsync("37BBF396-11F0-4B39-85C8-B319C729AF6D");

// This code will retrieve the item even if soft-deleted.
TodoItem item = await remoteTable.GetItemAsync("37BBF396-11F0-4B39-85C8-B319C729AF6D", includeDeleted: true);

Вставка данных на удаленном сервере

Все типы клиентов должны содержать член с именем Id(идентификатор), который по умолчанию является строкой. Этот идентификатор необходим для выполнения операций CRUD и для автономной синхронизации. В следующем коде показано, как использовать InsertItemAsync метод для вставки новых строк в таблицу. Параметр содержит данные, которые вставляются в качестве объекта .NET.

var item = new TodoItem { Title = "Text", IsComplete = false };
await remoteTable.InsertItemAsync(item);
// Note that item.Id will now be set

Если уникальное пользовательское значение идентификатора не включено во item время вставки, сервер создает идентификатор. Созданный идентификатор можно получить, проверив объект после возвращения вызова.

Обновление данных на удаленном сервере

В следующем коде показано, как использовать ReplaceItemAsync метод для обновления существующей записи с тем же идентификатором с новыми сведениями.

// In this example, we assume the item has been created from the InsertItemAsync sample

item.IsComplete = true;
await remoteTable.ReplaceItemAsync(todoItem);

Удаление данных на удаленном сервере

В следующем коде показано, как использовать DeleteItemAsync метод для удаления существующего экземпляра.

// In this example, we assume the item has been created from the InsertItemAsync sample

await todoTable.DeleteItemAsync(item);

Разрешение конфликтов и оптимистическое параллелизм

Два или более клиентов могут одновременно записывать изменения в один и тот же элемент. Без механизма определения конфликтов последняя операция записи переписывала бы любые предыдущие обновления. Элемент управления оптимистичным параллелизмом предполагает, что каждая транзакция может зафиксировать и поэтому не использует блокировку ресурсов. Элемент управления оптимистичным параллелизмом проверяет, что другие транзакции не изменили данные перед фиксацией данных. Если данные были изменены, транзакция откатится.

Мобильные приложения Azure поддерживают управление оптимистическим параллелизмом, отслеживая изменения каждого элемента с помощью version столбца системного свойства, определенного для каждой таблицы в серверной части мобильного приложения. При каждом обновлении записи мобильные приложения задают новое значение свойства version для этой записи. При обработке каждого запроса на обновление свойство version записи, включенное в запрос, сравнивается с тем же свойством записи на сервере. Если версия, переданная с запросом, не соответствует серверной части, клиентская библиотека вызывает DatasyncConflictException<T> исключение. Тип, включенный в исключение, является записью серверной части, которая содержит версию записи на сервере. Затем приложение может использовать эти данные, чтобы решить, следует ли повторно выполнить полученный из серверной части запрос изменения с правильным значением version для фиксации изменений.

Оптимистическое параллелизм включается автоматически при использовании DatasyncClientData базового объекта.

Помимо включения оптимистического параллелизма, необходимо также поймать DatasyncConflictException<T> исключение в коде. Устраните конфликт, применив правильный version к обновленной записи, а затем повторите вызов с разрешенной записью. В следующем коде показано, как разрешить конфликт записи после его обнаружения:

private async void UpdateToDoItem(TodoItem item)
{
    DatasyncConflictException<TodoItem> exception = null;

    try
    {
        //update at the remote table
        await remoteTable.UpdateAsync(item);
    }
    catch (DatasyncConflictException<TodoItem> writeException)
    {
        exception = writeException;
    }

    if (exception != null)
    {
        // Conflict detected, the item has changed since the last query
        // Resolve the conflict between the local and server item
        await ResolveConflict(item, exception.Item);
    }
}


private async Task ResolveConflict(TodoItem localItem, TodoItem serverItem)
{
    //Ask user to choose the resolution between versions
    MessageDialog msgDialog = new MessageDialog(
        String.Format("Server Text: \"{0}\" \nLocal Text: \"{1}\"\n",
        serverItem.Text, localItem.Text),
        "CONFLICT DETECTED - Select a resolution:");

    UICommand localBtn = new UICommand("Commit Local Text");
    UICommand ServerBtn = new UICommand("Leave Server Text");
    msgDialog.Commands.Add(localBtn);
    msgDialog.Commands.Add(ServerBtn);

    localBtn.Invoked = async (IUICommand command) =>
    {
        // To resolve the conflict, update the version of the item being committed. Otherwise, you will keep
        // catching a MobileServicePreConditionFailedException.
        localItem.Version = serverItem.Version;

        // Updating recursively here just in case another change happened while the user was making a decision
        UpdateToDoItem(localItem);
    };

    ServerBtn.Invoked = async (IUICommand command) =>
    {
        RefreshTodoItems();
    };

    await msgDialog.ShowAsync();
}

Работа с автономными таблицами

Автономные таблицы используют локальное хранилище SQLite для хранения данных, которые могут использоваться в режиме "вне сети". Во всех операциях с таблицами используется локальное хранилище SQLite, а не хранилище удаленного сервера. Убедитесь, что вы добавите Microsoft.Datasync.Client.SQLiteStore его в каждый проект платформы и в любые общие проекты.

Перед созданием ссылки на таблицу необходимо подготовить локальное хранилище.

var store = new OfflineSQLiteStore(Constants.OfflineConnectionString);
store.DefineTable<TodoItem>();

После определения хранилища можно создать клиент:

var options = new DatasyncClientOptions 
{
    OfflineStore = store
};
var client = new DatasyncClient("MOBILE_URL", options);

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

await client.InitializeOfflineStoreAsync();

Обычно хранилище инициализируется сразу же после создания клиента. Автономный Подключение ionString — это универсальный код ресурса (URI), используемый для указания расположения базы данных SQLite и параметров, используемых для открытия базы данных. Дополнительные сведения см. в разделе URI Filenames in SQLite.

  • Чтобы использовать кэш в памяти, используйте file:inmemory.db?mode=memory&cache=private.
  • Чтобы использовать файл, используйте file:/path/to/file.db

Необходимо указать абсолютное имя файла. При использовании Xamarin можно использовать вспомогательные средства файловой системы Xamarin Essentials для создания пути: например:

var dbPath = $"{Filesystem.AppDataDirectory}/todoitems.db";
var store = new OfflineSQLiteStore($"file:/{dbPath}?mode=rwc");

Если вы используете MAUI, вы можете использовать вспомогательные службы файловой системы MAUI для создания пути: например:

var dbPath = $"{Filesystem.AppDataDirectory}/todoitems.db";
var store = new OfflineSQLiteStore($"file:/{dbPath}?mode=rwc");

Создание автономной таблицы

Ссылку на таблицу можно получить с помощью метода GetOfflineTable<T>.

IOfflineTable<TodoItem> table = client.GetOfflineTable<TodoItem>();

Как и в удаленной таблице, можно также предоставить автономную таблицу только для чтения:

IReadOnlyOfflineTable<TodoItem> table = client.GetOfflineTable<TodoItem>();

Для использования автономной таблицы не требуется проходить проверку подлинности. При взаимодействии с серверной службой необходимо пройти проверку подлинности.

Синхронизация автономной таблицы

Автономные таблицы по умолчанию не синхронизируются с серверной частью. Синхронизация происходит в два этапа. Можно передавать изменения отдельно от скачивания новых элементов. Например:

public async Task SyncAsync()
{
    ReadOnlyCollection<TableOperationError> syncErrors = null;

    try
    {
        foreach (var offlineTable in offlineTables.Values)
        {
            await offlineTable.PushItemsAsync();
            await offlineTable.PullItemsAsync("", options);
        }
    }
    catch (PushFailedException exc)
    {
        if (exc.PushResult != null)
        {
            syncErrors = exc.PushResult.Errors;
        }
    }

    // Simple error/conflict handling
    if (syncErrors != null)
    {
        foreach (var error in syncErrors)
        {
            if (error.OperationKind == TableOperationKind.Update && error.Result != null)
            {
                //Update failed, reverting to server's copy.
                await error.CancelAndUpdateItemAsync(error.Result);
            }
            else
            {
                // Discard local change.
                await error.CancelAndDiscardItemAsync();
            }

            Debug.WriteLine(@"Error executing sync operation. Item: {0} ({1}). Operation discarded.", error.TableName, error.Item["id"]);
        }
    }
}

По умолчанию все таблицы используют добавочную синхронизацию— извлекаются только новые записи. Запись включается для каждого уникального запроса (созданного путем создания хэша MD5 запроса OData).

Примечание.

Первым аргументом PullItemsAsync является запрос OData, указывающий, какие записи необходимо извлечь на устройство. Лучше изменить службу, чтобы возвращать только записи, относящиеся к пользователю, а не создавать сложные запросы на стороне клиента.

Параметры (определенные PullOptions объектом) обычно не нужно задавать. Возможные варианты:

  • PushOtherTables — если задано значение true, все таблицы отправляются.
  • QueryId — определенный идентификатор запроса, используемый вместо созданного.
  • WriteDeltaTokenInterval — как часто следует записывать разностный маркер, используемый для отслеживания добавочной синхронизации.

Пакет SDK выполняет неявную операцию PushAsync() перед извлечением записей.

Обработка конфликтов происходит в методе PullAsync(). Обрабатывать конфликты так же, как и онлайн-таблицы. Конфликт возникает, когда во время вставки, обновления или удаления вызывается PullAsync(). При возникновении нескольких конфликтов они объединяются в один.PushFailedException Каждая ошибка должна обрабатываться отдельно.

Отправка изменений для всех таблиц

Чтобы отправить все изменения на удаленный сервер, используйте следующую команду:

await client.PushTablesAsync();

Чтобы отправить изменения для подмножества таблиц, предоставьте IEnumerable<string> методу PushTablesAsync() :

var tablesToPush = new string[] { "TodoItem", "Notes" };
await client.PushTables(tablesToPush);

client.PendingOperations Используйте свойство для чтения количества операций, ожидающих отправки в удаленную службу. Это свойство происходит null , если автономное хранилище не настроено.

Выполнение сложных запросов SQLite

Если вам нужно выполнить сложные sql-запросы к автономной базе данных, это можно сделать с помощью ExecuteQueryAsync() метода. Например, чтобы выполнить SQL JOIN инструкцию, определите JObject структуру возвращаемого значения, а затем используйте ExecuteQueryAsync():

var definition = new JObject() 
{
    { "id", string.Empty },
    { "title", string.Empty },
    { "first_name", string.Empty },
    { "last_name", string.Empty }
};
var sqlStatement = "SELECT b.id as id, b.title as title, a.first_name as first_name, a.last_name as last_name FROM books b INNER JOIN authors a ON b.author_id = a.id ORDER BY b.id";

var items = await store.ExecuteQueryAsync(definition, sqlStatement, parameters);
// Items is an IList<JObject> where each JObject conforms to the definition.

Определение — это набор ключей и значений. Ключи должны соответствовать именам полей, возвращаемых sql-запросом, и значения должны быть значениями по умолчанию ожидаемого типа. Используется 0L для чисел (long), false для логических элементов и string.Empty для всего остального.

SQLite имеет ограничивающий набор поддерживаемых типов. Дата и время хранятся в виде количества миллисекунда с эпохи, чтобы разрешить сравнения.

Аутентификация пользователей

Мобильные приложения Azure позволяют создавать поставщик проверки подлинности для обработки вызовов проверки подлинности. Укажите поставщика проверки подлинности при создании клиента службы:

AuthenticationProvider authProvider = GetAuthenticationProvider();
var client = new DatasyncClient("APP_URL", authProvider);

Каждый раз, когда требуется проверка подлинности, поставщик проверки подлинности вызывается для получения маркера. Универсальный поставщик проверки подлинности можно использовать как для проверки подлинности на основе заголовка авторизации, так и для проверки подлинности на основе Служба приложений аутентификации и проверки подлинности на основе авторизации. Используйте следующую модель:

public AuthenticationProvider GetAuthenticationProvider()
    => new GenericAuthenticationProvider(GetTokenAsync);

// Or, if using Azure App Service Authentication and Authorization
// public AuthenticationProvider GetAuthenticationProvider()
//    => new GenericAuthenticationProvider(GetTokenAsync, "X-ZUMO-AUTH");

public async Task<AuthenticationToken> GetTokenAsync()
{
    // TODO: Any code necessary to get the right access token.
    
    return new AuthenticationToken 
    {
        DisplayName = "/* the display name of the user */",
        ExpiresOn = DateTimeOffset.Now.AddHours(1), /* when does the token expire? */
        Token = "/* the access token */",
        UserId = "/* the user id of the connected user */"
    };
}

Маркеры проверки подлинности кэшируются в памяти (никогда не записываются на устройство) и обновляются при необходимости.

Использование платформа удостоверений Майкрософт

Платформа удостоверений Майкрософт позволяет легко интегрироваться с идентификатором Microsoft Entra. Ознакомьтесь с краткими руководствами по началу работы с полным руководством по реализации проверки подлинности Microsoft Entra. В следующем коде показан пример получения маркера доступа:

private readonly string[] _scopes = { /* provide your AAD scopes */ };
private readonly object _parentWindow; /* Fill in with the required object before using */
private readonly PublicClientApplication _pca; /* Create one */

public MyAuthenticationHelper(object parentWindow) 
{
    _parentWindow = parentWindow;
    _pca = PublicClientApplicationBuilder.Create(clientId)
            .WithRedirectUri(redirectUri)
            .WithAuthority(authority)
            /* Add options methods here */
            .Build();
}

public async Task<AuthenticationToken> GetTokenAsync()
{
    // Silent authentication
    try
    {
        var account = await _pca.GetAccountsAsync().FirstOrDefault();
        var result = await _pca.AcquireTokenSilent(_scopes, account).ExecuteAsync();
        
        return new AuthenticationToken 
        {
            ExpiresOn = result.ExpiresOn,
            Token = result.AccessToken,
            UserId = result.Account?.Username ?? string.Empty
        };    
    }
    catch (Exception ex) when (exception is not MsalUiRequiredException)
    {
        // Handle authentication failure
        return null;
    }

    // UI-based authentication
    try
    {
        var account = await _pca.AcquireTokenInteractive(_scopes)
            .WithParentActivityOrWindow(_parentWindow)
            .ExecuteAsync();
        
        return new AuthenticationToken 
        {
            ExpiresOn = result.ExpiresOn,
            Token = result.AccessToken,
            UserId = result.Account?.Username ?? string.Empty
        };    
    }
    catch (Exception ex)
    {
        // Handle authentication failure
        return null;
    }
}

Дополнительные сведения об интеграции платформа удостоверений Майкрософт с ASP.NET 6 см. в документации по платформа удостоверений Майкрософт.

Использование Xamarin Essentials или MAUI WebAuthenticator

Для проверки подлинности службы приложение Azure можно использовать Xamarin Essentials WebAuthenticator или MAUI WebAuthenticator, чтобы получить маркер:

Uri authEndpoint = new Uri(client.Endpoint, "/.auth/login/aad");
Uri callback = new Uri("myapp://easyauth.callback");

public async Task<AuthenticationToken> GetTokenAsync()
{
    var authResult = await WebAuthenticator.AuthenticateAsync(authEndpoint, callback);
    return new AuthenticationToken 
    {
        ExpiresOn = authResult.ExpiresIn,
        Token = authResult.AccessToken
    };
}

DisplayName Он UserId недоступен напрямую при использовании проверки подлинности службы приложение Azure. Вместо этого используйте отложенный запроситель для получения сведений из конечной /.auth/me точки:

var userInfo = new AsyncLazy<UserInformation>(() => GetUserInformationAsync());

public async Task<UserInformation> GetUserInformationAsync() 
{
    // Get the token for the current user
    var authInfo = await GetTokenAsync();

    // Construct the request
    var request = new HttpRequestMessage(HttpMethod.Get, new Uri(client.Endpoint, "/.auth/me"));
    request.Headers.Add("X-ZUMO-AUTH", authInfo.Token);

    // Create a new HttpClient, then send the request
    var httpClient = new HttpClient();
    var response = await httpClient.SendAsync(request);

    // If the request is successful, deserialize the content into the UserInformation object.
    // You will have to create the UserInformation class.
    if (response.IsSuccessStatusCode) 
    {
        var content = await response.ReadAsStringAsync();
        return JsonSerializer.Deserialize<UserInformation>(content);
    }
}

Дополнительные разделы

Очистка сущностей в локальной базе данных

В обычной операции очистка сущностей не требуется. Процесс синхронизации удаляет удаленные сущности и сохраняет необходимые метаданные для локальных таблиц баз данных. Однако есть случаи, когда очистка сущностей в базе данных полезна. Один из таких сценариев заключается в удалении большого количества сущностей и более эффективной очистке данных из таблицы локально.

Чтобы очистить записи из таблицы, используйте table.PurgeItemsAsync():

var query = table.CreateQuery();
var purgeOptions = new PurgeOptions();
await table.PurgeItermsAsync(query, purgeOptions, cancellationToken);

Запрос определяет сущности, которые будут удалены из таблицы. Определите сущности для очистки с помощью LINQ:

var query = table.CreateQuery().Where(m => m.Archived == true);

Класс PurgeOptions предоставляет параметры для изменения операции очистки:

  • DiscardPendingOperationsdis карта любые ожидающие операции для таблицы, которая находится в очереди операций, ожидающей отправки на сервер.
  • QueryId указывает идентификатор запроса, используемый для идентификации разностного маркера, используемого для операции.
  • TimestampUpdatePolicy указывает, как настроить разностный маркер в конце операции очистки:
    • TimestampUpdatePolicy.NoUpdate указывает, что разностный маркер не должен быть обновлен.
    • TimestampUpdatePolicy.UpdateToLastEntity указывает, что разностный маркер должен быть обновлен до updatedAt поля для последней сущности, хранящейся в таблице.
    • TimestampUpdatePolicy.UpdateToNow указывает, что разностный маркер должен быть обновлен до текущей даты и времени.
    • TimestampUpdatePolicy.UpdateToEpoch указывает, что разностный маркер должен быть сброшен для синхронизации всех данных.

Используйте то же QueryId значение, которое использовалось при вызове table.PullItemsAsync() для синхронизации данных. Указывает QueryId разностный маркер для обновления после завершения очистки.

Настройка заголовков запросов

Для поддержки определенного сценария приложения вам может потребоваться настроить связь с внутренним сервером мобильных приложений. Например, можно добавить пользовательский заголовок к каждому исходящему запросу или изменить коды состояния ответа перед возвратом пользователю. Используйте пользовательский DelegatingHandler, как показано в следующем примере:

public async Task CallClientWithHandler()
{
    var options = new DatasyncClientOptions
    {
        HttpPipeline = new DelegatingHandler[] { new MyHandler() }
    };
    var client = new Datasync("AppUrl", options);
    var todoTable = client.GetRemoteTable<TodoItem>();
    var newItem = new TodoItem { Text = "Hello world", Complete = false };
    await todoTable.InsertItemAsync(newItem);
}

public class MyHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // Change the request-side here based on the HttpRequestMessage
        request.Headers.Add("x-my-header", "my value");

        // Do the request
        var response = await base.SendAsync(request, cancellationToken);

        // Change the response-side here based on the HttpResponseMessage

        // Return the modified response
        return response;
    }
}

Включение ведения журнала запросов

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

public class LoggingHandler : DelegatingHandler
{
    public LoggingHandler() : base() { }
    public LoggingHandler(HttpMessageHandler innerHandler) : base(innerHandler) { }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token)
    {
        Debug.WriteLine($"[HTTP] >>> {request.Method} {request.RequestUri}");
        if (request.Content != null)
        {
            Debug.WriteLine($"[HTTP] >>> {await request.Content.ReadAsStringAsync().ConfigureAwait(false)}");
        }

        HttpResponseMessage response = await base.SendAsync(request, token).ConfigureAwait(false);

        Debug.WriteLine($"[HTTP] <<< {response.StatusCode} {response.ReasonPhrase}");
        if (response.Content != null)
        {
            Debug.WriteLine($"[HTTP] <<< {await response.Content.ReadAsStringAsync().ConfigureAwait(false)}");
        }

        return response;
    }
}

Мониторинг событий синхронизации

Когда происходит событие синхронизации, событие публикуется делегату client.SynchronizationProgress события. События можно использовать для мониторинга хода выполнения процесса синхронизации. Определите обработчик событий синхронизации следующим образом:

client.SynchronizationProgress += (sender, args) => {
    // args is of type SynchronizationEventArgs
};

Тип SynchronizationEventArgs определяется следующим образом:

public enum SynchronizationEventType
{
    PushStarted,
    ItemWillBePushed,
    ItemWasPushed,
    PushFinished,
    PullStarted,
    ItemWillBeStored,
    ItemWasStored,
    PullFinished
}

public class SynchronizationEventArgs
{
    public SynchronizationEventType EventType { get; }
    public string ItemId { get; }
    public long ItemsProcessed { get; } 
    public long QueueLength { get; }
    public string TableName { get; }
    public bool IsSuccessful { get; }
}

Свойства внутри args или когда свойство не относится null-1 к событию синхронизации.