다음을 통해 공유


방법: 변경 내용을 일괄 처리로 전달(SQL Server)

이 항목에서는 Sync Framework에서 SqlSyncProvider, SqlCeSyncProvider 또는 DbSyncProvider를 사용하는 데이터베이스 동기화의 변경 내용을 일괄 처리로 전달하는 방법에 대해 설명합니다. 이 항목의 코드에서는 다음과 같은 Sync Framework 클래스를 중점적으로 다룹니다.

일괄 처리 이해

기본적으로 Sync Framework에서는 각 노드에 대한 변경 내용을 단일 DataSet 개체로 전달합니다. 이 개체는 노드에 적용할 변경 내용으로 메모리에 유지됩니다. 변경 내용을 적용할 컴퓨터에 메모리가 충분하고 컴퓨터에 대한 연결이 안정적이면 기본 동작도 잘 작동합니다. 그러나 일부 응용 프로그램에서는 변경 내용을 일괄 처리로 나누는 것이 더욱 좋습니다. 동기화 응용 프로그램에 대해 다음과 같은 시나리오를 가정해 봅니다.

  • SqlCeSyncProvider를 사용하는 많은 수의 클라이언트가 SqlSyncProvider를 사용하는 서버와 정기적으로 동기화되는 경우

  • 각 클라이언트에 메모리 크기와 디스크 공간이 제한된 경우

  • 서버와 클라이언트 간의 연결에 느린 대역폭 및 중단 문제가 발생하여 종종 동기화 시간이 오래 걸리거나 연결이 끊어지는 경우

  • 일반적인 동기화 세션에 대한 변경 내용의 크기(KB)가 큰 경우

이러한 유형의 시나리오에서는 변경 내용을 일괄 처리하여 다음 기능을 활용하는 것이 좋습니다.

  • 개발자가 클라이언트에서 변경 내용을 저장하는 데 사용되는 메모리의 양(메모리 데이터 캐시 크기)을 제어할 수 있습니다. 이렇게 하면 클라이언트에서 메모리 부족 오류가 발생하지 않도록 방지할 수 있습니다.

  • Sync Framework에서 전체 변경 내용 집합을 시작하는 대신 현재 일괄 처리부터 시작하여 실패한 동기화 작업을 다시 시작할 수 있습니다.

  • 실패한 작업 때문에 서버에서 변경 내용을 다시 다운로드하거나 다시 열거할 필요가 없거나 적습니다.

2 계층 및 N 계층 응용 프로그램에 대해 일괄 처리를 구성하는 작업은 간단하며 초기 동기화 세션 및 이후 세션에서 해당 일괄 처리를 사용할 수 있습니다.

일괄 처리 구성 및 사용

Sync Framework에서 일괄 처리는 다음과 같이 작동합니다.

  1. 응용 프로그램에서 동기화 세션에 참여하고 있는 각 공급자에 대해 메모리 데이터 캐시 크기를 지정합니다.

    두 공급자가 캐시 크기를 지정할 경우 Sync Framework에서 두 공급자에 더 적은 값을 사용합니다. 실제 캐시 크기는 가장 작게 지정된 크기의 110%를 초과하지 않습니다. 동기화 세션 동안 단일 행이 크기의 110%를 넘으면 세션이 종료되고 예외가 발생합니다.

    값이 0(기본값)이면 일괄 처리가 사용되지 않습니다. 한 공급자는 일괄 처리를 사용하고 다른 공급자는 사용하지 않으면 업로드 및 다운로드 모두에 대해 일괄 처리가 사용됩니다.

  2. 응용 프로그램에서 각 공급자의 스풀링 파일 위치를 지정합니다. 기본적으로 스풀링 파일은 동기화 프로세스가 실행되는 계정의 임시 디렉터리에 작성됩니다.

  3. 응용 프로그램에서 Synchronize를 호출합니다.

  4. Sync Framework에서 행 단위로 변경 내용을 열거합니다. 원본 공급자의 메모리 데이터 캐시 크기에 도달하면 변경 내용이 로컬 스풀링 파일에 유지되고 메모리 내 데이터가 플러시됩니다. 모든 변경 내용이 열거될 때까지 이 프로세스가 계속됩니다.

  5. N 계층 시나리오의 경우 응용 프로그램의 서비스 및 프록시 코드에서 스풀링 파일을 대상으로 스트리밍합니다. 자세한 내용은 이 항목의 N 계층 전용 코드를 참조하십시오. 2 계층 시나리오에서는 이 경우 모든 동기화 코드가 대상에서 실행되므로 로컬 파일이 대상에 이미 있습니다.

  6. Sync Framework에서 스풀링 파일의 변경 내용을 역직렬화하고 이러한 변경 내용을 적용합니다. 모든 변경 내용이 대상에 적용될 때까지 이 프로세스가 계속됩니다.

    모든 일괄 처리는 하나의 트랜잭션으로 적용됩니다. 대상 공급자에서 마지막 일괄 처리를 받을 때까지는 해당 트랜잭션이 만들어지지 않습니다.

  7. 2 계층 시나리오의 경우 Sync Framework에서 스풀링 파일을 정리합니다. N 계층 시나리오의 경우에는 Sync Framework에서 동기화가 시작된 컴퓨터의 스풀링 파일을 정리하지만 중간 계층의 파일은 프록시에서 정리되어야 합니다. 이러한 내용은 이 항목 뒷부분의 샘플 Cleanup() 메서드에서 보여 줍니다. 세션이 중단되는 경우 처리하려면 중간 계층에서 프로세스를 사용하여 특정 날짜보다 오래된 파일을 정리해야 합니다.

참고

노드에 적용될 데이터 변경 내용은 DbChangesSelectedEventArgs 개체의 Context 속성에서 사용할 수 있습니다. 데이터가 일괄 처리되지 않는 경우 ChangesSelected 이벤트가 한 번만 발생하고, Context 속성에서 모든 변경 내용을 사용할 수 있습니다. 데이터가 일괄 처리되는 경우 각 일괄 처리에 대해 ChangesSelected가 발생하고 해당 일괄 처리의 변경 내용만 해당 시점에 사용할 수 있습니다. 모든 일괄 처리에 대해 변경 내용을 요구하는 경우 각 ChangesSelected 이벤트에 응답하고 반환된 데이터를 저장합니다.

다음 표에서는 일괄 처리와 관련된 형식 및 멤버에 대해 설명합니다. 일괄 처리에 필요한 유일한 속성은 MemoryDataCacheSize이지만 BatchingDirectory도 설정하는 것이 좋습니다.

형식 또는 멤버 설명

BatchingDirectory

배치 파일이 스풀링되는 디스크의 디렉터리를 가져오거나 설정합니다. 지정된 경로는 공급자에 대해 로컬인 디렉터리 또는 실행 중인 프록시여야 합니다. UNC 파일 경로와 파일이 아닌 URI 경로는 지원되지 않습니다.

참고

스풀링 파일에는 원시 데이터베이스 데이터가 포함됩니다. 파일이 작성되는 디렉터리는 적절한 액세스 제어로 보호해야 합니다.

CleanupBatchingDirectory

파일의 변경 내용이 대상에 적용된 후 일괄 처리 파일을 정리할지 여부를 가져오거나 설정합니다. 기본값은 파일을 정리하는 것입니다.

MemoryDataCacheSize

Sync Framework에서 변경 내용을 디스크로 스풀링하기 전에 해당 변경 내용을 캐시하는 데 사용하는 최대 메모리 크기(KB)를 가져오거나 설정합니다.

참고

이 설정은 대상으로 보내는 변경 내용에 대해 메모리에 유지되는 데이터 및 메타데이터의 크기에만 영향을 줍니다. 다른 Sync Framework 구성 요소 또는 사용자 응용 프로그램 구성 요소에서 사용되는 메모리는 제한하지 않습니다.

BatchApplied

각 변경 내용 일괄 처리가 대상에 적용된 후 발생하는 이벤트입니다.

BatchSpooled

각 변경 내용 일괄 처리가 디스크에 작성된 후 발생하는 이벤트입니다.

DbBatchAppliedEventArgs

현재 일괄 처리 번호 및 적용할 일괄 처리의 총 수를 포함한 BatchApplied 이벤트 데이터를 제공합니다.

DbBatchSpooledEventArgs

현재 일괄 처리 번호 및 일괄 처리 크기를 포함한 BatchSpooled 이벤트 데이터를 제공합니다.

BatchFileName

스풀링된 변경 내용이 작성되는 파일의 이름을 가져오거나 설정합니다.

IsDataBatched

데이터를 여러 일괄 처리로 보낼지 또는 단일 DataSet 개체로 보낼지를 가져오거나 설정합니다.

IsLastBatch

현재 일괄 처리가 마지막 변경 내용 일괄 처리인지 여부를 가져오거나 설정합니다.

BatchedDeletesRetried

변경 내용이 일괄 처리된 동기화 세션 중에 다시 시도된 삭제 작업 수를 가져오거나 설정합니다.

기본 키 및 외래 키 삭제의 순서 때문에 일괄 처리에 대한 삭제가 다시 시도됩니다. 외래 키 삭제가 현재 일괄 처리 또는 이전 일괄 처리에 없는 경우 해당 기본 키 삭제가 실패합니다. 실패한 삭제는 모든 일괄 처리가 적용된 후 한 번 다시 시도됩니다.

SelectIncrementalChangesCommand(DbSyncProvider에만 관련이 있음)

로컬 데이터베이스에서 증분 변경 내용을 선택하는 데 사용되는 쿼리 또는 저장 프로시저를 가져오거나 설정합니다.

참고

지정된 쿼리에 ORDER BY [sync_row_timestamp] 절을 포함하는 것이 좋습니다. 타임스탬프 값으로 행을 정렬하면 동기화 세션이 다시 시작될 경우 공급자에서 가장 높은 타임스탬프 워터마크부터 열거하기 시작하고(개별 테이블 워터마크는 각 일괄 처리에서 유지됨) 변경 내용이 손실되지 않습니다.

DataTable

동기화할 변경 내용이 포함된 DataTable 개체를 가져오거나 설정합니다. 일괄 처리를 사용하는 경우 이 속성에 액세스하면 디스크에서 스풀링된 파일이 역직렬화됩니다. 그러면 테이블에 수행된 모든 변경 내용이 다시 스풀링된 파일에 유지됩니다.

DataSet

피어 데이터베이스에서 선택한 행이 포함된 DataSet 개체를 가져오거나 설정합니다. IsDataBatchedtrue이면 null을 반환합니다.

2 계층 및 N 계층에 대한 공통 코드

이 섹션의 코드 예제에서는 2 계층 및 N 계층 시나리오에서 일괄 처리를 수행하는 방법을 보여 줍니다. 이 코드는 Sync Framework SDK에 포함된 두 샘플 SharingAppDemo-CEProviderEndToEndWebSharingAppDemo-CEProviderEndToEnd에서 가져온 것입니다. 각 예제에서 SharingAppDemo/CESharingForm 같은 코드 위치를 소개합니다. 일괄 처리의 관점에서 두 응용 프로그램 간의 주요 차이점은 N 계층의 경우 스풀링된 파일을 업로드 및 다운로드하고 변경 내용을 열거하는 각 노드에 대해 디렉터리를 만드는 추가 코드가 필요하다는 것입니다.

SharingAppDemo/CESharingFormsynchronizeBtn_Click 이벤트 처리기에서 가져온 다음 코드 예제에서는 메모리 데이터 캐시 크기 및 스풀링 파일을 작성할 디렉터리를 설정합니다. BatchingDirectory로 지정된 경로는 공급자에 대해 로컬인 디렉터리 또는 실행 중인 프록시여야 합니다. UNC 파일 경로와 파일이 아닌 URI 경로는 지원되지 않습니다. BatchingDirectory로 지정된 경로는 루트 디렉터리입니다. 각 동기화 세션에서 Sync Framework는 해당 세션의 스풀링 파일을 저장할 고유한 하위 디렉터리를 만듭니다. 이 디렉터리는 현재 원본-대상 조합에 대해 고유하므로 파일이 다른 세션과 격리됩니다.

WebSharingAppDemo/CESharingFormsynchronizeBtn_Click 이벤트 처리기에서 가져온 다음 코드 예제에서는 같은 속성을 설정하지만 대상의 일괄 처리 디렉터리를 프록시로 설정합니다. 2 계층 시나리오에서는 공급자로 직접 설정합니다.

//Set memory data cache size property. 0 represents non batched mode.
//No need to set memory cache size for Proxy, because the source is 
//enabled for batching: both upload and download will be batched.
srcProvider.MemoryDataCacheSize = this._batchSize;
 

//Set batch spool location. Default value if not set is %Temp% directory.
if (!string.IsNullOrEmpty(this.batchSpoolLocation.Text))
{
    srcProvider.BatchingDirectory = this.batchSpoolLocation.Text;
    destinationProxy.BatchingDirectory = this.batchSpoolLocation.Text;
}

두 응용 프로그램의 SynchronizationHelper 파일에서 가져온 다음 코드 예제에서는 변경 내용을 열거하고 적용하는 동안 공급자에서 발생한 BatchSpooledBatchAppliedEvents를 처리하는 메서드를 만듭니다.

void provider_BatchSpooled(object sender, DbBatchSpooledEventArgs e)
{
    this.progressForm.listSyncProgress.Items.Add("BatchSpooled event fired: Details");
    this.progressForm.listSyncProgress.Items.Add("\tSource Database :" + ((RelationalSyncProvider)sender).Connection.Database);
    this.progressForm.listSyncProgress.Items.Add("\tBatch Name      :" + e.BatchFileName);
    this.progressForm.listSyncProgress.Items.Add("\tBatch Size      :" + e.DataCacheSize);
    this.progressForm.listSyncProgress.Items.Add("\tBatch Number    :" + e.CurrentBatchNumber);
    this.progressForm.listSyncProgress.Items.Add("\tTotal Batches   :" + e.TotalBatchesSpooled);
    this.progressForm.listSyncProgress.Items.Add("\tBatch Watermark :" + ReadTableWatermarks(e.CurrentBatchTableWatermarks));
}
void provider_BatchApplied(object sender, DbBatchAppliedEventArgs e)
{
    this.progressForm.listSyncProgress.Items.Add("BatchApplied event fired: Details");
    this.progressForm.listSyncProgress.Items.Add("\tDestination Database   :" + ((RelationalSyncProvider)sender).Connection.Database);
    this.progressForm.listSyncProgress.Items.Add("\tBatch Number           :" + e.CurrentBatchNumber);
    this.progressForm.listSyncProgress.Items.Add("\tTotal Batches To Apply :" + e.TotalBatchesToApply);
}
//Reads the watermarks for each table from the batch spooled event. //The watermark denotes the max tickcount for each table in each batch.
private string ReadTableWatermarks(Dictionary<string, ulong> dictionary)
{
    StringBuilder builder = new StringBuilder();
    Dictionary<string, ulong> dictionaryClone = new Dictionary<string, ulong>(dictionary);
    foreach (KeyValuePair<string, ulong> kvp in dictionaryClone)
    {
        builder.Append(kvp.Key).Append(":").Append(kvp.Value).Append(",");
    }
    return builder.ToString();
}

N 계층 전용 코드

나머지 코드 예제는 WebSharingAppDemo의 N 계층 시나리오에만 적용됩니다. 관련 N 계층 코드는 다음 세 파일에 포함되어 있습니다.

  • 서비스 계약: IRelationalSyncContract

  • 웹 서비스: RelationalWebSyncService

  • 프록시: RelationalProviderProxy

두 공급자 SqlSyncProviderSqlCeSyncProvider는 모두 RelationalSyncProvider에서 파생되므로 이 코드는 두 공급자 모두에게 적용됩니다. 추가 저장 관련 기능은 공급자 형식에 따라 프록시와 서비스 파일로 구분됩니다.

N 계층 시나리오에서 일괄 처리가 수행되는 방법을 이해하려면 서버가 원본이고 클라이언트가 대상인 동기화 세션을 생각해 봅니다. 서버의 로컬 디렉터리에 변경 내용이 작성된 후 다운로드된 변경 내용에 대해 다음 프로세스가 발생합니다.

  1. 클라이언트 프록시에서 GetChangeBatch 메서드가 호출됩니다. 샘플 코드의 뒷부분에 설명된 대로 이 메서드에 일괄 처리를 수행하는 코드를 포함해야 합니다.

  2. 서비스를 통해 SqlSyncProvider에서 배치 파일을 가져옵니다. 서비스에서 전체 경로 정보가 제거되고 네트워크를 통해 파일 이름만 전송됩니다. 이렇게 하면 서버의 디렉터리 구조가 클라이언트에 노출되지 않습니다.

  3. GetChangeBatch에 대한 프록시 호출이 반환됩니다.

    1. 프록시에서 변경 내용이 일괄 처리되었음이 감지되므로 배치 파일 이름을 인수로 전달하여 DownloadBatchFile을 호출합니다.

    2. 이러한 배치 파일을 로컬로 저장하기 위한 세션의 고유한 디렉터리가 없는 경우 프록시에서 RelationalProviderProxy.BatchingDirectory 아래에 만듭니다. 디렉터리 이름은 변경 내용을 열거하고 있는 피어의 복제본 ID입니다. 이렇게 하면 프록시 및 서비스에서 각 열거 피어에 대해 하나의 고유한 디렉터리를 사용하게 됩니다.

  4. 프록시에서 파일을 다운로드하고 로컬로 저장합니다. 프록시는 컨텍스트의 파일 이름을 로컬 디스크에 대한 배치 파일의 새로운 전체 경로로 바꿉니다.

  5. 프록시에서 컨텍스트를 다시 조정자로 반환합니다.

  6. 프록시에서 마지막 일괄 처리를 받을 때까지 1-6단계를 반복합니다.

업로드된 변경 내용에 대해 다음 프로세스가 발생합니다.

  1. 조정자가 프록시에 대해 ProcessChangeBatch를 호출합니다.

  2. 프록시에서 배치 파일인지 확인하고 다음 단계를 수행합니다.

    1. 전체 경로 정보를 제거하고 네트워크를 통해 파일 이름만 전송합니다.

    2. HasUploadedBatchFile을 호출하여 파일이 이미 업로드되었는지 여부를 확인합니다. 업로드되었으면 C 단계는 수행하지 않아도 됩니다.

    3. HasUploadedBatchFile에서 false를 반환하면 서비스에 대해 UploadBatchFile을 호출하고 배치 파일 내용을 업로드합니다.

      서비스에서 UploadBatchFile에 대한 호출을 받고 일괄 처리를 로컬로 저장합니다. 디렉터리 만들기는 위의 4단계와 비슷합니다.

    4. 서비스에 대해 ApplyChanges를 호출합니다.

  3. 서버에서 ApplyChanges 호출을 받고 배치 파일인지 확인합니다. 컨텍스트의 파일 이름을 로컬 디스크에 대한 배치 파일의 새로운 전체 경로로 바꿉니다.

  4. 서버에서 DbSyncContext를 로컬 SqlSyncProvider로 전달합니다.

  5. 마지막 일괄 처리를 보낼 때까지 1-6단계를 반복합니다.

IRelationalSyncContract에서 가져온 다음 코드 예제에서는 스풀링된 파일을 중간 계층으로 주고 받는 사용되는 업로드 및 다운로드 메서드를 지정합니다.

[OperationContract(IsOneWay = true)]
void UploadBatchFile(string batchFileid, byte[] batchFile);

[OperationContract]
byte[] DownloadBatchFile(string batchFileId);

RelationalWebSyncService에서 가져온 다음 코드 예제에서는 계약에 정의된 UploadBatchFileDownloadBatchFile 메서드를 노출하고 다음 메서드에 추가 일괄 처리 관련 논리를 포함합니다.

  • Cleanup: 지정된 디렉터리 또는 임시 디렉터리(지정되지 않은 경우)에서 스풀링된 파일을 정리합니다.

  • GetChanges: 데이터가 일괄 처리되었는지 여부를 확인하고 일괄 처리되었으면 경로가 네트워크를 통해 전송되지 않도록 스풀링된 파일의 디렉터리 경로를 제거합니다. N 계층 시나리오에서는 네트워크 연결을 통해 전체 디렉터리 경로를 전송하면 보안 위험에 노출될 수 있습니다. 파일 이름은 GUID입니다.

  • HasUploadedBatchFile: 특정 배치 파일이 서비스에 이미 업로드되었는지 여부를 반환합니다.

  • ApplyChanges: 데이터가 일괄 처리되었는지 여부를 확인하고 일괄 처리되었으면 예상 배치 파일이 이미 업로드되었는지 여부를 확인합니다. 파일이 업로드되지 않았으면 예외가 발생합니다. ApplyChanges를 호출하기 전에 클라이언트에서 스풀링된 파일을 업로드해야 합니다.

public abstract class RelationalWebSyncService: IRelationalSyncContract
{
    protected bool isProxyToCompactDatabase;
    protected RelationalSyncProvider peerProvider;
    protected DirectoryInfo sessionBatchingDirectory = null;
    protected Dictionary<string, string> batchIdToFileMapper;
    int batchCount = 0;

    public void Initialize(string scopeName, string hostName)
    {
        this.peerProvider = this.ConfigureProvider(scopeName, hostName);
        this.batchIdToFileMapper = new Dictionary<string, string>();
    }

    public void Cleanup()
    {
        this.peerProvider = null;
        //Delete all file in the temp session directory
        if (sessionBatchingDirectory != null && sessionBatchingDirectory.Exists)
        {
            try
            {
                sessionBatchingDirectory.Delete(true);
            }
            catch 
            { 
                //Ignore 
            }
        }
    }

    public void BeginSession(SyncProviderPosition position)
    {
        Log("*****************************************************************");
        Log("******************** New Sync Session ***************************");
        Log("*****************************************************************");
        Log("BeginSession: ScopeName: {0}, Position: {1}", this.peerProvider.ScopeName, position);
        //Clean the mapper for each session.
        this.batchIdToFileMapper = new Dictionary<string, string>();

        this.peerProvider.BeginSession(position, null/*SyncSessionContext*/);
        this.batchCount = 0;
    }

    public SyncBatchParameters GetKnowledge()
    {
        Log("GetSyncBatchParameters: {0}", this.peerProvider.Connection.ConnectionString);
        SyncBatchParameters destParameters = new SyncBatchParameters();
        this.peerProvider.GetSyncBatchParameters(out destParameters.BatchSize, out destParameters.DestinationKnowledge);
        return destParameters;
    }

    public GetChangesParameters GetChanges(uint batchSize, SyncKnowledge destinationKnowledge)
    {
        Log("GetChangeBatch: {0}", this.peerProvider.Connection.ConnectionString);
        GetChangesParameters changesWrapper = new GetChangesParameters();
        changesWrapper.ChangeBatch  = this.peerProvider.GetChangeBatch(batchSize, destinationKnowledge, out changesWrapper.DataRetriever);

        DbSyncContext context = changesWrapper.DataRetriever as DbSyncContext;
        //Check to see if data is batched
        if (context != null && context.IsDataBatched)
        {
            Log("GetChangeBatch: Data Batched. Current Batch #:{0}", ++this.batchCount);
            //Dont send the file location info. Just send the file name
            string fileName = new FileInfo(context.BatchFileName).Name;
            this.batchIdToFileMapper[fileName] = context.BatchFileName;
            context.BatchFileName = fileName;
        }
        return changesWrapper;
    }

    public SyncSessionStatistics ApplyChanges(ConflictResolutionPolicy resolutionPolicy, ChangeBatch sourceChanges, object changeData)
    {
        Log("ProcessChangeBatch: {0}", this.peerProvider.Connection.ConnectionString);

        DbSyncContext dataRetriever = changeData as DbSyncContext;

        if (dataRetriever != null && dataRetriever.IsDataBatched)
        {
            string remotePeerId = dataRetriever.MadeWithKnowledge.ReplicaId.ToString();
            //Data is batched. The client should have uploaded this file to us prior to calling ApplyChanges.
            //So look for it.
            //The Id would be the DbSyncContext.BatchFileName which is just the batch file name without the complete path
            string localBatchFileName = null;
            if (!this.batchIdToFileMapper.TryGetValue(dataRetriever.BatchFileName, out localBatchFileName))
            {
                //Service has not received this file. Throw exception
                throw new FaultException<WebSyncFaultException>(new WebSyncFaultException("No batch file uploaded for id " + dataRetriever.BatchFileName, null));
            }
            dataRetriever.BatchFileName = localBatchFileName;
        }

        SyncSessionStatistics sessionStatistics = new SyncSessionStatistics();
        this.peerProvider.ProcessChangeBatch(resolutionPolicy, sourceChanges, changeData, new SyncCallbacks(), sessionStatistics);
        return sessionStatistics;
    }

    public void EndSession()
    {
        Log("EndSession: {0}", this.peerProvider.Connection.ConnectionString);
        Log("*****************************************************************");
        Log("******************** End Sync Session ***************************");
        Log("*****************************************************************");
        this.peerProvider.EndSession(null);
        Log("");
    }

    /// <summary>
    /// Used by proxy to see if the batch file has already been uploaded. Optimizes by not resending batch files.
    /// NOTE: This method takes in a file name as an input parameter and hence is suseptible for name canonicalization
    /// attacks. This sample is meant to be a starting point in demonstrating how to transfer sync batch files and is
    /// not intended to be a secure way of doing the same. This SHOULD NOT be used as such in production environment
    /// without doing proper security analysis.
    /// 
    /// Please refer to the following two MSDN whitepapers for more information on guidelines for securing Web servies.
    /// 
    /// Design Guidelines for Secure Web Applications - https://msdn.microsoft.com/en-us/library/aa302420.aspx (Refer InputValidation section)
    /// Architecture and Design Review for Security - https://msdn.microsoft.com/en-us/library/aa302421.aspx (Refer InputValidation section)
    /// </summary>
    /// <param name="batchFileId"></param>
    /// <returns>bool</returns>
    public bool HasUploadedBatchFile(String batchFileId, string remotePeerId)
    {
        this.CheckAndCreateBatchingDirectory(remotePeerId);

        //The batchFileId is the fileName without the path information in it.
        FileInfo fileInfo = new FileInfo(Path.Combine(this.sessionBatchingDirectory.FullName, batchFileId));
        if (fileInfo.Exists && !this.batchIdToFileMapper.ContainsKey(batchFileId))
        {
            //If file exists but is not in the memory id to location mapper then add it to the mapping
            this.batchIdToFileMapper.Add(batchFileId, fileInfo.FullName);
        }
        //Check to see if the proxy has already uploaded this file to the service
        return fileInfo.Exists;
    }

    /// <summary>
    /// NOTE: This method takes in a file name as an input parameter and hence is suseptible for name canonicalization
    /// attacks. This sample is meant to be a starting point in demonstrating how to transfer sync batch files and is
    /// not intended to be a secure way of doing the same. This SHOULD NOT be used as such in production environment
    /// without doing proper security analysis.
    /// 
    /// Please refer to the following two MSDN whitepapers for more information on guidelines for securing Web servies.
    /// 
    /// Design Guidelines for Secure Web Applications - https://msdn.microsoft.com/en-us/library/aa302420.aspx (Refer InputValidation section)
    /// Architecture and Design Review for Security - https://msdn.microsoft.com/en-us/library/aa302421.aspx (Refer InputValidation section)
    /// </summary>
    /// <param name="batchFileId"></param>
    /// <param name="batchContents"></param>
    /// <param name="remotePeerId"></param>
    public void UploadBatchFile(string batchFileId, byte[] batchContents, string remotePeerId)
    {
        Log("UploadBatchFile: {0}", this.peerProvider.Connection.ConnectionString);
        try
        {
            if (HasUploadedBatchFile(batchFileId, remotePeerId))
            {
                //Service has already received this file. So dont save it again.
                return;
            }
            
            //Service hasnt seen the file yet so save it.
            String localFileLocation = Path.Combine(sessionBatchingDirectory.FullName, batchFileId);
            FileStream fs = new FileStream(localFileLocation, FileMode.Create, FileAccess.Write);
            using (fs)
            {
                    fs.Write(batchContents, 0, batchContents.Length);
            }
            //Save this Id to file location mapping in the mapper object
            this.batchIdToFileMapper[batchFileId] = localFileLocation;
        }
        catch (Exception e)
        {
            throw new FaultException<WebSyncFaultException>(new WebSyncFaultException("Unable to save batch file.", e));
        }
    }

    /// <summary>
    /// NOTE: This method takes in a file name as an input parameter and hence is suseptible for name canonicalization
    /// attacks. This sample is meant to be a starting point in demonstrating how to transfer sync batch files and is
    /// not intended to be a secure way of doing the same. This SHOULD NOT be used as such in production environment
    /// without doing proper security analysis.
    /// 
    /// Please refer to the following two MSDN whitepapers for more information on guidelines for securing Web servies.
    /// 
    /// Design Guidelines for Secure Web Applications - https://msdn.microsoft.com/en-us/library/aa302420.aspx (Refer InputValidation section)
    /// Architecture and Design Review for Security - https://msdn.microsoft.com/en-us/library/aa302421.aspx (Refer InputValidation section)
    /// </summary>
    /// <param name="batchFileId"></param>
    /// <returns></returns>
    public byte[] DownloadBatchFile(string batchFileId)
    {
        try
        {
            Log("DownloadBatchFile: {0}", this.peerProvider.Connection.ConnectionString);
            Stream localFileStream = null;

            string localBatchFileName = null;

            if (!this.batchIdToFileMapper.TryGetValue(batchFileId, out localBatchFileName))
            {
                throw new FaultException<WebSyncFaultException>(new WebSyncFaultException("Unable to retrieve batch file for id." + batchFileId, null));
            }

            localFileStream = new FileStream(localBatchFileName, FileMode.Open, FileAccess.Read);
            byte[] contents = new byte[localFileStream.Length];
            localFileStream.Read(contents, 0, contents.Length);
            return contents;
        }
        catch (Exception e)
        {
            throw new FaultException<WebSyncFaultException>(new WebSyncFaultException("Unable to read batch file for id " + batchFileId, e));
        }
    }

    protected void Log(string p, params object[] paramArgs)
    {
        Console.WriteLine(p, paramArgs);
    }

    //Utility functions that the sub classes need to implement.
    protected abstract RelationalSyncProvider ConfigureProvider(string scopeName, string hostName);


    private void CheckAndCreateBatchingDirectory(string remotePeerId)
    {
        //Check to see if we have temp directory for this session.
        if (sessionBatchingDirectory == null)
        {
            //Generate a unique Id for the directory
            //We use the peer id of the store enumerating the changes so that the local temp directory is same for a given source
            //across sync sessions. This enables us to restart a failed sync by not downloading already received files.
            string sessionDir = Path.Combine(this.peerProvider.BatchingDirectory, "WebSync_" + remotePeerId);
            sessionBatchingDirectory = new DirectoryInfo(sessionDir);
            //Create the directory if it doesnt exist.
            if (!sessionBatchingDirectory.Exists)
            {
                sessionBatchingDirectory.Create();
            }
        }
    }
}

RelationalProviderProxy에서 가져온 다음 코드 예제에서는 속성을 설정하고 웹 서비스에서 메서드를 호출합니다.

  • BatchingDirectory: 응용 프로그램에서 중간 계층에 대한 일괄 처리 디렉터리를 설정할 수 있습니다.

  • EndSession: 지정된 디렉터리에서 스풀링된 파일을 정리합니다.

  • GetChangeBatch: DownloadBatchFile 메서드를 호출하여 변경 내용 일괄 처리를 다운로드합니다.

  • ProcessChangeBatch: UploadBatchFile 메서드를 호출하여 변경 내용 일괄 처리를 업로드합니다.

public abstract class RelationalProviderProxy : KnowledgeSyncProvider, IDisposable
{
    protected IRelationalSyncContract proxy;
    protected SyncIdFormatGroup idFormatGroup;
    protected string scopeName;
    protected DirectoryInfo localBatchingDirectory;

    //Represents either the SQL server host name or the CE database file name. Sql database name
    //is always peer1
    //For this sample scopeName is always Sales
    protected string hostName;

    private string batchingDirectory = Environment.ExpandEnvironmentVariables("%TEMP%");

    public string BatchingDirectory
    {
        get { return batchingDirectory; }
        set 
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException("value cannot be null or empty");
            }
            try
            {
                Uri uri = new Uri(value);
                if (!uri.IsFile || uri.IsUnc)
                {
                    throw new ArgumentException("value must be a local directory");
                }
                batchingDirectory = value;
            }
            catch (Exception e)
            {
                throw new ArgumentException("Invalid batching directory.", e);
            }
        }
    }

    public RelationalProviderProxy(string scopeName, string hostName)
    {
        this.scopeName = scopeName;
        this.hostName = hostName;
        this.CreateProxy();            
        this.proxy.Initialize(scopeName, hostName);
    }

    public override void BeginSession(SyncProviderPosition position, SyncSessionContext syncSessionContext)
    {
        this.proxy.BeginSession(position);
    }

    public override void EndSession(SyncSessionContext syncSessionContext)
    {
        proxy.EndSession();
        if (this.localBatchingDirectory != null && this.localBatchingDirectory.Exists)
        {
            //Cleanup batching releated files from this session
            this.localBatchingDirectory.Delete(true);
        }
    }

    public override ChangeBatch GetChangeBatch(uint batchSize, SyncKnowledge destinationKnowledge, out object changeDataRetriever)
    {
        GetChangesParameters changesWrapper = proxy.GetChanges(batchSize, destinationKnowledge);
        //Retrieve the ChangeDataRetriever and the ChangeBatch
        changeDataRetriever = changesWrapper.DataRetriever;

        DbSyncContext context = changeDataRetriever as DbSyncContext;
        //Check to see if the data is batched.
        if (context != null && context.IsDataBatched)
        {
            if (this.localBatchingDirectory == null)
            {
                //Retrieve the remote peer id from the MadeWithKnowledge.ReplicaId. MadeWithKnowledge is the local knowledge of the peer 
                //that is enumerating the changes.
                string remotePeerId = context.MadeWithKnowledge.ReplicaId.ToString();

                //Generate a unique Id for the directory.
                //We use the peer id of the store enumerating the changes so that the local temp directory is same for a given source
                //across sync sessions. This enables us to restart a failed sync by not downloading already received files.
                string sessionDir = Path.Combine(this.batchingDirectory, "WebSync_" + remotePeerId);
                this.localBatchingDirectory = new DirectoryInfo(sessionDir);
                //Create the directory if it doesnt exist.
                if (!this.localBatchingDirectory.Exists)
                {
                    this.localBatchingDirectory.Create();
                }
            }

            string localFileName = Path.Combine(this.localBatchingDirectory.FullName, context.BatchFileName);
            FileInfo localFileInfo = new FileInfo(localFileName);
            
            //Download the file only if doesnt exist
            FileStream localFileStream = new FileStream(localFileName, FileMode.Create, FileAccess.Write);
            if (!localFileInfo.Exists)
            {
                byte[] remoteFileContents = this.proxy.DownloadBatchFile(context.BatchFileName);
                using (localFileStream)
                {
                    localFileStream.Write(remoteFileContents, 0, remoteFileContents.Length);
                }
            }
            //Set DbSyncContext.Batchfile name to the new local file name
            context.BatchFileName = localFileName;
        }

        return changesWrapper.ChangeBatch;
    }

    public override FullEnumerationChangeBatch GetFullEnumerationChangeBatch(uint batchSize, SyncId lowerEnumerationBound, SyncKnowledge knowledgeForDataRetrieval, out object changeDataRetriever)
    {
        throw new NotImplementedException();
    }

    public override void GetSyncBatchParameters(out uint batchSize, out SyncKnowledge knowledge)
    {
        SyncBatchParameters wrapper = proxy.GetKnowledge();
        batchSize = wrapper.BatchSize;
        knowledge = wrapper.DestinationKnowledge;
    }

    public override SyncIdFormatGroup IdFormats
    {
        get
        {
            if (idFormatGroup == null)
            {
                idFormatGroup = new SyncIdFormatGroup();

                //
                // 1 byte change unit id (Harmonica default before flexible ids)
                //
                idFormatGroup.ChangeUnitIdFormat.IsVariableLength = false;
                idFormatGroup.ChangeUnitIdFormat.Length = 1;

                //
                // Guid replica id
                //
                idFormatGroup.ReplicaIdFormat.IsVariableLength = false;
                idFormatGroup.ReplicaIdFormat.Length = 16;


                //
                // Sync global id for item ids
                //
                idFormatGroup.ItemIdFormat.IsVariableLength = true;
                idFormatGroup.ItemIdFormat.Length = 10 * 1024;
            }

            return idFormatGroup;
        }
    }

    public override void ProcessChangeBatch(ConflictResolutionPolicy resolutionPolicy, ChangeBatch sourceChanges, object changeDataRetriever, SyncCallbacks syncCallbacks, SyncSessionStatistics sessionStatistics)
    {
        DbSyncContext context = changeDataRetriever as DbSyncContext;
        if (context != null && context.IsDataBatched)
        {
            string fileName = new FileInfo(context.BatchFileName).Name;

            //Retrieve the remote peer id from the MadeWithKnowledge.ReplicaId. MadeWithKnowledge is the local knowledge of the peer 
            //that is enumerating the changes.
            string peerId = context.MadeWithKnowledge.ReplicaId.ToString();

            //Check to see if service already has this file
            if (!this.proxy.HasUploadedBatchFile(fileName, peerId))
            {
                //Upload this file to remote service
                FileStream stream = new FileStream(context.BatchFileName, FileMode.Open, FileAccess.Read);
                byte[] contents = new byte[stream.Length];
                using (stream)
                {
                    stream.Read(contents, 0, contents.Length);
                }
                this.proxy.UploadBatchFile(fileName, contents, peerId);
            }

            context.BatchFileName = fileName;
        }
        this.proxy.ApplyChanges(resolutionPolicy, sourceChanges, changeDataRetriever);
    }

    public override void ProcessFullEnumerationChangeBatch(ConflictResolutionPolicy resolutionPolicy, FullEnumerationChangeBatch sourceChanges, object changeDataRetriever, SyncCallbacks syncCallbacks, SyncSessionStatistics sessionStatistics)
    {
        throw new NotImplementedException();
    }

    protected abstract void CreateProxy();

    #region IDisposable Members

    public void Dispose()
    {
        this.proxy.Cleanup();
        this.proxy = null;
        GC.SuppressFinalize(this);
    }

    #endregion
}

참고 항목

개념

SQL Server와 SQL Server Compact 동기화