Freigeben über


Vorgehensweise: Übermitteln von Änderungen in Batches (SQL Server)

In diesem Thema wird beschrieben, wie Änderungen in Batches für die Datenbanksynchronisierung in Sync Framework übermittelt werden, die SqlSyncProvider, SqlCeSyncProvider oder DbSyncProvider verwendet. Der Code für dieses Thema ist auf die folgenden Sync Framework-Klassen ausgerichtet:

Grundlegendes zur Batchverarbeitung

Standardmäßig werden in Sync Framework Änderungen zu den einzelnen Knoten in einem einzelnen DataSet-Objekt übermittelt. Dieses Objekt wird im Arbeitsspeicher beibehalten, wenn für einen Knoten Änderungen übernommen werden. Das Standardverhalten gilt, wenn auf dem Computer, auf dem Änderungen übernommen werden, genügend Arbeitsspeicher vorhanden ist und wenn die Verbindung mit diesem Computer zuverlässig ist. Für einige Anwendungen ist es jedoch vorteilhaft, Änderungen in Batches zu unterteilen. Ziehen Sie für eine Synchronisierungsanwendung folgendes Szenario in Betracht:

  • Eine große Anzahl von Clients, die SqlCeSyncProvider verwenden, führt die Synchronisierung in regelmäßigen Abständen mit einem Server aus, der SqlSyncProvider verwendet.

  • Die einzelnen Clients verfügen über beschränkten Arbeitsspeicher und Speicherplatz.

  • Die Verbindungen zwischen dem Server und den Clients weisen niedrige Bandbreiten und Unterbrechungen auf, sodass häufig lange Synchronisierungszeiten und abgebrochene Verbindungen auftreten.

  • Die Größe der Änderungen (in KB) einer typischen Synchronisierungssitzung ist umfangreich.

Eine Batchverarbeitung der Änderungen ist für diese Art von Szenario ideal, da sie folgende Funktionen bietet:

  • Der Entwickler kann den zum Speichern von Änderungen auf dem Client verwendeten Umfang an Arbeitsspeicher (die Datencachegröße des Arbeitsspeichers) steuern. Dadurch können Fehler durch ungenügenden Arbeitsspeicher auf dem Client vermieden werden.

  • Fehlgeschlagene Synchronisierungsvorgänge können in Sync Framework vom aktuellen Batch neu gestartet werden, sodass nicht der gesamte Änderungssatz neu gestartet werden muss.

  • Die Notwendigkeit, Änderungen aufgrund fehlgeschlagener Vorgänge erneut herunterzuladen oder neu aufzulisten kann verringert oder vermieden werden.

Eine Batchverarbeitung ist für 2- oder n-Ebenen-Anwendungen einfach zu konfigurieren. Zudem kann sie sowohl für die Erstsynchronisierungssitzung als auch für nachfolgende Sitzungen verwendet werden.

Konfigurieren und Verwenden der Batchverarbeitung

Die Batchverarbeitung in Sync Framework funktioniert wie folgt:

  1. Die Anwendung gibt die Datencachegröße des Arbeitsspeichers für jeden Anbieter an, der an der Synchronisierungssitzung teilnimmt.

    Wenn beide Anbieter eine Cachegröße angeben, wird in Sync Framework für beide Anbieter der kleinere Wert verwendet. Die tatsächliche Cachegröße ist nicht größer als 110 % der kleinsten angegebenen Größe. Wenn bei einer Synchronisierungssitzung eine einzelne Zeile 110 % größer als diese Größe ist, wird die Sitzung mit einer Ausnahme beendet.

    Der (Standard-)Wert „0“ deaktiviert die Batchverarbeitung. Wenn ein Anbieter die Batchverarbeitung aktiviert hat, und der andere nicht, wird die Batchverarbeitung sowohl für den Upload als auch für den Download aktiviert.

  2. Die Anwendung gibt den Speicherort der Spooldateien für die einzelnen Anbieter an. In der Standardeinstellung werden die Spooldateien in das temporäre Verzeichnis des Kontos geschrieben, unter dem der Synchronisierungsvorgang ausgeführt wird.

  3. Die Anwendung ruft Synchronize auf.

  4. Sync Framework listet die Änderungen für jeweils eine Zeile auf. Wenn die Datencachegröße des Arbeitsspeichers für den Quellenanbieter erreicht wird, werden die Änderungen an einer lokalen Spooldatei beibehalten, und die Daten im Arbeitsspeicher werden geleert. Dieser Prozess wird durchgeführt, bis alle Änderungen aufgelistet werden.

  5. Bei n-Ebenen-Szenarien werden die Spooldateien mithilfe des Dienst- und Proxycodes der Anwendung an das Ziel gesendet. Weitere Informationen dazu finden Sie im Abschnitt Code für n Ebenen in diesem Thema. In 2-Ebenen-Szenarien befindet sich die lokale Datei bereits am Ziel, da der gesamte Synchronisierungscode am Ziel ausgeführt wird.

  6. In Sync Framework wird die Serialisierung der Änderungen von den Spooldateien aufgehoben. Anschließend werden diese Änderungen übernommen. Dieser Prozess wird durchgeführt, bis alle Änderungen für das Ziel übernommen wurden.

    Alle Batches werden in einer Transaktion übernommen. Diese Transaktion wird erst erstellt, nachdem der letzte Batch vom Zielanbieter empfangen wurde.

  7. In 2-Ebenen-Szenarien wird die Spooldatei in Sync Framework bereinigt. In n-Ebenen-Szenarien werden die Spooldateien in Sync Framework auf dem Computer bereinigt, auf dem die Synchronisierung initiiert wurde. Dateien der mittleren Ebene sollten jedoch auf dem Proxy bereinigt werden (wie im Beispielcode für die Cleanup()-Methode weiter unten in diesem Thema). Um mit Fällen umgehen zu können, in denen eine Sitzung abgebrochen wird, sollte auch die mittlere Ebene einen Prozess verwenden, mit dem Dateien, die älter als ein bestimmtes Datum sind, bereinigt werden.

Hinweis

Die auf einen Knoten angewendeten Datenänderungen stehen über die Context-Eigenschaft des DbChangesSelectedEventArgs-Objekts zur Verfügung. Wenn die Daten nicht in Batches verarbeitet werden, wird das ChangesSelected-Ereignis nur einmal ausgelöst, und alle Änderungen sind über die Context-Eigenschaft verfügbar. Wenn Daten in Batches verarbeitet werden, wird ChangesSelected für jeden Batch ausgelöst, und nur die Änderungen aus dem Batch sind zu dieser Zeit verfügbar. Wenn Sie die Änderungen aus allen Batches benötigen, reagieren Sie auf jedes ChangesSelected-Ereignis und speichern die zurückgegebenen Daten.

In der folgenden Tabelle werden die Typen und Elemente beschrieben, die mit der Batchverarbeitung in Zusammenhang stehen. Die einzige für die Batchverarbeitung erforderliche Eigenschaft ist MemoryDataCacheSize. Es empfiehlt sich jedoch, auch BatchingDirectory festzulegen.

Typ oder Element Beschreibung

BatchingDirectory

Ruft das Verzeichnis ab, in dem Batchdateien auf den Datenträger gespoolt werden, oder legt dieses fest. Beim angegebenen Pfad muss es sich um ein lokales Verzeichnis des ausführenden Anbieters oder Proxys handeln. UNC-Dateipfade und nicht dateibezogene URI-Pfade werden nicht unterstützt.

Hinweis

Spooldateien enthalten unformatierte Datenbankdaten. Das Verzeichnis, in das die Dateien geschrieben werden, muss mit den entsprechenden Zugriffssteuerungen geschützt werden.

CleanupBatchingDirectory

Ruft ab oder legt fest, ob Batchdateien im Anschluss an die Übernahme der Änderungen für das Ziel bereinigt werden sollen. In der Standardeinstellung werden die Dateien bereinigt.

MemoryDataCacheSize

Ruft (in KB) die Höchstmenge an Arbeitsspeicher ab oder legt sie fest, die in Sync Framework verwendet wird, um Änderungen zwischenzuspeichern, bevor diese auf dem Datenträger gespoolt werden.

Hinweis

Diese Einstellung wirkt sich nur auf die Größe der Daten und Metadaten aus, die im Arbeitsspeicher für Änderungen beibehalten werden, die an das Ziel gesendet werden. Der von anderen Sync Framework- oder Benutzeranwendungskomponenten verwendete Arbeitsspeicher wird nicht eingeschränkt.

BatchApplied

Das Ereignis, das auftritt, nachdem die einzelnen Änderungsbatches für das Ziel übernommen wurden.

BatchSpooled

Das Ereignis, das nach den einzelnen Änderungsbatches auftritt, wurde auf den Datenträger geschrieben.

DbBatchAppliedEventArgs

Stellt Daten für das Ereignis BatchApplied bereit, einschließlich der aktuellen Batchnummer und der Gesamtzahl der zu übernehmenden Batches.

DbBatchSpooledEventArgs

Stellt Daten für das Ereignis BatchSpooled bereit, einschließlich der aktuellen Batchnummer und der Batchgröße.

BatchFileName

Ruft den Namen der Datei ab, in die gespoolte Änderungen geschrieben werden, oder legt ihn fest.

IsDataBatched

Ruft ab oder legt fest, ob Daten in mehreren Batches oder in einem einzelnen DataSet-Objekt gesendet werden.

IsLastBatch

Ruft ab oder legt fest, ob es sich beim aktuellen Batch um den letzten Änderungsbatch handelt.

BatchedDeletesRetried

Ruft die Anzahl von Löschvorgängen ab oder legt diese fest, die in einer Synchronisierungssitzung wiederholt wurden, in der Änderungen als Batches verarbeitet wurden.

Löschvorgänge werden für Batches aufgrund der Sortierung der Löschvorgänge von Primärschlüsseln und Fremdschlüsseln wiederholt. Wenn im aktuellen oder einem früheren Batch kein Löschvorgang für einen Fremdschlüssel vorhanden ist, schlägt der Löschvorgang für den entsprechenden Primärschlüssel fehl. Fehlgeschlagene Löschvorgänge werden einmal wiederholt, nachdem alle Batches übernommen wurden.

SelectIncrementalChangesCommand (nur für DbSyncProvider relevant)

Ruft die Abfrage oder gespeicherte Prozedur ab, mit der inkrementelle Änderungen aus der lokalen Datenbank ausgewählt werden, oder legt sie fest.

Hinweis

Die festgelegte Abfrage sollte die ORDER BY [sync_row_timestamp]-Klausel enthalten. Wenn die Reihenfolge der Zeilen mithilfe von Timestampwerten festgelegt wird, kann sichergestellt werden, dass der Anbieter beim Neustart einer Synchronisierungssitzung mit der Auflistung beim höchsten Timestamp-Wasserzeichen beginnt (einzelne Tabellenwasserzeichen werden für die einzelnen Batches beibehalten) und dass keine Änderungen ausgelassen werden.

DataTable

Ruft das DataTable-Objekt ab, das die zu synchronisierenden Änderungen enthält, oder legt dieses fest. Wenn die Batchverarbeitung aktiviert wurde, wird mithilfe dieser Einstellung die Serialisierung der gespoolten Datei vom Datenträger aufgehoben. Alle an den Tabellen vorgenommenen Änderungen werden anschließend für die gespoolte Datei übernommen.

DataSet

Ruft ein DataSet-Objekt ab, das die ausgewählten Zeilen aus der Peerdatenbank enthält, oder legt es fest. Gibt NULL zurück, wenn IsDataBatched den Wert true hat.

Allgemeiner Code für zwei Ebenen und n Ebenen

Mit den Codebeispielen in diesem Abschnitt wird veranschaulicht, wie die Batchverarbeitung in 2- und n-Ebenen-Szenarien behandelt wird. Dieser Code wurde zwei Beispielen entnommen, die im Sync Framework-SDK enthalten sind: SharingAppDemo-CEProviderEndToEnd und WebSharingAppDemo-CEProviderEndToEnd. Alle Beispiele beginnen mit dem Speicherort des Codes, z. B. SharingAppDemo/CESharingForm. Hinsichtlich der Batchverarbeitung besteht der Hauptunterschied zwischen den zwei Anwendungen im zusätzlich erforderlichen Code für die n-Ebenen-Anwendung, mit dem die gespoolten Dateien hoch- und heruntergeladen und die Verzeichnisse für die einzelnen Knoten erstellt werden, die Änderungen auflisten.

Das folgende Codebeispiel des Ereignishandlers synchronizeBtn_Click in SharingAppDemo/CESharingForm legt die Datencachegröße des Arbeitsspeichers und das Verzeichnis fest, in das die Spooldateien geschrieben werden sollen. Beim für BatchingDirectory angegebenen Pfad muss es sich um ein lokales Verzeichnis des ausführenden Anbieters oder Proxys handeln. UNC-Dateipfade und nicht dateibezogene URI-Pfade werden nicht unterstützt. Beim für BatchingDirectory angegebenen Pfad handelt es sich um das Stammverzeichnis. In Sync Framework wird für jede Synchronisierungssitzung ein eindeutiges Unterverzeichnis erstellt, in dem die Spooldateien für diese Sitzung gespeichert werden. Dieses Verzeichnis ist für die aktuelle Quell-Ziel-Kombination eindeutig, damit die Dateien für verschiedene Sitzungen isoliert werden können.

Das folgende Codebeispiel des Ereignishandlers synchronizeBtn_Click in WebSharingAppDemo/CESharingForm legt dieselben Eigenschaften fest. Das Batchverarbeitungsverzeichnis für das Ziel wird jedoch auf den Proxy festgelegt, nicht wie im 2-Ebenen-Szenario direkt auf den Anbieter:

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

Mit den folgenden Codebeispielen der Datei SynchronizationHelper werden in beiden Anwendungen Methoden für die Behandlung von BatchSpooled und BatchAppliedEvents erstellt, die bei Änderungsauflistung und -übernahme von einem Anbieter ausgelöst werden:

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

Code für n Ebenen

Die übrigen Codebeispiele gelten nur für das n-Ebenen-Szenario in WebSharingAppDemo. Der entsprechende n-Ebenen-Code ist in drei Dateien enthalten:

  • Der Dienstvertrag: IRelationalSyncContract

  • Der Webdienst: RelationalWebSyncService

  • Der Proxy: RelationalProviderProxy

Die beiden Anbieter SqlSyncProvider und SqlCeSyncProvider erben beide von RelationalSyncProvider, daher gilt dieser Code für beide Anbieter. Zusätzliche speicherspezifische Funktionen werden für die einzelnen Anbietertypen in Proxy- und Dienstdateien getrennt.

Um zu verstehen, wie die Batchverarbeitung in einem n-Ebenen-Szenario funktioniert, betrachten Sie eine Synchronisierungssitzung, in der es sich bei der Quelle um den Server und beim Ziel um den Client handelt. Nachdem Änderungen in das lokale Verzeichnis des Servers geschrieben wurden, tritt für die heruntergeladenen Änderungen der folgende Prozess auf:

  1. Die GetChangeBatch-Methode wird auf dem Clientproxy aufgerufen. Wie später im Beispielcode veranschaulicht, sollte diese Methode Code einschließen, mit dem die Batchverarbeitung behandelt werden kann.

  2. Der Dienst ruft eine Batchdatei von SqlSyncProvider ab. Der Dienst entfernt die vollständigen Pfadinformationen und sendet nur den Dateinamen über das Netzwerk. Dadurch wird verhindert, dass den Clients die Verzeichnisstruktur des Servers offen gelegt wird.

  3. Der Proxyaufruf von GetChangeBatch wird zurückgegeben.

    1. Der Proxy erkennt, dass es sich um Änderungen im Batchmodus handelt. Daher wird DownloadBatchFile aufgerufen, indem der Batchdateiname als Argument übergeben wird.

    2. Der Proxy erstellt unter RelationalProviderProxy.BatchingDirectory ein eindeutiges Verzeichnis (sofern für die Sitzung noch nicht vorhanden), um diese Batchdateien lokal beizubehalten. Beim Verzeichnisnamen handelt es sich um die Replikat-ID des Peers, der Änderungen auflistet. Dadurch wird sichergestellt, dass der Proxy und der Dienst über ein eindeutiges Verzeichnis für die einzelnen Auflistungspeers verfügen.

  4. Der Proxy lädt die Datei herunter und speichert sie lokal. Der Proxy ersetzt den Dateinamen im Kontext durch den neuen vollständigen Pfad zur Batchdatei auf dem lokalen Datenträger.

  5. Der Proxy gibt dem Orchestrator den Kontext zurück.

  6. Wiederholen Sie Schritt 1 bis 6, bis der letzte Batch vom Proxy empfangen wurde.

Der folgende Prozess tritt für hochgeladene Änderungen auf.

  1. Der Orchestrator ruft auf dem Proxy ProcessChangeBatch auf.

  2. Der Proxy erkennt, dass es sich um eine Batchdatei handelt, und führt daher die folgenden Schritte durch:

    1. Er entfernt die vollständigen Pfadinformationen und sendet nur den Dateinamen über das Netzwerk.

    2. Er ruft HasUploadedBatchFile auf, um zu ermitteln, ob die Datei bereits hochgeladen wurde. Wenn dies der Fall ist, ist Schritt C nicht erforderlich.

    3. Wenn HasUploadedBatchFile den Wert false zurückgibt, wird für den Dienst UploadBatchFile aufgerufen und der Batchdateiinhalt hochgeladen.

      Der Dienst empfängt den Aufruf für UploadBatchFile und speichert den Batch lokal. Die Verzeichniserstellung erfolgt ähnlich wie oben unter Schritt 4.

    4. Für den Dienst wird ApplyChanges aufgerufen.

  3. Der Server empfängt den Aufruf ApplyChanges und erkennt, dass es sich um eine Batchdatei handelt. Er ersetzt den Dateinamen im Kontext durch den neuen vollständigen Pfad zur Batchdatei auf dem lokalen Datenträger.

  4. Der Server übergibt DbSyncContext an den lokalen SqlSyncProvider.

  5. Wiederholen Sie die Schritte 1 bis 6, bis der letzte Batch gesendet wurde.

Das folgende Codebeispiel für IRelationalSyncContract legt Upload- und Downloadmethoden fest, die verwendet werden, um gespoolte Dateien von und zur mittleren Ebene zu übertragen:

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

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

Die folgenden Codebeispiele fürRelationalWebSyncService legen die im Vertrag definierten Methoden UploadBatchFile und DownloadBatchFile offen und schließen eine zusätzliche batchverarbeitungsrelevante Logik in die folgenden Methoden ein:

  • Cleanup: bereinigt beliebige Spooldateien eines bestimmten Verzeichnisses oder (sofern kein Verzeichnis angegeben wurde) des temporären Verzeichnisses.

  • GetChanges: Überprüft, ob es sich um Daten im Batchmodus handelt, und entfernt gegebenenfalls den Verzeichnispfad der Spooldatei, damit der Pfad nicht über das Netzwerk gesendet wird. In n-Ebenen-Szenarien stellt es ein Sicherheitsrisiko dar, vollständige Verzeichnispfade über eine Netzwerkverbindung zu senden. Beim Dateinamen handelt es sich um eine GUID.

  • HasUploadedBatchFile: Gibt zurück, ob eine bestimmte Batchdatei bereits in den Dienst hochgeladen wurde.

  • ApplyChanges: Überprüft, ob sich Daten im Batchmodus befinden, und prüft gegebenenfalls, ob die erwartete Batchdatei bereits hochgeladen wurde. Wenn die Datei noch nicht hochgeladen wurde, wird eine Ausnahme ausgelöst. Der Client sollte die gespoolte Datei vor dem Aufrufen von ApplyChanges hochgeladen haben.

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

Mit den folgenden Codebeispielen für RelationalProviderProxy werden Eigenschaften und Methoden für den Webdienst festgelegt und aufgerufen:

  • BatchingDirectory: Aktiviert die Anwendung, um das Batchverarbeitungsverzeichnis für die mittlere Ebene festzulegen.

  • EndSession: Bereinigt beliebige Dateien eines angegebenen Verzeichnisses.

  • GetChangeBatch: Lädt Änderungsbatches herunter, indem die DownloadBatchFile-Methode aufgerufen wird.

  • ProcessChangeBatch: Lädt Änderungsbatches hoch, indem die UploadBatchFile-Methode aufgerufen wird.

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
}

Siehe auch

Konzepte

Synchronisieren von SQL Server und SQL Server Compact