Compartilhar via


Como entregar alterações em lotes (SQL Server)

Este tópico descreve como entregar alterações em lotes para sincronização de banco de dados no Sync Framework que usa SqlSyncProvider, SqlCeSyncProvider ou DbSyncProvider. O código neste tópico enfoca as seguintes classes do Sync Framework:

Entendendo o envio em lote

Por padrão, o Sync Framework entrega alterações para cada nó em um único objeto DataSet. Esse objeto é mantido na memória enquanto as alterações são aplicadas a um nó. O comportamento padrão funciona bem quando há memória suficiente no computador em que as alterações são aplicadas e a conexão com o computador é confiável. Porém, alguns aplicativos podem se beneficiar das alterações divididas em lotes. Considere o seguinte cenário para um aplicativo de sincronização:

  • Um grande número de clientes que usa SqlCeSyncProvider sincroniza periodicamente com um servidor que usa SqlSyncProvider.

  • Cada cliente tem uma quantidade limitada de memória e espaço em disco.

  • As conexões entre o servidor e os clientes são de largura de banda baixa e intermitentes, muitas vezes resultando em tempos de sincronização mais longos e em conexões descartadas.

  • O tamanho das alterações (em KB) em uma sessão de sincronização típica é grande.

As alterações em lote são ideais para esse tipo de cenário porque fornecem os seguintes recursos:

  • Permite que o desenvolvedor controle a quantidade de memória (o tamanho do cache de dados de memória) usada para armazenar as alterações no cliente. Isso pode eliminar erros de memória insuficiente no cliente.

  • Permitem que o Sync Framework reinicie uma operação de sincronização com falha desde o início do lote atual, em vez do início do conjunto inteiro de alterações.

  • Podem reduzir ou eliminar a necessidade de baixar ou enumerar novamente as alterações no servidor devido a operações com falha.

O envio em lote consiste basicamente em configurar aplicativos de 2 camadas e de N camadas, podendo ser usado na sessão de sincronização inicial e nas sessões subsequentes.

Configurando e usando o envio em lote

O envio em lote no Sync Framework funciona da seguinte maneira:

  1. O aplicativo especifica o tamanho do cache de dados da memória para cada provedor que está participando da sessão de sincronização.

    Se os dois os provedores especificarem um tamanho de cache, o Sync Framework usará o valor menor para ambos. O tamanho real do cache não será superior a 110% do menor tamanho especificado. Durante uma sessão de sincronização, se uma única linha for maior que 110% do tamanho, a sessão terminará com uma exceção.

    Um valor igual a 0 (o padrão) desabilita o processamento em lotes. Se um provedor tiver o processamento em lotes habilitado, e o outro provedor não, o processamento em lotes será habilitado para carregamento e download.

  2. O aplicativo especifica o local dos arquivos de spool para cada provedor. Por padrão, os arquivos de spool são gravados no diretório temporário da conta na qual o processo de sincronização é executado.

  3. O aplicativo chama Synchronize.

  4. O Sync Framework enumera as alterações em uma linha de cada vez. Se o tamanho do cache de dados de memória para o provedor de origem for atingido, as alterações serão mantidas em um arquivo de spool local e os dados de memória serão liberados. Esse processo continua até que todas as alterações sejam enumeradas.

  5. Nos cenários de N camadas, o código proxy e de serviço no aplicativo envia os arquivos de spool para o destino. Para obter mais informações, consulte Código específico para N camadas neste tópico. Nos cenários de duas camadas, o arquivo local já está no destino porque nesse caso todo o código de sincronização é executado no destino.

  6. O Sync Framework desserializa as alterações dos arquivos de spool e aplica essas alterações. Esse processo continua até que todas as alterações sejam aplicadas ao destino.

    Todos os lotes são aplicados em uma única transação. Essa transação só é criada depois que o último lote é recebido pelo provedor de destino.

  7. Nos cenários de duas camadas, o Sync Framework limpa o arquivo de spool. Nos cenários de N camadas, o Sync Framework limpa os arquivos de spool no computador no qual a sincronização é iniciada, mas os arquivos na camada intermediária devem ser excluídos pelo proxy (demonstrado no exemplo de método Cleanup() mais adiante neste tópico). Nos casos em que uma sessão é anulada, a camada intermediária também deve usar um processo para limpar os arquivos que sejam anteriores a determinada data.

Dica

As alterações de dados que serão aplicadas a um nó estão disponíveis na propriedade Context do objeto DbChangesSelectedEventArgs. Quando os dados não estiverem em lotes, o evento ChangesSelected é disparado apenas uma vez e todas as alterações estão disponíveis na propriedade Context. Quando os dados estão em lotes, ChangesSelected é disparado para cada lote e somente as alterações desse lote ficam disponíveis nesse momento. Se você exigir alterações de todos os lotes, responda a cada evento ChangesSelected e armazene os dados retornados.

A tabela a seguir descreve os tipos e os membros relacionados ao envio em lote. A única propriedade necessária para o envio em lote é MemoryDataCacheSize, mas também é recomendável definir BatchingDirectory.

Tipo ou membro Descrição

BatchingDirectory

Obtém ou define o diretório no qual arquivos em lotes são colocados em spool no disco. O caminho especificado deve ser um diretório local ao provedor ou proxy que está sendo executado. Não são suportados caminhos de arquivos UNC nem caminhos URI não relacionados a arquivos.

NoteImportante:
Arquivos de spool contêm dados de banco de dados brutos. O diretório no qual os arquivos são gravados deve ser protegido com os controles de acesso apropriados.

CleanupBatchingDirectory

Obtém ou define se os arquivos em lote devem ser limpos depois que as alterações nos arquivos tiverem sido aplicadas ao destino. O padrão é limpar os arquivos.

MemoryDataCacheSize

Obtém ou define a quantidade máxima de memória, em KB, que o Sync Framework usa para armazenar em cache as alterações antes de criar spool dessas alterações no disco.

NoteObservação:
Essa configuração afeta somente o tamanho dos dados e metadados mantidos na memória para as alterações enviadas ao destino. Ela não limita a memória usada pelos outros componentes do Sync Framework ou componentes de aplicativos de usuário.

BatchApplied

O evento que ocorre depois que cada lote de alterações é aplicado ao destino.

BatchSpooled

O evento que ocorre depois que cada lote de alterações é gravado em disco.

DbBatchAppliedEventArgs

Fornece dados para o evento BatchApplied, incluindo o número do lote atual e o número total de lotes a ser aplicado.

DbBatchSpooledEventArgs

Fornece dados para o evento BatchSpooled, incluindo o número do lote atual e o tamanho do lote.

BatchFileName

Obtém ou define o nome do arquivo no qual as alterações em spool são gravadas.

IsDataBatched

Obtém ou define se os dados são enviados em vários lotes ou em um único objeto DataSet.

IsLastBatch

Obtém ou define se o lote atual é o último lote de alterações.

BatchedDeletesRetried

Obtém ou define o número de operações de exclusão que foram repetidas durante uma sessão de sincronização na qual alterações foram processadas em lotes.

As exclusões são repetidas para os lotes por causa da ordem das exclusões de chave primária e chave estrangeira. Se a exclusão de uma chave estrangeira não existir no lote atual ou em um lote anterior, a exclusão da chave primária correspondente falhará. As exclusões com falha são repetidas uma vez depois que todos os lotes são aplicados.

SelectIncrementalChangesCommand (relevante apenas para DbSyncProvider)

Obtém ou define a consulta ou o procedimento armazenado usado para selecionar alterações incrementais no banco de dados local.

NoteObservação:
Recomenda-se que a consulta especificada inclua a cláusula ORDER BY [sync_row_timestamp]. A ordenação de linhas pelo valor de carimbo de data/hora garante que se uma sessão de sincronização for reiniciada, o provedor começará a enumerar a partir da marca-d'água mais alta de carimbo de data/hora (as marcas-d'água de tabela individuais são mantidas em cada lote) e nenhuma alteração será perdida.

DataTable

Obtém ou define o objeto DataTable que contém as alterações a serem sincronizadas. Se o processamento em lotes for habilitado, o acesso a essa propriedade irá desserializar o arquivo em spool do disco. Qualquer alteração feita nas tabelas será mantida no arquivo em spool.

DataSet

Obtém ou define um objeto DataSet que contém as linhas selecionadas no banco de dados par. Retornará valores nulos se IsDataBatched for true.

Código comum para duas camadas e N camadas

Os exemplos de código nesta seção demonstram como tratar o processamento em lotes nos cenários de 2 camadas e de N camadas. Esse código é retirado de dois exemplos incluídos no SDK do Sync Framework: SharingAppDemo-CEProviderEndToEnd e WebSharingAppDemo-CEProviderEndToEnd. Cada exemplo é introduzido com o local do código, como SharingAppDemo/CESharingForm. Em termos de envio em lote, a principal diferença entre os dois aplicativos é o código adicional exigido no caso de N camadas para carregar e baixar os arquivos em spool e criar diretórios para cada nó que enumera as alterações.

O exemplo de código a seguir do manipulador de eventos synchronizeBtn_Click no SharingAppDemo/CESharingForm define o tamanho do cache de dados de memória e o diretório no qual os arquivos de spool devem ser gravados. O caminho especificado para BatchingDirectory deve ser um diretório local ao provedor ou proxy que está sendo executado. Não são suportados caminhos de arquivos UNC nem caminhos URI não relacionados a arquivos. O caminho especificado para BatchingDirectory é o diretório raiz. Durante cada sessão de sincronização, o Sync Framework cria um subdiretório exclusivo no qual armazenar arquivos de spool durante a sessão. Esse diretório é exclusivo para a combinação atual de origem-destino para isolar os arquivos de sessões diferentes.

O exemplo de código a seguir do manipulador de eventos synchronizeBtn_Click no WebSharingAppDemo/CESharingForm define as mesmas propriedades, mas o diretório de envio em lote do destino é definido para o proxy, e não diretamente para o provedor como no cenário de 2 camadas:

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

Os exemplos de código a seguir do arquivo SynchronizationHelper, em ambos os aplicativos, criam métodos para manipular BatchSpooled e BatchAppliedEvents, que são gerados por um provedor durante a enumeração e a aplicação de alterações:

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

Código específico para N camadas

O restante dos exemplos de código aplica-se somente ao cenário de N camadas em WebSharingAppDemo. O código relevante de N camadas está contido em três arquivos:

  • O contrato de serviço: IRelationalSyncContract

  • O serviço Web: RelationalWebSyncService

  • O proxy: RelationalProviderProxy

O dois provedores SqlSyncProvider e SqlCeSyncProvider herdam de RelationalSyncProvider, por isso esse código se aplica a ambos. A funcionalidade adicional específica de repositório é separada em arquivos de proxy e de serviço para cada tipo de provedor.

Para entender como é feito o processamento em lotes em um cenário de N camadas, considere uma sessão de sincronização na qual o servidor é a origem e o cliente é o destino. Depois que as alterações forem gravadas no diretório local do servidor, o seguinte processo ocorre para as alterações baixadas:

  1. O método GetChangeBatch é chamado no proxy cliente. Como será demonstrado posteriormente no código de exemplo, esse método deverá incluir código específico para manipular o envio em lote.

  2. O serviço obtém um arquivo em lotes de SqlSyncProvider. O serviço remove as informações de caminho completas e envia somente o nome do arquivo pela rede. Isso evita que a estrutura de diretórios do servidor seja exposta aos clientes.

  3. A chamada do proxy para GetChangeBatch é retornada.

    1. O proxy detecta que as alterações foram colocadas em lote, por isso ele chama DownloadBatchFile, transmitindo o nome do arquivo em lotes como um argumento.

    2. O proxy cria um diretório exclusivo (se não existir um para a sessão) em RelationalProviderProxy.BatchingDirectory para armazenar esses arquivos em lote localmente. O nome do diretório é a ID da réplica do par que está enumerando alterações. Isso assegura que o proxy e o serviço tenham um diretório exclusivo para cada par da enumeração.

  4. O proxy baixa o arquivo e o armazena localmente. O proxy substitui o nome do arquivo no contexto pelo novo caminho completo para o arquivo em lotes no disco local.

  5. O proxy retorna o contexto ao organizador.

  6. Repita as etapas de 1 a 6 até que o último lote seja recebido pelo proxy.

O processo a seguir ocorre para as alterações carregadas

  1. O orquestrador chama ProcessChangeBatch no proxy.

  2. O proxy determina que é um arquivo em lotes e executa as seguintes etapas:

    1. Remove as informações de caminho completas e envia somente o nome do arquivo pela rede.

    2. Chama HasUploadedBatchFile para determinar se o arquivo já foi carregado. Em caso afirmativo, a etapa C não será necessária.

    3. Se HasUploadedBatchFile retornar false, ele chamará UploadBatchFile no serviço e carregará o conteúdo do arquivo em lotes.

      O serviço receberá a chamada para UploadBatchFile e armazenará o lote localmente. A criação de diretórios é semelhante à etapa 4 acima.

    4. Chama ApplyChanges no serviço.

  3. O servidor recebe a chamada ApplyChanges e determina que ela é um arquivo em lotes. Ele substitui o nome do arquivo no contexto pelo novo caminho completo para o arquivo em lotes no disco local.

  4. O servidor transmite o DbSyncContext ao SqlSyncProvider local.

  5. Repita as etapas de 1 a 6 até que o último lote seja enviado.

O exemplo de código a seguir de IRelationalSyncContract especifica métodos de carregamento e download que são usados para transferir arquivos em spool na camada intermediária:

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

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

Os exemplos de código a seguir de RelationalWebSyncService expõem os métodos UploadBatchFile e DownloadBatchFile definidos no contrato e incluem lógica adicional relacionada ao envio em lote nos seguintes métodos:

  • Cleanup: limpa qualquer arquivo em spool de um diretório especificado ou do diretório temporário se nenhum for especificado.

  • GetChanges: verifica se dados são processados em lotes e, nesse caso, remove o caminho de diretório do arquivo em spool de forma que o caminho não seja enviado pela rede. Nos cenários de N camadas, é um risco de segurança enviar caminhos de diretório completos por uma conexão de rede. O nome do arquivo é um GUID.

  • HasUploadedBatchFile: retorna se um arquivo em lotes específico já foi carregado no serviço.

  • ApplyChanges: verifica se dados são colocados em lotes e, nesse caso, verificará se o arquivo em lotes esperado já foi carregado. Se o arquivo não tiver sido carregado, uma exceção será lançada. O cliente deveria ter carregado o arquivo em spool antes de chamar 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();
            }
        }
    }
}

Os exemplos de código de RelationalProviderProxy a seguir definem propriedades e chamam métodos no serviço Web:

  • BatchingDirectory: permite que o aplicativo defina o diretório do envio em lote para a camada intermediária.

  • EndSession: limpa qualquer arquivo em spool de um diretório especificado.

  • GetChangeBatch: baixa lotes de alterações chamando o método DownloadBatchFile.

  • ProcessChangeBatch: carrega lotes de alterações chamando o método 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
}

Consulte também

Conceitos

Sincronizando o SQL Server e o SQL Server Compact