De Azure Mobile Apps-clientbibliotheek voor .NET gebruiken

Deze handleiding laat zien hoe u algemene scenario's uitvoert met behulp van de .NET-clientbibliotheek voor Azure Mobile Apps. Gebruik de .NET-clientbibliotheek in elke .NET 6- of .NET Standard 2.0-toepassing, waaronder MAUI, Xamarin en Windows (WPF, UWP en WinUI).

Als u nog niet eerder met Azure Mobile Apps werkt, kunt u overwegen eerst een van de snelstartzelfstudies uit te voeren:

Notitie

In dit artikel wordt de nieuwste versie (v6.0) van het Microsoft Datasync Framework behandeld. Zie de v4.2.0-documentatie voor oudere clients.

Ondersteunde platforms

De .NET-clientbibliotheek ondersteunt elk .NET Standard 2.0- of .NET 6-platform, waaronder:

  • .NET MAUI voor Android-, iOS- en Windows-platforms.
  • Android-API-niveau 21 en hoger (Xamarin en Android voor .NET).
  • iOS-versie 12.0 en hoger (Xamarin en iOS voor .NET).
  • Universeel Windows-platform builds 19041 en hoger.
  • Windows Presentation Framework (WPF).
  • Windows-app SDK (WinUI 3).
  • Xamarin.Forms

Daarnaast zijn er monsters gemaakt voor Den Enna en Uno Platform. Het TodoApp-voorbeeld bevat een voorbeeld van elk getest platform.

Installatie en vereisten

Voeg de volgende bibliotheken toe vanuit NuGet:

Als u een platformproject gebruikt (bijvoorbeeld .NET MAUI), moet u ervoor zorgen dat u de bibliotheken toevoegt aan het platformproject en een gedeeld project.

De serviceclient maken

Met de volgende code wordt de serviceclient gemaakt, die wordt gebruikt om alle communicatie met de back-end- en offlinetabellen te coördineren.

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

Vervang in de voorgaande code door MOBILE_APP_URL de URL van de ASP.NET Core-back-end. De client moet worden gemaakt als een singleton. Als u een verificatieprovider gebruikt, kan deze als volgt worden geconfigureerd:

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

Verderop in dit document vindt u meer informatie over de verificatieprovider.

Opties

U kunt als volgt een volledige (standaard) set opties maken:

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

Normaal gesproken wordt een HTTP-aanvraag gedaan door de aanvraag door te geven via de verificatieprovider (waarmee de Authorization header voor de momenteel geverifieerde gebruiker wordt toegevoegd) voordat de aanvraag wordt verzonden. U kunt desgewenst meer delegeringshandlers toevoegen. Elke aanvraag doorloopt de delegerende handlers voordat deze naar de service worden verzonden. Door handlers te delegeren, kunt u extra headers toevoegen, nieuwe pogingen uitvoeren of logboekregistratiemogelijkheden bieden.

Verderop in dit artikel worden voorbeelden gegeven van het delegeren van handlers voor logboekregistratie en het toevoegen van aanvraagheaders .

IdGenerator

Wanneer een entiteit wordt toegevoegd aan een offlinetabel, moet deze een id hebben. Er wordt een id gegenereerd als er geen id is opgegeven. Met de IdGenerator optie kunt u de id aanpassen die wordt gegenereerd. Standaard wordt er een wereldwijd unieke id gegenereerd. Met de volgende instelling wordt bijvoorbeeld een tekenreeks gegenereerd die de tabelnaam en een GUID bevat:

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

InstallationId

Als een InstallationId set is ingesteld, wordt er een aangepaste header X-ZUMO-INSTALLATION-ID verzonden met elke aanvraag om de combinatie van de toepassing op een specifiek apparaat te identificeren. Deze header kan worden vastgelegd in logboeken en stelt u in staat om het aantal afzonderlijke installaties voor uw app te bepalen. Als u deze gebruikt InstallationId, moet de id worden opgeslagen in permanente opslag op het apparaat, zodat unieke installaties kunnen worden bijgehouden.

OfflineStore

De OfflineStore gegevens worden gebruikt bij het configureren van offlinegegevenstoegang. Zie Werken met offlinetabellen voor meer informatie.

ParallelOperations

Een deel van het offlinesynchronisatieproces omvat het pushen van in de wachtrij geplaatste bewerkingen naar de externe server. Wanneer de pushbewerking wordt geactiveerd, worden de bewerkingen verzonden in de volgorde waarin ze zijn ontvangen. U kunt desgewenst maximaal acht threads gebruiken om deze bewerkingen te pushen. Parallelle bewerkingen gebruiken meer resources op zowel client als server om de bewerking sneller te voltooien. De volgorde waarin bewerkingen op de server binnenkomen, kan niet worden gegarandeerd wanneer u meerdere threads gebruikt.

Serializer Instellingen

Als u de serializer-instellingen op de gegevenssynchronisatieserver hebt gewijzigd, moet u dezelfde wijzigingen aanbrengen in de SerializerSettings client. Met deze optie kunt u uw eigen serialisatie-instellingen opgeven.

TableEndpointResolver

Volgens de conventie bevinden tabellen zich op de externe service op het /tables/{tableName} pad (zoals opgegeven door het Route kenmerk in de servercode). Tabellen kunnen echter bestaan op elk eindpuntpad. Dit TableEndpointResolver is een functie waarmee een tabelnaam wordt omgezet in een pad voor communicatie met de externe service.

Met de volgende wijzigingen wordt bijvoorbeeld de aanname gewijzigd, zodat alle tabellen zich onder /api:

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

UserAgent

De gegevenssynchronisatieclient genereert een geschikte headerwaarde voor de gebruikersagent op basis van de versie van de bibliotheek. Sommige ontwikkelaars vinden dat de header van de gebruikersagent informatie over de client lekt. U kunt de UserAgent eigenschap instellen op een geldige headerwaarde.

Werken met externe tabellen

De volgende sectie bevat informatie over het zoeken en ophalen van records en het wijzigen van de gegevens in een externe tabel. De volgende onderwerpen worden behandeld:

Een externe tabelreferentie maken

Als u een externe tabelreferentie wilt maken, gebruikt u GetRemoteTable<T>:

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

Als u een alleen-lezen tabel wilt retourneren, gebruikt u de IReadOnlyRemoteTable<T> versie:

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

Het modeltype moet het ITableData contract van de service implementeren. Gebruik DatasyncClientData dit om de vereiste velden op te geven:

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

Het DatasyncClientData object bevat:

  • Id (tekenreeks) - een globaal unieke id voor het item.
  • UpdatedAt (System.DataTimeOffset) - de datum/tijd waarop het item voor het laatst is bijgewerkt.
  • Version (tekenreeks) - een ondoorzichtige tekenreeks die wordt gebruikt voor versiebeheer.
  • Deleted (Booleaanse waarde): als true, wordt het item verwijderd.

De service onderhoudt deze velden. Pas deze velden niet aan als onderdeel van uw clienttoepassing.

Modellen kunnen worden geannoteerd met behulp van Newtonsoft.JSON-kenmerken. De naam van de tabel kan worden opgegeven met behulp van het DataTable kenmerk:

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

U kunt ook de naam van de tabel in de GetRemoteTable() aanroep opgeven:

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

De client gebruikt het pad /tables/{tablename} als de URI. De tabelnaam is ook de naam van de offlinetabel in de SQLite-database.

Ondersteunde typen

Afgezien van primitieve typen (int, float, tekenreeks, enzovoort), worden de volgende typen ondersteund voor modellen:

  • System.DateTime - als een ISO-8601 UTC-datum-/tijdtekenreeks met ms-nauwkeurigheid.
  • System.DateTimeOffset - als een ISO-8601 UTC-datum-/tijdtekenreeks met ms-nauwkeurigheid.
  • System.Guid - opgemaakt als 32 cijfers gescheiden als afbreekstreepjes.

Query's uitvoeren op gegevens van een externe server

De externe tabel kan worden gebruikt met LINQ-achtige instructies, waaronder:

  • Filteren met een .Where() component.
  • Sorteren met verschillende .OrderBy() componenten.
  • Eigenschappen selecteren met .Select().
  • Paging met .Skip() en .Take().

Items uit een query tellen

Als u een telling nodig hebt van de items die door de query worden geretourneerd, kunt u deze gebruiken .CountItemsAsync() voor een tabel of .LongCountAsync() voor een query:

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

Deze methode veroorzaakt een retour naar de server. U kunt ook een telling krijgen tijdens het invullen van een lijst (bijvoorbeeld), om de extra retour te vermijden:

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

Het aantal wordt ingevuld na de eerste aanvraag om de inhoud van de tabel op te halen.

Alle gegevens retourneren

Gegevens worden geretourneerd via een IAsyncEnumerable:

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

Gebruik een van de volgende afsluitclausules om de IAsyncEnumerable<T> naar een andere verzameling te converteren:

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

Achter de schermen verwerkt de externe tabel het paggineren van het resultaat voor u. Alle items worden geretourneerd, ongeacht hoeveel aanvragen aan de serverzijde nodig zijn om aan de query te voldoen. Deze elementen zijn ook beschikbaar voor queryresultaten (bijvoorbeeld remoteTable.Where(m => m.Rating == "R")).

Het Data Sync-framework biedt ConcurrentObservableCollection<T> ook een thread-veilige verzameling waarneembare verzameling. Deze klasse kan worden gebruikt in de context van UI-toepassingen die normaal gesproken worden gebruikt ObservableCollection<T> voor het beheren van een lijst (bijvoorbeeld Xamarin Forms of FORMS-lijsten). U kunt een ConcurrentObservableCollection<T> tabel of query rechtstreeks uit een tabel of query wissen en laden:

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

Met het gebruik van .ToObservableCollection(collection) triggers wordt de CollectionChanged gebeurtenis eenmaal geactiveerd voor de hele verzameling in plaats van voor afzonderlijke items, wat resulteert in een snellere hertekeningstijd.

De ConcurrentObservableCollection<T> heeft ook predicaatgestuurde wijzigingen:

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

Predicaatgestuurde wijzigingen kunnen worden gebruikt in gebeurtenis-handlers wanneer de index van het item niet van tevoren bekend is.

Gegevens filteren

U kunt een .Where() component gebruiken om gegevens te filteren. Voorbeeld:

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

Filteren wordt uitgevoerd op de service vóór de IAsyncEnumerable en op de client na de IAsyncEnumerable. Voorbeeld:

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

De eerste .Where() component (alleen onvolledige items retourneren) wordt uitgevoerd op de service, terwijl de tweede .Where() component (beginnend met 'The') op de client wordt uitgevoerd.

De Where component ondersteunt bewerkingen die worden omgezet in de OData-subset. Bewerkingen zijn onder andere:

  • Relationele operatoren (==, !=, <, <=, , >), >=
  • Rekenkundige operatoren (+, -, /, *, ), %
  • Getalprecisie (Math.Floor, Math.Ceiling),
  • Tekenreeksfuncties (Lengthalleen ReplaceEndsWithSubstringIndexOfEqualsStartsWithordinale en invariante culturen),
  • Datumeigenschappen (Year, Month, Day, Hour, ), SecondMinute
  • Toegangseigenschappen van een object en
  • Expressies die een van deze bewerkingen combineren.

Gegevens sorteren

Gebruik .OrderBy(), .OrderByDescending(), .ThenBy()en .ThenByDescending() met een eigenschapstoegangsor om gegevens te sorteren.

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

De sortering wordt uitgevoerd door de service. U kunt geen expressie opgeven in een sorteercomponent. Als u wilt sorteren op een expressie, gebruikt u sortering aan de clientzijde:

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

Eigenschappen selecteren

U kunt een subset met gegevens van de service retourneren:

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

Een pagina met gegevens retourneren

U kunt een subset van de gegevensset retourneren met .Skip() behulp van en .Take() om paging te implementeren:

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

In een echte app kunt u query's gebruiken die vergelijkbaar zijn met het voorgaande voorbeeld met een pager-besturingselement of vergelijkbare gebruikersinterface om tussen pagina's te navigeren.

Alle functies die tot nu toe worden beschreven, zijn additief, zodat we ze kunnen blijven koppelen. Elke gekoppelde aanroep beïnvloedt meer van de query. Nog een voorbeeld:

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

Externe gegevens opzoeken op id

De GetItemAsync functie kan worden gebruikt om objecten uit de database op te zoeken met een bepaalde id.

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

Als het item dat u probeert op te halen voorlopig is verwijderd, moet u de includeDeleted parameter gebruiken:

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

Gegevens invoegen op de externe server

Alle clienttypen moeten een lid met de naam Id bevatten. Dit is standaard een tekenreeks. Deze id is vereist voor het uitvoeren van CRUD-bewerkingen en voor offlinesynchronisatie. De volgende code illustreert hoe u de InsertItemAsync methode gebruikt om nieuwe rijen in een tabel in te voegen. De parameter bevat de gegevens die moeten worden ingevoegd als een .NET-object.

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

Als een unieke aangepaste id-waarde niet is opgenomen in de item invoegpositie, genereert de server een id. U kunt de gegenereerde id ophalen door het object te inspecteren nadat de aanroep is geretourneerd.

Gegevens op de externe server bijwerken

De volgende code illustreert hoe u de ReplaceItemAsync methode gebruikt om een bestaande record bij te werken met dezelfde id met nieuwe informatie.

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

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

Gegevens op de externe server verwijderen

De volgende code illustreert hoe u de DeleteItemAsync methode gebruikt om een bestaand exemplaar te verwijderen.

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

await todoTable.DeleteItemAsync(item);

Conflictoplossing en optimistische gelijktijdigheid

Twee of meer clients kunnen tegelijkertijd wijzigingen naar hetzelfde item schrijven. Zonder conflictdetectie overschrijft de laatste schrijfbewerking eventuele eerdere updates. Optimistisch gelijktijdigheidsbeheer gaat ervan uit dat elke transactie kan worden doorgevoerd en daarom geen resourcevergrendeling gebruikt. Optimistisch gelijktijdigheidsbeheer controleert of er geen andere transactie de gegevens heeft gewijzigd voordat de gegevens worden doorgevoerd. Als de gegevens zijn gewijzigd, wordt de transactie teruggedraaid.

Azure Mobile Apps biedt ondersteuning voor optimistisch gelijktijdigheidsbeheer door wijzigingen in elk item bij te houden met behulp van de version kolom systeemeigenschap die is gedefinieerd voor elke tabel in de back-end van uw mobiele app. Telkens wanneer een record wordt bijgewerkt, stelt Mobile Apps de version eigenschap voor die record in op een nieuwe waarde. Tijdens elke updateaanvraag wordt de version eigenschap van de record die is opgenomen in de aanvraag vergeleken met dezelfde eigenschap voor de record op de server. Als de versie die is doorgegeven met de aanvraag niet overeenkomt met de back-end, genereert de clientbibliotheek een DatasyncConflictException<T> uitzondering. Het type dat is opgenomen met de uitzondering, is de record van de back-end met de serverversie van de record. De toepassing kan deze informatie vervolgens gebruiken om te bepalen of de updateaanvraag opnieuw moet worden uitgevoerd met de juiste version waarde van de back-end om wijzigingen door te voeren.

Optimistische gelijktijdigheid wordt automatisch ingeschakeld wanneer u het DatasyncClientData basisobject gebruikt.

Naast het inschakelen van optimistische gelijktijdigheid moet u ook de DatasyncConflictException<T> uitzondering in uw code ondervangen. Los het conflict op door het juiste version toe te passen op de bijgewerkte record en herhaal de aanroep vervolgens met de opgeloste record. De volgende code laat zien hoe u een schrijfconflict kunt oplossen nadat dit is gedetecteerd:

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

Werken met offlinetabellen

Offlinetabellen gebruiken een lokaal SQLite-archief om gegevens op te slaan voor gebruik wanneer ze offline zijn. Alle tabelbewerkingen worden uitgevoerd op basis van het lokale SQLite-archief in plaats van het externe serverarchief. Zorg ervoor dat u het Microsoft.Datasync.Client.SQLiteStore aan elk platformproject en aan alle gedeelde projecten toevoegt.

Voordat u een tabelreferentie kunt maken, moet het lokale archief worden voorbereid:

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

Zodra de store is gedefinieerd, kunt u de client maken:

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

Ten slotte moet u ervoor zorgen dat de offlinemogelijkheden zijn geïnitialiseerd:

await client.InitializeOfflineStoreAsync();

De initialisatie van de opslag wordt normaal gesproken onmiddellijk uitgevoerd nadat de client is gemaakt. Offline Verbinding maken ionString is een URI die wordt gebruikt voor het opgeven van zowel de locatie van de SQLite-database als de opties die worden gebruikt om de database te openen. Zie URI-bestandsnamen in SQLite voor meer informatie.

  • Als u een cache in het geheugen wilt gebruiken, gebruikt u file:inmemory.db?mode=memory&cache=private.
  • Als u een bestand wilt gebruiken, gebruikt u file:/path/to/file.db

U moet de absolute bestandsnaam voor het bestand opgeven. Als u Xamarin gebruikt, kunt u de Xamarin Essentials File System Helpers gebruiken om een pad te maken: bijvoorbeeld:

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

Als u GEBRUIKMAAKT van MAUI, kunt u de HELP-helpers van het BESTANDSSYSTEEM VAN MAUI gebruiken om een pad te maken: bijvoorbeeld:

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

Een offlinetabel maken

Een tabelreferentie kan worden verkregen met behulp van de GetOfflineTable<T> methode:

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

Net als bij de externe tabel kunt u ook een alleen-lezen offlinetabel beschikbaar maken:

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

U hoeft zich niet te verifiëren voor het gebruik van een offlinetabel. U hoeft alleen te verifiëren wanneer u communiceert met de back-endservice.

Een offlinetabel synchroniseren

Offlinetabellen worden niet standaard gesynchroniseerd met de back-end. Synchronisatie wordt gesplitst in twee delen. U kunt wijzigingen afzonderlijk pushen van het downloaden van nieuwe items. Voorbeeld:

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

Standaard maken alle tabellen gebruik van incrementele synchronisatie. Alleen nieuwe records worden opgehaald. Er wordt een record opgenomen voor elke unieke query (gegenereerd door het maken van een MD5-hash van de OData-query).

Notitie

Het eerste argument PullItemsAsync hiervoor is de OData-query die aangeeft welke records moeten worden opgehaald naar het apparaat. Het is beter om de service zo te wijzigen dat alleen records worden geretourneerd die specifiek zijn voor de gebruiker in plaats van complexe query's aan de clientzijde te maken.

De opties (gedefinieerd door het PullOptions object) hoeven doorgaans niet te worden ingesteld. De volgende opties zijn beschikbaar:

  • PushOtherTables - indien ingesteld op true, worden alle tabellen gepusht.
  • QueryId - een specifieke query-id die moet worden gebruikt in plaats van de gegenereerde id.
  • WriteDeltaTokenInterval - hoe vaak u het deltatoken schrijft dat wordt gebruikt om incrementele synchronisatie bij te houden.

De SDK voert een impliciete bewerking uit PushAsync() voordat records worden opgehaald.

Conflictafhandeling vindt plaats op een PullAsync() methode. Conflicten op dezelfde manier verwerken als onlinetabellen. Het conflict wordt veroorzaakt wanneer PullAsync() deze wordt aangeroepen in plaats van tijdens het invoegen, bijwerken of verwijderen. Als er meerdere conflicten optreden, worden ze gebundeld in één PushFailedException. Elke fout afzonderlijk afhandelen.

Wijzigingen voor alle tabellen pushen

Als u alle wijzigingen naar de externe server wilt pushen, gebruikt u:

await client.PushTablesAsync();

Als u wijzigingen wilt pushen voor een subset van tabellen, geeft u een IEnumerable<string> aan de PushTablesAsync() methode op:

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

Gebruik de client.PendingOperations eigenschap om het aantal bewerkingen te lezen dat moet worden gepusht naar de externe service. Deze eigenschap is null wanneer er geen offlinearchief is geconfigureerd.

Complexe SQLite-query's uitvoeren

Als u complexe SQL-query's wilt uitvoeren voor de offlinedatabase, kunt u dit doen met behulp van de ExecuteQueryAsync() methode. Als u bijvoorbeeld een SQL JOIN instructie wilt uitvoeren, definieert u een JObject instructie die de structuur van de retourwaarde weergeeft en gebruikt u 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.

De definitie is een set sleutel/waarden. De sleutels moeten overeenkomen met de veldnamen die de SQL-query retourneert en de waarden moeten de standaardwaarde van het verwachte type zijn. Gebruiken 0L voor getallen (lang), false voor Booleaanse waarden en string.Empty voor alles anders.

SQLite heeft een beperkende set ondersteunde typen. Datum/tijden worden opgeslagen als het aantal milliseconden sinds het tijdvak om vergelijkingen toe te staan.

Gebruikers verifiëren

Met Azure Mobile Apps kunt u een verificatieprovider genereren voor het verwerken van verificatieaanroepen. Geef de verificatieprovider op bij het maken van de serviceclient:

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

Wanneer verificatie is vereist, wordt de verificatieprovider aangeroepen om het token op te halen. Een algemene verificatieprovider kan worden gebruikt voor verificatie op basis van zowel verificatie op basis van autorisatie- als App Service-verificatie en verificatie op basis van autorisatie. Gebruik het volgende model:

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 */"
    };
}

Verificatietokens worden in de cache opgeslagen in het geheugen (nooit naar het apparaat geschreven) en indien nodig vernieuwd.

Het Microsoft Identity Platform gebruiken

Met het Microsoft Identity Platform kunt u eenvoudig integreren met Microsoft Entra ID. Zie de zelfstudies aan de slag voor een volledige zelfstudie over het implementeren van Microsoft Entra-verificatie. De volgende code toont een voorbeeld van het ophalen van het toegangstoken:

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

Zie de documentatie van het Microsoft Identity Platform voor meer informatie over het integreren van het Microsoft Identity Platform met ASP.NET 6.

Xamarin Essentials of MAUI WebAuthenticator gebruiken

Voor Azure-app serviceverificatie kunt u de Xamarin Essentials WebAuthenticator of de TENANT WebAuthenticator gebruiken om een token op te halen:

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

De UserId en DisplayName zijn niet rechtstreeks beschikbaar bij het gebruik van Azure-app serviceverificatie. Gebruik in plaats daarvan een luie aanvrager om de informatie op te halen van het /.auth/me eindpunt:

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

Geavanceerde onderwerpen

Entiteiten in de lokale database opschonen

Onder normale werking is het opschonen van entiteiten niet vereist. Het synchronisatieproces verwijdert verwijderde entiteiten en onderhoudt de vereiste metagegevens voor lokale databasetabellen. Er zijn echter situaties waarin het opschonen van entiteiten in de database nuttig is. Een dergelijk scenario is wanneer u een groot aantal entiteiten moet verwijderen en het efficiënter is om gegevens uit de tabel lokaal te wissen.

Als u records uit een tabel wilt opschonen, gebruikt u table.PurgeItemsAsync():

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

De query identificeert de entiteiten die uit de tabel moeten worden verwijderd. Identificeer de entiteiten die moeten worden opgeschoond met LINQ:

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

De PurgeOptions klasse biedt instellingen voor het wijzigen van de opschoonbewerking:

  • DiscardPendingOperations verwijdert alle bewerkingen die in behandeling zijn voor de tabel die zich in de wachtrij voor bewerkingen bevinden die wachten om naar de server te worden verzonden.
  • QueryId geeft een query-id op die wordt gebruikt om het deltatoken te identificeren dat moet worden gebruikt voor de bewerking.
  • TimestampUpdatePolicy geeft aan hoe u het deltatoken aan het einde van de opschoningsbewerking kunt aanpassen:
    • TimestampUpdatePolicy.NoUpdate geeft aan dat het deltatoken niet mag worden bijgewerkt.
    • TimestampUpdatePolicy.UpdateToLastEntity geeft aan dat het deltatoken moet worden bijgewerkt naar het updatedAt veld voor de laatste entiteit die in de tabel is opgeslagen.
    • TimestampUpdatePolicy.UpdateToNow geeft aan dat het deltatoken moet worden bijgewerkt naar de huidige datum/tijd.
    • TimestampUpdatePolicy.UpdateToEpoch geeft aan dat het deltatoken opnieuw moet worden ingesteld om alle gegevens te synchroniseren.

Gebruik dezelfde QueryId waarde die u hebt gebruikt bij het aanroepen table.PullItemsAsync() om gegevens te synchroniseren. Hiermee QueryId geeft u het deltatoken op dat moet worden bijgewerkt wanneer de opschoning is voltooid.

Aanvraagheaders aanpassen

Ter ondersteuning van uw specifieke app-scenario moet u mogelijk de communicatie aanpassen met de back-end van de mobiele app. U kunt bijvoorbeeld een aangepaste header toevoegen aan elke uitgaande aanvraag of antwoordstatuscodes wijzigen voordat u terugkeert naar de gebruiker. Gebruik een aangepaste DelegatingHandler, zoals in het volgende voorbeeld:

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

Logboekregistratie van aanvragen inschakelen

U kunt ook een DelegatingHandler gebruiken om logboekregistratie van aanvragen toe te voegen:

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

Synchronisatie-gebeurtenissen bewaken

Wanneer er een synchronisatiegebeurtenis plaatsvindt, wordt de gebeurtenis gepubliceerd naar de gemachtigde van de client.SynchronizationProgress gebeurtenis. De gebeurtenissen kunnen worden gebruikt om de voortgang van het synchronisatieproces te controleren. Definieer als volgt een synchronisatie-gebeurtenis-handler:

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

Het SynchronizationEventArgs type wordt als volgt gedefinieerd:

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

De eigenschappen binnen args zijn of null-1 wanneer de eigenschap niet relevant is voor de synchronisatie-gebeurtenis.