.NET용 Azure Mobile Apps 클라이언트 라이브러리를 사용하는 방법

이 가이드에서는 Azure Mobile Apps용 .NET 클라이언트 라이브러리를 사용하여 일반적인 시나리오를 수행하는 방법을 보여 줍니다. MAUI, Xamarin 및 Windows(WPF, UWP 및 WinUI)를 비롯한 모든 .NET 6 또는 .NET Standard 2.0 애플리케이션에서 .NET 클라이언트 라이브러리를 사용합니다.

Azure Mobile Apps를 처음 접하는 경우 먼저 빠른 시작 자습서 중 하나를 완료하는 것이 좋습니다.

참고 항목

이 문서에서는 Microsoft Datasync Framework의 최신(v6.0) 버전을 다룹니다. 이전 클라이언트의 경우 v4.2.0 설명서를 참조 하세요.

지원되는 플랫폼

.NET 클라이언트 라이브러리는 다음을 포함하여 모든 .NET Standard 2.0 또는 .NET 6 플랫폼을 지원합니다.

  • Android, iOS 및 Windows 플랫폼용 .NET MAUI.
  • Android API 수준 21 이상(.NET용 Xamarin 및 Android).
  • iOS 버전 12.0 이상(.NET용 Xamarin 및 iOS).
  • 유니버설 Windows 플랫폼 19041 이상을 빌드합니다.
  • WPF(Windows Presentation Framework).
  • Windows 앱 SDK(WinUI 3).
  • Xamarin.Forms

또한 AvaloniaUno Platform에 대한 샘플이 생성되었습니다. TodoApp 샘플에는 테스트된 각 플랫폼의 예가 포함되어 있습니다.

설정 및 필수 구성 요소

NuGet에서 다음 라이브러리를 추가합니다.

플랫폼 프로젝트(예: .NET MAUI)를 사용하는 경우 플랫폼 프로젝트 및 공유 프로젝트에 라이브러리를 추가해야 합니다.

서비스 클라이언트 만들기

다음 코드는 백 엔드 및 오프라인 테이블에 대한 모든 통신을 조정하는 데 사용되는 서비스 클라이언트를 만듭니다.

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

앞의 코드에서 ASP.NET Core 백 엔드URL로 바꿉 MOBILE_APP_URL 니다. 클라이언트는 싱글톤으로 만들어야 합니다. 인증 공급자를 사용하는 경우 다음과 같이 구성할 수 있습니다.

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

엔터티가 오프라인 테이블에 추가되면 ID가 있어야 합니다. ID가 제공되지 않으면 생성됩니다. 이 IdGenerator 옵션을 사용하면 생성된 ID를 조정할 수 있습니다. 기본적으로 전역적으로 고유한 ID가 생성됩니다. 예를 들어 다음 설정은 테이블 이름과 GUID를 포함하는 문자열을 생성합니다.

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

InstallationId

InstallationId 설정된 경우 특정 디바이스에서 애플리케이션의 조합을 식별하기 위해 각 요청과 함께 사용자 지정 헤더 X-ZUMO-INSTALLATION-ID 가 전송됩니다. 이 헤더는 로그에 기록할 수 있으며 앱에 대한 고유한 설치 수를 확인할 수 있습니다. 사용하는 InstallationId경우 고유한 설치를 추적할 수 있도록 ID를 디바이스의 영구 스토리지에 저장해야 합니다.

OfflineStore

오프라인 OfflineStore 데이터 액세스를 구성할 때 사용됩니다. 자세한 내용은 오프라인 테이블 작업을 참조 하세요.

ParallelOperations

오프라인 동기화 프로세스의 일부에는 대기 중인 작업을 원격 서버에 푸시하는 작업이 포함됩니다. 푸시 작업이 트리거되면 작업이 수신된 순서대로 제출됩니다. 필요에 따라 최대 8개의 스레드를 사용하여 이러한 작업을 푸시할 수 있습니다. 병렬 작업은 클라이언트와 서버 모두에서 더 많은 리소스를 사용하여 작업을 더 빠르게 완료합니다. 여러 스레드를 사용하는 경우 작업이 서버에 도착하는 순서를 보장할 수 없습니다.

Serializer설정

데이터 동기화 서버에서 직렬 변환기 설정을 변경한 경우 클라이언트에서 동일한 변경을 SerializerSettings 수행해야 합니다. 이 옵션을 사용하면 고유한 직렬 변환기 설정을 지정할 수 있습니다.

TableEndpointResolver

규칙에 따라 테이블은 경로의 원격 서비스에 /tables/{tableName} 있습니다(서버 코드의 Route 특성에 지정된 대로). 그러나 테이블은 모든 엔드포인트 경로에 있을 수 있습니다. 테이블 TableEndpointResolver 이름을 원격 서비스와 통신하기 위한 경로로 바꾸는 함수입니다.

예를 들어 다음에서는 모든 테이블이 다음 아래에 /api배치되도록 가정을 변경합니다.

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

UserAgent

데이터 동기화 클라이언트는 라이브러리 버전에 따라 적절한 사용자 에이전트 헤더 값을 생성합니다. 일부 개발자는 사용자 에이전트 헤더가 클라이언트에 대한 정보를 유출하는 것을 느낍니다. 속성을 유효한 헤더 값으로 설정할 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 (문자열) - 항목에 대한 전역적으로 고유한 ID입니다.
  • UpdatedAt (System.DataTimeOffset) - 항목이 마지막으로 업데이트된 날짜/시간입니다.
  • Version (string) - 버전 관리에 사용되는 불투명 문자열입니다.
  • Deleted(boolean) - 항목이 삭제된 경우 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 - MS 정확도가 있는 ISO-8601 UTC 날짜/시간 문자열입니다.
  • System.DateTimeOffset - MS 정확도가 있는 ISO-8601 UTC 날짜/시간 문자열입니다.
  • System.Guid - 하이픈으로 구분된 32자리 숫자로 서식이 지정됩니다.

원격 서버에서 데이터 쿼리

원격 테이블은 다음을 포함하여 LINQ와 유사한 문과 함께 사용할 수 있습니다.

  • 절을 사용하여 필터링합니다 .Where() .
  • 다양한 .OrderBy() 절을 사용하여 정렬합니다.
  • 를 사용하여 속성 .Select()선택
  • 페이징 및 .Skip().Take().

쿼리에서 항목 개수 계산

쿼리에서 반환할 항목 수가 필요한 경우 테이블 또는 .LongCountAsync() 쿼리에서 사용할 .CountItemsAsync() 수 있습니다.

// 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> . 이 클래스는 일반적으로 목록을 관리하는 데 사용하는 UI 애플리케이션의 컨텍스트에서 사용할 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, ReplaceIndexOf, Equals, StartsWithEndsWith) (서수 및 고정 문화권만 해당),
  • 날짜 속성(Year, Month, Day, Hour, Minute, Second),
  • 개체의 액세스 속성 및
  • 이러한 작업을 결합하는 식입니다.

데이터 정렬

, .OrderByDescending().ThenBy().ThenByDescending() 속성 접근자를 사용하여 .OrderBy()데이터를 정렬합니다.

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

실제 앱에서는 이전 예제와 유사한 쿼리를 페이저 컨트롤 또는 비교 가능한 UI와 함께 사용하여 페이지 간을 탐색할 수 있습니다.

지금까지 설명한 모든 함수는 가산적이므로 계속 연결할 수 있습니다. 연결된 각 호출은 더 많은 쿼리에 영향을 줍니다. 한 가지 더 예제:

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

ID별로 원격 데이터 조회

이 함수는 GetItemAsync 특정 ID를 사용하여 데이터베이스에서 개체를 조회하는 데 사용할 수 있습니다.

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라는 멤버를 포함해야 하며 이는 기본적으로 문자열입니다. 이 ID 는 CRUD 작업을 수행하고 오프라인 동기화에 필요합니다. 다음 코드에서는 메서드를 사용하여 InsertItemAsync 테이블에 새 행을 삽입하는 방법을 보여 줍니다. 매개 변수에는 .NET 개체로 삽입할 데이터가 포함됩니다.

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

삽입하는 동안 고유한 사용자 지정 ID 값이 item 포함되지 않으면 서버에서 ID를 생성합니다. 호출이 반환된 후 개체를 검사하여 생성된 ID를 검색할 수 있습니다.

원격 서버에서 데이터 업데이트

다음 코드에서는 메서드를 사용하여 ReplaceItemAsync 새 정보로 동일한 ID로 기존 레코드를 업데이트하는 방법을 보여 줍니다.

// 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 Mobile Apps는 모바일 앱 백 엔드의 각 테이블에 대해 정의된 시스템 속성 열을 사용하여 version 각 항목의 변경 내용을 추적하여 낙관적 동시성 제어를 지원합니다. 레코드가 업데이트 될 때마다 Mobile Apps는 해당 레코드의 version 속성을 새 값으로 설정합니다. 각 업데이트 요청 중에 요청에 포함된 레코드의 version 속성이 서버에 있는 레코드의 동일 속성과 비교됩니다. 요청과 함께 전달된 버전이 백 엔드와 일치하지 않으면 클라이언트 라이브러리에서 예외가 DatasyncConflictException<T> 발생합니다. 예외에 포함된 형식은 레코드의 서버 버전을 포함하는 백 엔드의 레코드입니다. 그런 다음 애플리케이션은 이 정보를 사용하여 변경 내용을 커밋하기 위해 백 엔드의 올바른 version 값으로 업데이트 요청을 다시 실행할지 여부를 결정할 수 있습니다.

낙관적 동시성은 기본 개체를 사용할 DatasyncClientData 때 자동으로 사용하도록 설정됩니다.

낙관적 동시성을 사용하도록 설정하는 것 외에도 코드에서 예외를 DatasyncConflictException<T> catch해야 합니다. 업데이트된 레코드에 올바른 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();

저장소 초기화는 일반적으로 클라이언트를 만든 직후에 수행됩니다. Offline커넥트ionString은 SQLite 데이터베이스의 위치와 데이터베이스를 여는 데 사용되는 옵션을 모두 지정하는 데 사용되는 URI입니다. 자세한 내용은 SQLite의 URI 파일 이름을 참조 하세요.

  • 메모리 내 캐시를 사용하려면 .를 사용합니다 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"]);
        }
    }
}

기본적으로 모든 테이블은 증분 동기화를 사용합니다. 새 레코드만 검색됩니다. 각 고유 쿼리에 대한 레코드가 포함됩니다(OData 쿼리의 MD5 해시를 만들어 생성됨).

참고 항목

첫 번째 인수는 디바이스로 PullItemsAsync 끌어올 레코드를 나타내는 OData 쿼리입니다. 클라이언트 쪽에서 복잡한 쿼리를 만드는 대신 사용자와 관련된 레코드만 반환하도록 서비스를 수정하는 것이 좋습니다.

개체에서 PullOptions 정의한 옵션은 일반적으로 설정할 필요가 없습니다. 표시되는 옵션은 다음과 같습니다.

  • PushOtherTables - true로 설정하면 모든 테이블이 푸시됩니다.
  • QueryId - 생성된 쿼리 ID가 아닌 사용할 특정 쿼리 ID입니다.
  • 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 쿼리가 반환하는 필드 이름과 일치해야 하며, 값은 필요한 형식의 기본값이어야 합니다. 숫자(long), false 부울 및 string.Empty 기타 모든 항목에 사용합니다0L.

SQLite에는 지원되는 형식의 제한적인 집합이 있습니다. 날짜/시간은 비교를 허용하기 위해 Epoch 이후의 시간(밀리초)으로 저장됩니다.

사용자 인증

Azure Mobile Apps를 사용하면 인증 호출을 처리하기 위한 인증 공급자를 생성할 수 있습니다. 서비스 클라이언트를 생성할 때 인증 공급자를 지정합니다.

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

인증이 필요할 때마다 토큰을 가져오기 위해 인증 공급자가 호출됩니다. 일반 인증 공급자는 인증 헤더 기반 인증과 App Service 인증 및 권한 부여 기반 인증 모두에 사용할 수 있습니다. 다음 모델을 사용합니다.

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 ID 플랫폼 사용

이 Microsoft ID 플랫폼 통해 Microsoft Entra ID와 쉽게 통합할 수 있습니다. 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;
    }
}

Microsoft ID 플랫폼 ASP.NET 6과 통합하는 방법에 대한 자세한 내용은 Microsoft ID 플랫폼 설명서를 참조하세요.

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

UserIdDisplayName 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 는 작업에 사용할 델타 토큰을 식별하는 데 사용되는 쿼리 ID를 지정합니다.
  • TimestampUpdatePolicy 는 제거 작업이 끝날 때 델타 토큰을 조정하는 방법을 지정합니다.
    • TimestampUpdatePolicy.NoUpdate 는 델타 토큰을 업데이트해서는 안 됨을 나타냅니다.
    • TimestampUpdatePolicy.UpdateToLastEntity 는 델타 토큰을 테이블에 저장된 마지막 엔터티의 필드로 업데이트 updatedAt 해야 했음을 나타냅니다.
    • TimestampUpdatePolicy.UpdateToNow 는 델타 토큰을 현재 날짜/시간으로 업데이트해야 했음을 나타냅니다.
    • TimestampUpdatePolicy.UpdateToEpoch 는 모든 데이터를 동기화하기 위해 델타 토큰을 다시 설정해야 했음을 나타냅니다.

데이터를 동기화하기 위해 호출 table.PullItemsAsync() 할 때 사용한 것과 동일한 QueryId 값을 사용합니다. 제거 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 동기화 이벤트와 관련이 없는 경우 또는 내 속성입니다.