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


Как доставить изменения в пакетах (SQL Server)

В этом разделе описывается, как выполнять пакетную передачу изменений для синхронизации в платформе Sync Framework баз данных с помощью поставщиков SqlSyncProvider, SqlCeSyncProvider или DbSyncProvider. Код из этого раздела построен на следующих классах Sync Framework:

Основные сведения о пакетной передаче

По умолчанию платформа Sync Framework передает изменения на каждый узел в виде отдельного объекта DataSet. Пока изменения применяются к узлу, этот объект хранится в памяти. Такое поведение по умолчанию вполне применимо, если компьютер, на котором применяются изменения, располагает достаточным объемом памяти, а соединение надежное. Однако для некоторых приложений удобнее, если изменения разделены на пакеты. Рассмотрите следующий сценарий для приложения синхронизации.

  • Большое количество клиентов, использующих SqlCeSyncProvider, периодически синхронизируются с сервером, использующим SqlSyncProvider.

  • Объем памяти и место на диске ограничены на каждом клиенте.

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

  • Изменения, передаваемые в ходе типичного сеанса синхронизации, имеют большой размер (в КБ).

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

  • Позволяет разработчику управлять объемом памяти (размера кэша данных в памяти), используемой для хранения изменений на клиенте. Это позволяет избежать возникновения ошибок нехватки памяти на клиенте.

  • Позволяет Sync Framework перезапускать неуспешно завершившуюся операцию синхронизации с начала текущего пакета, а не с начала всего набора изменений.

  • Может снизить или устранить потребность в повторной загрузке или повторном перечислении изменений на сервере в результате ошибочных операций.

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

Настройка и использование пакетной передачи

Пакетная передача в платформе Sync Framework работает следующим образом.

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

    Если оба поставщика указывают размер кэша, то Sync Framework использует для обоих поставщиков меньшее значение из указанных. Фактический размер кэша не будет превышать 110 % от меньшего из указанных размеров. Если в ходе сеанса синхронизации размер одной строки превысит 100 % от размера кэша, сеанс будет завершен с вызовом исключения.

    Значение 0 (по умолчанию) отключает пакетную передачу. Если пакетная передача включена только в одном поставщике, то пакетная передача выполняется и для отправки, и для загрузки.

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

  3. Приложение вызывает метод Synchronize.

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

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

  6. Sync Framework выполняет десериализацию изменений из файлов буферизации и применяет эти изменения. Этот процесс продолжается до завершения применения всех изменений в назначении.

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

  7. В двухуровневых сценариях платформа Sync Framework очищает файл буферизации. В многоуровневых сценариях платформа Sync Framework очищает файлы буферизации на компьютере, где запущена синхронизация, однако файлы на промежуточном уровне должны очищаться учетной записью-посредником (как показано в образце метода Cleanup() далее в этом разделе). Для обработки ситуаций, в которых работа сеанса прерывается, на среднем уровне должен применяться процесс очистки файлов старше определенного срока.

Примечание

Изменения данных, применяемые к узлу, доступны через свойство Context объекта DbChangesSelectedEventArgs. Если данные не имеют пакетной структуры, то событие ChangesSelected вызывается только один раз и все изменения доступны через свойство Context. Если данные имеют пакетную структуру, то событие ChangesSelected вызывается для каждого пакета и во время этого вызова доступны только изменения из этого пакета. Если необходимы изменения из всех пакетов, то обработайте все события ChangesSelected, сохранив возвращаемые данные.

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

Тип или элемент

Описание

BatchingDirectory

Возвращает или задает каталог, в котором сохраняются пакетные файлы, сохраняемые на диске. Указанный путь должен быть локальным каталогом выполняющегося поставщика или учетной записью-посредником. Пути к файлам в формате UNC и пути URI к объектам, не являющимся файлами, не поддерживаются.

Внимание!

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

CleanupBatchingDirectory

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

MemoryDataCacheSize

Возвращает или задает максимальный объем памяти (в КБ), используемой Sync Framework для кэширования изменений перед сохранением на диске. 

Примечание

Этот параметр влияет только на размер данных и метаданных, хранящихся в памяти для изменений, отправленных в назначение. Он не ограничивает объем памяти, используемой компонентами Sync Framework и компонентами пользовательских приложений.

BatchApplied

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

BatchSpooled

Событие, вызываемое после записи каждого пакета изменений на диск.

DbBatchAppliedEventArgs

Предоставляет данные для события BatchApplied, включая номер текущего пакета и общее количество применяемых пакетов.

DbBatchSpooledEventArgs

Предоставляет данные для события BatchSpooled, включая номер текущего пакета и размер пакета.

BatchFileName

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

IsDataBatched

Возвращает или задает значение, определяющее способ отправки данных — несколькими пакетами или единым объектом DataSet.

IsLastBatch

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

BatchedDeletesRetried

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

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

SelectIncrementalChangesCommand (применимо только для DbSyncProvider)

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

Примечание

Рекомендуется включать в указываемый запрос предложение ORDER BY [sync_row_timestamp]. Сортировка строк по значению временной метки гарантирует, что в случае перезапуска сеанса синхронизации поставщик начнет перечисление с максимальной отметки времени (в каждом пакете сохраняются служебные отметки для каждой таблицы) и изменения не будут потеряны.

DataTable

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

DataSet

Возвращает или задает объект DataSet, который содержит строки из одноранговой базы данных. Возвращает значение NULL, если свойство IsDataBatched имеет значение true.

Общий код для двухуровневых и многоуровневых сценариев

В примерах кода из этого раздела показано, как обрабатывать пакетную передачу в двухуровневых и многоуровневых сценариях. Этот код взят из двух образцов, входящих в пакет SDK платформы Sync Framework: SharingAppDemo-CEProviderEndToEnd и WebSharingAppDemo-CEProviderEndToEnd. В начале каждого примера указывается расположение кода, например SharingAppDemo/CESharingForm. С точки зрения пакетной передачи, главным различием между этими двумя приложениями является дополнительный код, необходимый в многоуровневом сценарии для отправки и загрузки файлов буферизации и создания каталогов для каждого узла, выполняющего перечисление изменений.

В следующем примере кода из обработчика события synchronizeBtn_Click в SharingAppDemo/CESharingForm задается размер кэша данных в памяти и каталог, в который будут записываться файлы буферизации. Путь, указываемый в BatchingDirectory, должен быть локальным каталогом выполняющегося поставщика или учетной записью-посредником. Пути к файлам в формате UNC и пути URI к объектам, не являющимся файлами, не поддерживаются. Путь, указанный для BatchingDirectory, является корневым каталогом. Для каждого сеанса синхронизации платформа Sync Framework создает уникальный вложенный каталог, в котором хранятся файлы буферизации для этого сеанса. Этот каталог является уникальным для текущего сочетания источника и назначения, что позволяет изолировать файлы, используемые в различных сеансах. При выборе каталога пакетной обработки необходимо учитывать возможные побочные эффекты. Например, если поставщик размещен в службах Internet Information Services (IIS), не следует в качестве каталога пакетной обработки использовать виртуальный каталог IIS. Изменение элементов в виртуальном каталоге может привести к перезапуску IIS, что приведет к ошибке синхронизации.

В следующем примере кода из обработчика события synchronizeBtn_Click в WebSharingAppDemo/CESharingForm задаются те же свойства, однако каталог пакетной передачи для адреса назначения задается для прокси-поставщика, а не сразу для поставщика, как в двухуровневом сценарии:

//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 в обоих приложениях создаются методы для обработки событий BatchSpooled и BatchAppliedEvents, которые вызываются поставщиком в ходе перечисления и применения изменений:

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

Код, относящийся к многоуровневым сценариям

Остальные примеры кода относятся только к многоуровневому сценарию в WebSharingAppDemo. Код, относящийся к многоуровневым сценариям, содержится в следующих трех файлах.

  • Контракт службы: IRelationalSyncContract

  • Веб-служба: RelationalWebSyncService

  • Учетная запись-посредник: RelationalProviderProxy

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

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

  1. На посреднике клиента вызывается метод GetChangeBatch. Как показано далее в образце кода, этот метод должен содержать специальный код для обработки пакетной передачи.

  2. Служба получает файл пакета от поставщика SqlSyncProvider. Служба удаляет полный путь и отправляет по сети только имя файла. Это предотвращает передачу сведений о серверной структуре каталогов на клиенты.

  3. Вызов посредником метода GetChangeBatch завершает работу.

    1. Посредник обнаруживает, что изменения организованы в пакеты, и вызывает метод DownloadBatchFile, передавая имя файла пакета в качестве аргумента.

    2. Посредник создает уникальный каталог (если для сеанса еще не существует каталог) в каталоге RelationalProviderProxy.BatchingDirectory для локального хранения этих файлов пакетов. Именем каталога служит идентификатор реплики однорангового узла, который выполняет перечисление изменений. Это гарантирует наличие в службе и учетной записи-посреднике уникального каталога для каждого однорангового узла, выполняющего перечисление.

  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 доступны методы UploadBatchFile и DownloadBatchFile, определенные в контракте, и приводится дополнительная логика обработки пакетной передачи в следующих методах:

  • Cleanup: очищает все файлы буферизации из указанного каталога (или временного каталога, если каталог не указан).

  • GetChanges: проверяет, имеют ли данные пакетную структуру, и в таком случае удаляет путь к каталогу файла буферизации, чтобы путь не отправлялся по сети. В многоуровневых сценариях отправка полных путей к каталогам по сетевому подключению представляет угрозу безопасности. Именем файла является идентификатор 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