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


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

Заметка

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

В этом руководстве показано, как выполнять распространенные сценарии с помощью клиентской библиотеки .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);

В приведенном выше коде замените 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

Если вы изменили параметры сериализатора на сервере синхронизации данных, необходимо внести те же изменения в 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 (строка) — глобальный уникальный идентификатор элемента.
  • UpdatedAt (System.DataTimeOffset) — дата и время последнего обновления элемента.
  • Version (строка) — непрозрачная строка, используемая для управления версиями.
  • 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 — как строка даты и времени UTC ISO-8601 с точностью мс.
  • System.DateTimeOffset — как строка даты и времени UTC ISO-8601 с точностью мс.
  • 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),
  • Строковые функции (Length, Substring, Replace, IndexOf, Equals, StartsWith, EndsWith) (только порядковые и инвариантные региональные параметры)
  • Свойства даты (Year, Month, Day, Hour, Minute, Second)
  • Доступ к свойствам объекта и
  • Выражения, объединяющие любую из этих операций.

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

Используйте .OrderBy(), .OrderByDescending(), .ThenBy()и .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();

Инициализация хранилища обычно выполняется сразу после создания клиента. OfflineConnectionString — это универсальный код ресурса (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
    };
}

UserId и DisplayName недоступны напрямую при использовании проверки подлинности службы приложений 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 предоставляет параметры для изменения операции очистки:

  • DiscardPendingOperations отменяет все ожидающие операции для таблицы, которая находится в очереди операций, ожидающей отправки на сервер.
  • 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; }
}

Свойства в argsnull или -1, если свойство не относится к событию синхронизации.