Procédure : remettre des modifications dans des lots (SQL Server)
Cette rubrique explique comment remettre des modifications dans les lots pour la synchronisation de bases de données dans Sync Framework qui utilise SqlSyncProvider, SqlCeSyncProvider ou DbSyncProvider. Le code proposé dans cette rubrique se concentre sur les classes Sync Framework suivantes :
Présentation du traitement par lot
Par défaut, Sync Framework remet les modifications à chaque nœud dans un même objet DataSet. Cet objet est conservé en mémoire à mesure que les modifications sont appliquées à un nœud. Le comportement par défaut fonctionne bien si la mémoire est suffisante sur l'ordinateur où les modifications sont appliquées et si la connexion à l'ordinateur est fiable. Il peut toutefois être avantageux pour certaines applications de disposer des modifications divisées en lots. Considérez le scénario suivant pour une application de synchronisation :
De nombreux clients qui utilisent SqlCeSyncProvider se synchronisent périodiquement avec un serveur qui utilise SqlSyncProvider.
Chaque client dispose d'une quantité limitée de mémoire et d'espace disque.
Les connexions entre le serveur et les clients présentent une bande passante faible et intermittente, ce qui aboutit souvent à de longues durées de synchronisation et à des interruptions de connexions.
La taille des modifications (en Ko) d'une session de synchronisation classique est importante.
Le traitement par lot des modifications est idéal dans ce type de scénario, car il offre les possibilités suivantes :
Permet au développeur de contrôler la quantité de mémoire (taille du cache de données de la mémoire) utilisée pour stocker des modifications sur le client, ce qui contribue à éviter des erreurs de mémoire insuffisante sur le client.
Permet à Sync Framework de redémarrer une opération de synchronisation non réussie depuis le début du lot actuel et non de l'ensemble de modifications complet.
Peut réduire ou éliminer la nécessité de procéder à un nouveau téléchargement ou à une nouvelle énumération des modifications sur le serveur en raison d'opérations non réussies.
Le traitement par lot est simple à configurer pour les applications à deux couches et multicouches, et il peut être utilisé pour la session de synchronisation initiale et les suivantes.
Configuration et utilisation du traitement par lot
Le traitement par lot dans Sync Framework fonctionne comme suit :
L'application spécifie la taille du cache de données de la mémoire pour chaque fournisseur qui participe à la session de synchronisation.
Si les deux fournisseurs spécifient une taille de cache, Sync Framework utilise la plus petite pour les deux fournisseurs. La taille de cache réelle ne dépassera pas 110 % de la plus petite taille spécifiée. Au cours d'une session de synchronisation, si une ligne dépasse à elle seule 110 % de la taille, la session prend fin avec une exception.
La valeur 0 (valeur par défaut) désactive le traitement par lot. Si le traitement par lot est activé sur un fournisseur et pas sur l'autre, il l'est pour le chargement et le téléchargement.
L'application spécifie l'emplacement des fichiers de mise en file d'attente pour chaque fournisseur. Par défaut, les fichiers de mise en file d'attente sont écrits dans le répertoire temp du compte utilisé pour exécuter le processus de synchronisation.
L'application appelle Synchronize.
Sync Framework énumère les modifications une ligne à la fois. Si la taille du cache de données de la mémoire est atteinte pour le fournisseur de source, les modifications sont rendues persistantes dans un fichier de mise en file d'attente local, et les données en mémoire sont vidées. Ce processus se poursuit jusqu'à ce que toutes les modifications aient été énumérées.
Pour les scénarios multicouches, le code du service et du proxy de l'application transmet en continu à la destination les fichiers de mise en file d'attente. Pour plus d'informations, consultez Code spécifique au scénario multicouche dans cette rubrique. Pour les scénarios à deux couches, le fichier local se trouve déjà sur la destination, car alors, l'intégralité du code de synchronisation s'exécute sur la destination.
Sync Framework désérialise les modifications des fichiers de mise en file d'attente et applique ces modifications. Ce processus se poursuit jusqu'à ce que toutes les modifications soient appliquées à la destination.
Tous les lots sont appliqués en une transaction. Cette transaction n'est créée qu'à la réception du dernier lot par le fournisseur de destination.
Pour les scénarios à deux couches, Sync Framework nettoie le fichier de mise en file d'attente. Pour les scénarios multicouches, Sync Framework nettoie des fichiers de mise en file d'attente sur l'ordinateur d'où est lancée la synchronisation, mais les fichiers du niveau intermédiaire doivent être nettoyés par le proxy (comme le montre l'exemple de méthode
Cleanup()
plus loin dans cette rubrique). Pour gérer les abandons de session, le niveau intermédiaire doit également utiliser un processus visant à nettoyer les fichiers antérieurs à une date donnée.
Notes
Les modifications de données qui seront appliquées à un nœud sont disponibles à partir de la propriété Context de l'objet DbChangesSelectedEventArgs. Lorsque les données ne sont pas traitées par lot, l'événement ChangesSelected est déclenché une fois seulement, puis toutes les modifications sont disponibles à partir de la propriété Context. Lorsque les données sont traitées par lot, ChangesSelected est déclenché pour chaque lot et seules les modifications de ce lot sont alors disponibles. Si vous nécessitez des modifications à partir de tous les lots, répondez à chaque événement ChangesSelected et stockez les données qui sont retournées.
Le tableau suivant décrit les types et membres associés au traitement par lot. La seule propriété requise pour le traitement par lot est MemoryDataCacheSize, mais il est également recommandé de définir BatchingDirectory.
Type ou membre | Description |
---|---|
BatchingDirectory |
Obtient ou définit le répertoire dans lequel les fichiers de commandes sont mis en attente sur le disque. Le chemin d'accès spécifié doit correspondre à un répertoire local du fournisseur ou du proxy qui s'exécute. Les chemins d'accès UNC de fichiers et les chemins d'accès d'URI qui ne concernent pas des fichiers ne sont pas pris en charge. Important Les fichiers de mise en file d'attente contiennent des données de base de données brutes. Le répertoire dans lequel les fichiers sont écrits doit être protégé avec les contrôles d'accès appropriés. |
Obtient ou définit s'il faut nettoyer les fichiers de traitement par lot après l'application à la destination des modifications contenues dans les fichiers. Le comportement par défaut est de nettoyer les fichiers. |
|
MemoryDataCacheSize |
Obtient ou définit la quantité de mémoire maximale, en Ko, que Sync Framework utilise pour mettre en cache les modifications avant de les mettre en attente sur le disque. Notes Ce paramètre affecte uniquement la taille des données et métadonnées conservées en mémoire pour les modifications envoyées à la destination. Il ne limite pas la mémoire utilisée par les autres composants Sync Framework ou les composants d'applications utilisateur. |
Événement qui se produit après que chaque lot de modifications a été appliqué à la destination. |
|
Événement qui se produit après que chaque lot de modifications a été écrit sur disque. |
|
DbBatchAppliedEventArgs |
Fournit des données pour l'événement BatchApplied, notamment le numéro du lot actuel et le nombre total de lots à appliquer. |
DbBatchSpooledEventArgs |
Fournit des données pour l'événement BatchSpooled, notamment le numéro et la taille du lot actuel. |
Obtient ou définit le nom du fichier dans lequel les modifications mises en attente sont écrites. |
|
Obtient ou définit si les données sont envoyées dans plusieurs lots ou dans un objet DataSet unique. |
|
Obtient ou définit si le lot actuel est le dernier lot de modifications. |
|
Obtient ou définit le nombre d'opérations de suppression retentées pendant une session de synchronisation dans laquelle les modifications étaient traitées par lot. Les suppressions sont retentées pour les lots en raison du classement des suppressions de clé primaire et de clé étrangère. Si une suppression de clé étrangère n'existe pas dans le lot actuel ou un lot précédent, la suppression de clé primaire correspondante échoue. Les suppressions ayant échoué sont retentées une fois lorsque tous les lots ont été appliqués. |
|
SelectIncrementalChangesCommand (pertinent uniquement pour DbSyncProvider) |
Obtient ou définit la requête ou la procédure stockée qui est utilisée pour sélectionner des modifications incrémentielles dans la base de données locale. Notes Il est recommandé que la requête spécifiée inclue la clause |
Obtient ou définit l'objet DataTable qui contient les modifications à synchroniser. Si le traitement par lot est activé, l'accès à cette propriété désérialise le fichier mis en attente sur le disque. Toute modification apportée aux tables est alors répercutée dans le fichier mis en attente. |
|
Obtient ou définit un objet DataSet qui contient les lignes sélectionnées de la base de données d'homologues. Retourne Null si IsDataBatched a la valeur true. |
Code commun aux scénarios à deux couches et multicouches
Les exemples de code de cette section montrent comment gérer le traitement par lot dans des scénarios à deux couches et multicouches. Ce code provient de deux exemples inclus dans le Kit de développement logiciel (SDK) Sync Framework : SharingAppDemo-CEProviderEndToEnd
et WebSharingAppDemo-CEProviderEndToEnd
. Chaque exemple est précédé d'une indication de l'emplacement du code, comme SharingAppDemo/CESharingForm
. En termes de traitement par lot, la différence essentielle entre les deux applications réside dans le code supplémentaire requis pour le cas multicouche afin de charger et télécharger les fichiers mis en attente et créer des répertoires pour chaque nœud qui énumère les modifications.
L'exemple de code suivant du gestionnaire d'événements synchronizeBtn_Click
de SharingAppDemo/CESharingForm
définit la taille du cache de données de la mémoire et le répertoire dans lequel les fichiers de mise en file d'attente doivent être écrits. Le chemin d'accès spécifié pour BatchingDirectory
doit correspondre à un répertoire local du fournisseur ou du proxy qui s'exécute. Les chemins d'accès UNC de fichiers et les chemins d'accès d'URI qui ne concernent pas des fichiers ne sont pas pris en charge. Le chemin d'accès spécifié pour BatchingDirectory
est le répertoire racine. Pour chaque session de synchronisation, Sync Framework crée un sous-répertoire unique dans lequel stocker les fichiers de mise en file d'attente pour cette session. Ce répertoire est unique pour la combinaison source-destination actuelle afin d'isoler les fichiers des différentes sessions.
L'exemple de code suivant du gestionnaire d'événements synchronizeBtn_Click
de WebSharingAppDemo/CESharingForm
définit les mêmes propriétés, mais le répertoire de traitement par lot de la destination est défini pour le proxy et non plus directement pour le fournisseur comme dans le scénario à deux couches :
//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;
}
Les exemples de code suivants du fichier SynchronizationHelper
des deux applications créent des méthodes pour gérer les événements BatchSpooled
et BatchAppliedEvents
déclenchés par un fournisseur pendant l'énumération et l'application des modifications :
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 spécifique au scénario multicouche
Le reste des exemples de code s'applique uniquement au scénario multicouche de WebSharingAppDemo
. Le code multicouche pertinent est contenu dans trois fichiers :
Contrat de service :
IRelationalSyncContract
Service Web :
RelationalWebSyncService
Proxy :
RelationalProviderProxy
Les fournisseurs SqlSyncProvider et SqlCeSyncProvider héritent tous deux de RelationalSyncProvider, aussi ce code s'applique-t-il aux deux fournisseurs. Les fonctionnalités supplémentaires spécifiques au magasin sont séparées en fichiers de proxy et de service pour chaque type de fournisseur.
Pour comprendre le fonctionnement du traitement par lot dans un scénario multicouche, imaginez une session de synchronisation dans laquelle le serveur est la source et le client est la destination. Lorsque les modifications ont été écrites dans le répertoire local du serveur, le processus suivant se produit pour les modifications téléchargées :
La méthode
GetChangeBatch
est appelée sur le proxy du client. Comme illustré plus loin dans l'exemple de code, cette méthode doit inclure du code spécifique pour gérer le traitement par lot.Le service obtient un fichier de commandes de
SqlSyncProvider
. Le service supprime les informations du chemin d'accès complet pour n'envoyer sur le réseau que le nom du fichier. Cela évite d'exposer la structure de répertoires du serveur aux clients.L'appel du proxy à
GetChangeBatch
est retourné.Le proxy détecte que les modifications sont traitées par lot et appelle donc
DownloadBatchFile
en passant le nom du fichier de commandes en tant qu'argument.Le proxy crée un répertoire unique (s'il n'en existe pas pour la session) sous
RelationalProviderProxy.BatchingDirectory
pour conserver localement ces fichiers de commandes. Le nom du répertoire est l'ID de réplica de l'homologue qui énumère les modifications. De cette façon, le proxy et le service disposent d'un répertoire unique pour chaque homologue d'énumération.
Le proxy télécharge et stocke le fichier localement. Le proxy remplace le nom de fichier du contexte par le nouveau chemin d'accès complet au fichier de commandes sur le disque local.
Le proxy retourne le contexte à l'orchestrateur.
Répétez les étapes 1 à 6 jusqu'à ce que le dernier lot soit reçu par le proxy.
Le processus suivant se produit pour les modifications chargées :
L'orchestrateur appelle
ProcessChangeBatch
sur le proxy.Le proxy l'identifie en tant que fichier de commandes et effectue donc les étapes suivantes :
Supprime les informations du chemin d'accès complet pour n'envoyer sur le réseau que le nom du fichier.
Appelle
HasUploadedBatchFile
pour déterminer si le fichier a déjà été chargé. Si c'est le cas, l'étape C n'est pas nécessaire.Si
HasUploadedBatchFile
retourne false, appelleUploadBatchFile
sur le service et charge le contenu du fichier de commandes.Le service recevra l'appel à
UploadBatchFile
et stockera le lot localement. La création de répertoires suit le processus décrit plus haut à l'étape 4.Appelle
ApplyChanges
sur le service.
Le serveur reçoit l'appel à
ApplyChanges
et l'identifie en tant que fichier de commandes. Il remplace le nom de fichier du contexte par le nouveau chemin d'accès complet au fichier de commandes sur le disque local.Le serveur passe le
DbSyncContext
auSqlSyncProvider
local.Répétez les étapes 1 à 6 jusqu'à ce que le dernier lot soit envoyé.
L'exemple de code suivant, qui provient d'IRelationalSyncContract
, spécifie les méthodes de chargement et de téléchargement utilisées pour transférer des fichiers mis en attente à destination et en provenance du niveau intermédiaire :
[OperationContract(IsOneWay = true)]
void UploadBatchFile(string batchFileid, byte[] batchFile);
[OperationContract]
byte[] DownloadBatchFile(string batchFileId);
Les exemples de code suivants, qui proviennent de RelationalWebSyncService
, exposent les méthodes UploadBatchFile
et DownloadBatchFile
définies dans le contrat et incluent une logique supplémentaire liée au traitement par lot dans les méthodes suivantes :
Cleanup
: nettoie tous les fichiers mis en attente d'un répertoire spécifié ou, s'il n'en a pas été spécifié, du répertoire temp.GetChanges
: vérifie si les données sont traitées par lot, et si tel est le cas, supprime du fichier mis en attente le chemin d'accès au répertoire pour ne pas envoyer ce chemin d'accès sur le réseau. Dans les scénarios multicouches, l'envoi sur une connexion réseau de chemins d'accès complets à des répertoires constitue un risque pour la sécurité. Le nom du fichier est un GUID.HasUploadedBatchFile
: retourne une valeur qui indique si un fichier de commandes particulier a déjà été chargé vers le service.ApplyChanges
: vérifie si les données sont traitées par lot, et si tel est le cas, vérifie si le fichier de commandes attendu a déjà été chargé. Si le fichier n'a pas été chargé, une exception est levée. Le client doit avoir chargé le fichier mis en attente avant d'appelerApplyChanges
.
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();
}
}
}
}
Les exemples de code suivants, qui proviennent de RelationalProviderProxy
, définissent des propriétés et appellent des méthodes sur le service Web :
BatchingDirectory
: permet à l'application de définir le répertoire de traitement par lot pour le niveau intermédiaire.EndSession
: nettoie tous les fichiers mis en attente d'un répertoire spécifié.GetChangeBatch
: télécharge des lots de modifications en appelant la méthodeDownloadBatchFile
.ProcessChangeBatch
: charge des lots de modifications en appelant la méthodeUploadBatchFile
.
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
}