HOW TO:以批次傳遞變更 (SQL Server)
本主題描述如何以批次方式針對 Sync Framework 中使用 SqlSyncProvider、SqlCeSyncProvider 或 DbSyncProvider 的資料庫同步處理傳遞變更。本主題的程式碼著重於下列 Sync Framework 類別:
了解批次處理
根據預設,Sync Framework 會在單一 DataSet 物件中傳遞每一個節點的變更。當變更套用到節點時,這個物件會保留在記憶體中。如果套用變更的電腦上有足夠的記憶體,而且電腦的連接很可靠,預設行為就會運作良好。但是,某些應用程式可能會因為變更分成批次而受益。假設有以下的同步處理應用程式案例:
使用 SqlCeSyncProvider 的大量用戶端會與使用 SqlSyncProvider 的伺服器定期同步處理。
每個用戶端的記憶體數量和磁碟空間都受到限制。
伺服器與用戶端之間的連線頻寬很低而且斷斷續續時,通常會導致同步處理的時間很長且連線會遭到卸除。
一般同步處理工作階段的變更很大 (以 KB 為單位)。
批次變更很適合這種情況,因為它提供下列功能:
可讓開發人員控制用來在用戶端上儲存變更的記憶體數量 (記憶體資料快取大小)。這樣可以消除用戶端上的記憶體不足錯誤。
可讓 Sync Framework 從目前批次開頭 (而不是從整個變更集合的開頭) 重新啟動失敗的同步處理作業。
可以減少或消除因為作業失敗而必須在伺服器上重新下載變更或重新列舉變更的需求。
對於 2 層式和 N 層式應用程式而言,批次處理很容易設定,也可用於首次同步處理工作階段和後續的工作階段。
設定及使用批次處理
Sync Framework 中批次處理的運作方式如下:
應用程式會針對每一個參與同步處理工作階段的提供者指定記憶體資料快取大小。
如果這兩個提供者都指定快取大小,Sync Framework 會針對這兩個提供者使用較小的值。實際的快取大小不會超過最小指定大小的 110%。在同步處理工作階段期間,如果單一資料列大於大小的 110%,工作階段會結束並傳回例外狀況。
0 的值 (預設值) 會停用批次處理。如果一個提供者已經啟用批次處理,而另一個提供者並未啟用,則上傳和下載都會啟用批次處理。
應用程式會針對每個提供者指定多工緩衝處理檔案的位置。根據預設,多工緩衝處理檔案會寫入執行同步處理程序所使用之帳戶的暫存目錄。
應用程式會呼叫 Synchronize。
Sync Framework 會一次列舉一列變更。如果已達到來源提供者的記憶體資料快取大小,變更就會保存到本機多工緩衝處理檔案中,而且會排清記憶體內的資料。此程序會繼續,直到列舉所有變更為止。
如果是 N 層式案例,應用程式中的服務和 Proxy 程式碼會將多工緩衝處理檔案以資料流方式傳送到目的地。如需詳細資訊,請參閱本主題中的 N 層特有的程式碼。如果是 2 層式案例,表示本機檔案已經在目的地,因為在此情況下,所有同步處理程式碼都會在目的地執行。
Sync Framework 會將多工緩衝處理檔案中的變更還原序列化,並套用這些變更。此程序會繼續,直到所有的變更套用至目的地為止。
所有的批次都會在一筆交易中套用。要等到目的地提供者收到最後一個批次之後,才會建立該筆交易。
如果是兩層式案例,Sync Framework 會清理多工緩衝處理檔案。如果是 N 層式案例,Sync Framework 會在起始同步處理的電腦上清理多工緩衝處理檔案,但是中間層上的檔案應該由 Proxy 所清除 (本主題稍後的範例
Cleanup()
方法會加以示範)。若要處理工作階段中止的狀況,中間層也應該使用一個程序來清除早於某個日期的檔案。
注意
DbChangesSelectedEventArgs 物件的 Context 屬性會提供即將套用至節點的資料變更。如果資料並未批次處理,ChangesSelected 事件只會引發一次,而且 Context 屬性會提供所有變更。如果資料已批次處理,ChangesSelected 就會針對每個批次引發,而且此時只會提供該批次的變更。如果您需要所有批次的變更,請回應每個 ChangesSelected 事件並儲存所傳回的資料。
下表描述與批次處理相關的型別和成員。雖然批次處理唯一必要的屬性是 MemoryDataCacheSize,不過我們也建議您設定 BatchingDirectory。
型別或成員 | 描述 |
---|---|
BatchingDirectory |
取得或設定磁碟上要當作批次檔案多工緩衝處理目的地的目錄。指定的路徑必須是提供者或執行之 Proxy 的本機目錄。不支援 UNC 檔案路徑和非檔案 URI 路徑。 注意 多工緩衝處理檔案包含原始資料庫的資料。要寫入檔案的目錄必須受到適當的存取控制保護。 |
取得或設定在批次處理檔案中的變更已經套用至目的地之後是否要清除這些檔案。預設是清除檔案。 |
|
MemoryDataCacheSize |
取得或設定將變更多工緩衝處理至磁碟之前,Sync Framework 用來快取這些變更的最大記憶體數量 (以 KB 為單位)。 注意 這個設定只會影響針對傳送給目的地的變更而保留在記憶體中的資料和中繼資料的大小。此設定不會限制其他 Sync Framework 元件或使用者應用程式元件所使用的記憶體。 |
當每個變更批次都已經套用至目的地之後所發生的事件。 |
|
當每個變更批次都已經寫入磁碟之後所發生的事件。 |
|
DbBatchAppliedEventArgs |
針對 BatchApplied 事件提供資料,包括目前批次號碼以及要套用的批次總數。 |
DbBatchSpooledEventArgs |
針對 BatchSpooled 事件提供資料,包括目前批次號碼以及批次大小。 |
取得或設定要寫入多工緩衝處理變更的檔案名稱。 |
|
取得或設定資料是透過多個批次或單一 DataSet 物件傳送。 |
|
取得或設定目前批次是否為最後一個變更批次。 |
|
取得或設定在批次處理變更的同步處理工作階段期間已經嘗試進行的刪除作業數目。 系統會根據主索引鍵和外部索引鍵刪除的順序,針對批次重試刪除。如果外部索引鍵不存在目前的批次或先前的批次中,對應的主索引鍵刪除就會失敗。一旦套用所有批次之後,系統會重試失敗的刪除。 |
|
SelectIncrementalChangesCommand (只與 DbSyncProvider 有關) |
取得或設定用來從本機資料庫中選取累加變更的查詢或預存程序。 注意 建議指定的查詢最好包含子句 |
取得或設定包含要同步處理之變更的 DataTable 物件。如果啟用了批次處理,當您存取這個屬性時,就會從磁碟還原序列化多工緩衝處理檔案。然後,對這些資料表所做的任何變更都會保存回多工緩衝處理檔案。 |
|
取得或設定 DataSet 物件,此物件包含對等 (Peer) 資料庫中的選定資料列。如果 IsDataBatched 為 true,則傳回 Null。 |
常見的兩層式和 N 層式程式碼
本節的程式碼範例示範如何處理 2 層式和 N 層式案例中的批次處理。此程式碼是從 Sync Framework SDK 中所包含的兩個範例取得:SharingAppDemo-CEProviderEndToEnd
和 WebSharingAppDemo-CEProviderEndToEnd
。每個範例的說明都是從程式碼的所在位置開始,例如 SharingAppDemo/CESharingForm
。對於批次處理而言,兩個應用程式之間的主要差異在於 N 層式案例中需要額外的程式碼,才能上傳及下載多工緩衝處理檔案以及針對列舉變更的每個節點建立目錄。
下列來自 SharingAppDemo/CESharingForm
中 synchronizeBtn_Click
事件處理常式的程式碼範例會設定記憶體資料快取大小以及多工緩衝處理檔案應該寫入的目錄。針對 BatchingDirectory
指定的路徑必須是提供者或執行之 Proxy 的本機目錄。不支援 UNC 檔案路徑和非檔案 URI 路徑。針對 BatchingDirectory
指定的路徑是根目錄。對於每一個同步處理工作階段而言,Sync Framework 都會建立一個唯一的子目錄,用來儲存該工作階段的多工緩衝處理檔案。此目錄對於目前的來源-目的地組合是唯一的,用以隔離不同工作階段的檔案。
下列來自 WebSharingAppDemo/CESharingForm
中 synchronizeBtn_Click
事件處理常式的程式碼範例會設定相同的屬性,但是目的地的批次處理目錄會針對 Proxy 設定,而不是針對 2 層式案例中的提供者直接設定:
//Set memory data cache size property. 0 represents non batched mode.
//No need to set memory cache size for Proxy, because the source is
//enabled for batching: both upload and download will be batched.
srcProvider.MemoryDataCacheSize = this._batchSize;
//Set batch spool location. Default value if not set is %Temp% directory.
if (!string.IsNullOrEmpty(this.batchSpoolLocation.Text))
{
srcProvider.BatchingDirectory = this.batchSpoolLocation.Text;
destinationProxy.BatchingDirectory = this.batchSpoolLocation.Text;
}
下列來自兩個應用程式中 SynchronizationHelper
檔案的程式碼範例會建立方法來處理 BatchSpooled
和 BatchAppliedEvents
,這兩者是提供者在變更列舉和變更套用期間所引發:
void provider_BatchSpooled(object sender, DbBatchSpooledEventArgs e)
{
this.progressForm.listSyncProgress.Items.Add("BatchSpooled event fired: Details");
this.progressForm.listSyncProgress.Items.Add("\tSource Database :" + ((RelationalSyncProvider)sender).Connection.Database);
this.progressForm.listSyncProgress.Items.Add("\tBatch Name :" + e.BatchFileName);
this.progressForm.listSyncProgress.Items.Add("\tBatch Size :" + e.DataCacheSize);
this.progressForm.listSyncProgress.Items.Add("\tBatch Number :" + e.CurrentBatchNumber);
this.progressForm.listSyncProgress.Items.Add("\tTotal Batches :" + e.TotalBatchesSpooled);
this.progressForm.listSyncProgress.Items.Add("\tBatch Watermark :" + ReadTableWatermarks(e.CurrentBatchTableWatermarks));
}
void provider_BatchApplied(object sender, DbBatchAppliedEventArgs e)
{
this.progressForm.listSyncProgress.Items.Add("BatchApplied event fired: Details");
this.progressForm.listSyncProgress.Items.Add("\tDestination Database :" + ((RelationalSyncProvider)sender).Connection.Database);
this.progressForm.listSyncProgress.Items.Add("\tBatch Number :" + e.CurrentBatchNumber);
this.progressForm.listSyncProgress.Items.Add("\tTotal Batches To Apply :" + e.TotalBatchesToApply);
}
//Reads the watermarks for each table from the batch spooled event. //The watermark denotes the max tickcount for each table in each batch.
private string ReadTableWatermarks(Dictionary<string, ulong> dictionary)
{
StringBuilder builder = new StringBuilder();
Dictionary<string, ulong> dictionaryClone = new Dictionary<string, ulong>(dictionary);
foreach (KeyValuePair<string, ulong> kvp in dictionaryClone)
{
builder.Append(kvp.Key).Append(":").Append(kvp.Value).Append(",");
}
return builder.ToString();
}
N 層特有的程式碼
程式碼範例的其餘部分只適用於 WebSharingAppDemo
中的 N 層式案例。相關的 N 層式程式碼包含在三個檔案中:
服務合約:
IRelationalSyncContract
Web 服務:
RelationalWebSyncService
Proxy:
RelationalProviderProxy
SqlSyncProvider 和 SqlCeSyncProvider 這兩個提供者都繼承自 RelationalSyncProvider,所以這段程式碼會套用到這兩個提供者。存放區特有的其他功能會針對每一種類型的提供者分為 Proxy 和服務檔案。
為了了解批次處理在 N 層式案例中的運作方式,假設有一個同步處理工作階段,其中的伺服器是來源,而用戶端是目的地。當變更已經寫入伺服器上的本機目錄之後,下載的變更會進行下列程序:
在用戶端 Proxy 上呼叫
GetChangeBatch
方法。如同此範例程式碼稍後所示範的內容,這個方法應該包含特定程式碼來處理批次。此服務會從
SqlSyncProvider
取得批次檔。此服務會移除完整的路徑資訊,並透過網路只傳送檔案名稱。這可避免將伺服器的目錄結構公開給用戶端。傳回對
GetChangeBatch
的 Proxy 呼叫。Proxy 會偵測到變更已經批次處理,所以它會將批次檔名稱當做引數傳遞來呼叫
DownloadBatchFile
。此 Proxy 會在
RelationalProviderProxy.BatchingDirectory
底下建立一個唯一目錄 (如果此工作階段還沒有該目錄的話),以便在本機保留這些批次檔。目錄名稱就是列舉變更之對等的複寫識別碼。如此可確保此 Proxy 和服務在每一個列舉對等端都有一個唯一的目錄。
此 Proxy 會下載檔案,並將它儲存在本機。此 Proxy 會將此內容中的檔案名稱取代為本機磁碟上批次檔的新完整路徑。
此 Proxy 會將內容傳回協調者。
重複步驟 1 至 6,直到 Proxy 收到最後一個批次為止。
上傳的變更會進行下列程序
協調者會在 Proxy 上呼叫
ProcessChangeBatch
。此 Proxy 判斷它是一個批次檔,所以它會執行下列步驟:
移除完整的路徑資訊,並透過網路只傳送檔案名稱。
呼叫
HasUploadedBatchFile
來判斷檔案是否已上傳。如果是的話,就不需要步驟 C。如果
HasUploadedBatchFile
傳回 false,就會在服務上呼叫UploadBatchFile
,並上傳批次檔內容。此服務會接收
UploadBatchFile
的呼叫,並在本機儲存批次。建立目錄與上述的步驟 4 很類似。在服務上呼叫
ApplyChanges
。
伺服器收到
ApplyChanges
呼叫,並判斷它為批次檔。它會將此內容中的檔案名稱取代為本機磁碟上批次檔的新完整路徑。伺服器會將
DbSyncContext
傳遞給本機SqlSyncProvider
。重複步驟 1 至 6,直到傳送最後一個批次為止。
下列來自 IRelationalSyncContract
的程式碼範例會指定用於在中介層之間來回傳輸多工緩衝處理檔案的上傳和下載方法:
[OperationContract(IsOneWay = true)]
void UploadBatchFile(string batchFileid, byte[] batchFile);
[OperationContract]
byte[] DownloadBatchFile(string batchFileId);
下列來自 RelationalWebSyncService
的程式碼範例會公開合約中所定義的 UploadBatchFile
和 DownloadBatchFile
方法,並包括下列方法中與批次處理相關的其他邏輯:
Cleanup
:從指定的目錄或暫存目錄 (未指定目錄的話),清除任何多工緩衝處理的檔案。GetChanges
:查看是否批次處理資料,如果是的話,則移除多工緩衝處理檔案的目錄路徑,好讓該路徑不會透過網路傳送。在 N 層式案例中,透過網路連線傳送完整的目錄路徑會有安全上的風險。檔案名稱為 GUID。HasUploadedBatchFile
:傳回是否特定批次檔已經上傳至服務。ApplyChanges
:查看是否批次處理資料,如果是的話,則檢查預期的批次檔是否已經上傳。如果尚未上傳檔案,則會擲回例外狀況。在呼叫ApplyChanges
之前,用戶端應該已經上傳多工緩衝處理的檔案。
public abstract class RelationalWebSyncService: IRelationalSyncContract
{
protected bool isProxyToCompactDatabase;
protected RelationalSyncProvider peerProvider;
protected DirectoryInfo sessionBatchingDirectory = null;
protected Dictionary<string, string> batchIdToFileMapper;
int batchCount = 0;
public void Initialize(string scopeName, string hostName)
{
this.peerProvider = this.ConfigureProvider(scopeName, hostName);
this.batchIdToFileMapper = new Dictionary<string, string>();
}
public void Cleanup()
{
this.peerProvider = null;
//Delete all file in the temp session directory
if (sessionBatchingDirectory != null && sessionBatchingDirectory.Exists)
{
try
{
sessionBatchingDirectory.Delete(true);
}
catch
{
//Ignore
}
}
}
public void BeginSession(SyncProviderPosition position)
{
Log("*****************************************************************");
Log("******************** New Sync Session ***************************");
Log("*****************************************************************");
Log("BeginSession: ScopeName: {0}, Position: {1}", this.peerProvider.ScopeName, position);
//Clean the mapper for each session.
this.batchIdToFileMapper = new Dictionary<string, string>();
this.peerProvider.BeginSession(position, null/*SyncSessionContext*/);
this.batchCount = 0;
}
public SyncBatchParameters GetKnowledge()
{
Log("GetSyncBatchParameters: {0}", this.peerProvider.Connection.ConnectionString);
SyncBatchParameters destParameters = new SyncBatchParameters();
this.peerProvider.GetSyncBatchParameters(out destParameters.BatchSize, out destParameters.DestinationKnowledge);
return destParameters;
}
public GetChangesParameters GetChanges(uint batchSize, SyncKnowledge destinationKnowledge)
{
Log("GetChangeBatch: {0}", this.peerProvider.Connection.ConnectionString);
GetChangesParameters changesWrapper = new GetChangesParameters();
changesWrapper.ChangeBatch = this.peerProvider.GetChangeBatch(batchSize, destinationKnowledge, out changesWrapper.DataRetriever);
DbSyncContext context = changesWrapper.DataRetriever as DbSyncContext;
//Check to see if data is batched
if (context != null && context.IsDataBatched)
{
Log("GetChangeBatch: Data Batched. Current Batch #:{0}", ++this.batchCount);
//Dont send the file location info. Just send the file name
string fileName = new FileInfo(context.BatchFileName).Name;
this.batchIdToFileMapper[fileName] = context.BatchFileName;
context.BatchFileName = fileName;
}
return changesWrapper;
}
public SyncSessionStatistics ApplyChanges(ConflictResolutionPolicy resolutionPolicy, ChangeBatch sourceChanges, object changeData)
{
Log("ProcessChangeBatch: {0}", this.peerProvider.Connection.ConnectionString);
DbSyncContext dataRetriever = changeData as DbSyncContext;
if (dataRetriever != null && dataRetriever.IsDataBatched)
{
string remotePeerId = dataRetriever.MadeWithKnowledge.ReplicaId.ToString();
//Data is batched. The client should have uploaded this file to us prior to calling ApplyChanges.
//So look for it.
//The Id would be the DbSyncContext.BatchFileName which is just the batch file name without the complete path
string localBatchFileName = null;
if (!this.batchIdToFileMapper.TryGetValue(dataRetriever.BatchFileName, out localBatchFileName))
{
//Service has not received this file. Throw exception
throw new FaultException<WebSyncFaultException>(new WebSyncFaultException("No batch file uploaded for id " + dataRetriever.BatchFileName, null));
}
dataRetriever.BatchFileName = localBatchFileName;
}
SyncSessionStatistics sessionStatistics = new SyncSessionStatistics();
this.peerProvider.ProcessChangeBatch(resolutionPolicy, sourceChanges, changeData, new SyncCallbacks(), sessionStatistics);
return sessionStatistics;
}
public void EndSession()
{
Log("EndSession: {0}", this.peerProvider.Connection.ConnectionString);
Log("*****************************************************************");
Log("******************** End Sync Session ***************************");
Log("*****************************************************************");
this.peerProvider.EndSession(null);
Log("");
}
/// <summary>
/// Used by proxy to see if the batch file has already been uploaded. Optimizes by not resending batch files.
/// NOTE: This method takes in a file name as an input parameter and hence is suseptible for name canonicalization
/// attacks. This sample is meant to be a starting point in demonstrating how to transfer sync batch files and is
/// not intended to be a secure way of doing the same. This SHOULD NOT be used as such in production environment
/// without doing proper security analysis.
///
/// Please refer to the following two MSDN whitepapers for more information on guidelines for securing Web servies.
///
/// Design Guidelines for Secure Web Applications - https://msdn.microsoft.com/en-us/library/aa302420.aspx (Refer InputValidation section)
/// Architecture and Design Review for Security - https://msdn.microsoft.com/en-us/library/aa302421.aspx (Refer InputValidation section)
/// </summary>
/// <param name="batchFileId"></param>
/// <returns>bool</returns>
public bool HasUploadedBatchFile(String batchFileId, string remotePeerId)
{
this.CheckAndCreateBatchingDirectory(remotePeerId);
//The batchFileId is the fileName without the path information in it.
FileInfo fileInfo = new FileInfo(Path.Combine(this.sessionBatchingDirectory.FullName, batchFileId));
if (fileInfo.Exists && !this.batchIdToFileMapper.ContainsKey(batchFileId))
{
//If file exists but is not in the memory id to location mapper then add it to the mapping
this.batchIdToFileMapper.Add(batchFileId, fileInfo.FullName);
}
//Check to see if the proxy has already uploaded this file to the service
return fileInfo.Exists;
}
/// <summary>
/// NOTE: This method takes in a file name as an input parameter and hence is suseptible for name canonicalization
/// attacks. This sample is meant to be a starting point in demonstrating how to transfer sync batch files and is
/// not intended to be a secure way of doing the same. This SHOULD NOT be used as such in production environment
/// without doing proper security analysis.
///
/// Please refer to the following two MSDN whitepapers for more information on guidelines for securing Web servies.
///
/// Design Guidelines for Secure Web Applications - https://msdn.microsoft.com/en-us/library/aa302420.aspx (Refer InputValidation section)
/// Architecture and Design Review for Security - https://msdn.microsoft.com/en-us/library/aa302421.aspx (Refer InputValidation section)
/// </summary>
/// <param name="batchFileId"></param>
/// <param name="batchContents"></param>
/// <param name="remotePeerId"></param>
public void UploadBatchFile(string batchFileId, byte[] batchContents, string remotePeerId)
{
Log("UploadBatchFile: {0}", this.peerProvider.Connection.ConnectionString);
try
{
if (HasUploadedBatchFile(batchFileId, remotePeerId))
{
//Service has already received this file. So dont save it again.
return;
}
//Service hasnt seen the file yet so save it.
String localFileLocation = Path.Combine(sessionBatchingDirectory.FullName, batchFileId);
FileStream fs = new FileStream(localFileLocation, FileMode.Create, FileAccess.Write);
using (fs)
{
fs.Write(batchContents, 0, batchContents.Length);
}
//Save this Id to file location mapping in the mapper object
this.batchIdToFileMapper[batchFileId] = localFileLocation;
}
catch (Exception e)
{
throw new FaultException<WebSyncFaultException>(new WebSyncFaultException("Unable to save batch file.", e));
}
}
/// <summary>
/// NOTE: This method takes in a file name as an input parameter and hence is suseptible for name canonicalization
/// attacks. This sample is meant to be a starting point in demonstrating how to transfer sync batch files and is
/// not intended to be a secure way of doing the same. This SHOULD NOT be used as such in production environment
/// without doing proper security analysis.
///
/// Please refer to the following two MSDN whitepapers for more information on guidelines for securing Web servies.
///
/// Design Guidelines for Secure Web Applications - https://msdn.microsoft.com/en-us/library/aa302420.aspx (Refer InputValidation section)
/// Architecture and Design Review for Security - https://msdn.microsoft.com/en-us/library/aa302421.aspx (Refer InputValidation section)
/// </summary>
/// <param name="batchFileId"></param>
/// <returns></returns>
public byte[] DownloadBatchFile(string batchFileId)
{
try
{
Log("DownloadBatchFile: {0}", this.peerProvider.Connection.ConnectionString);
Stream localFileStream = null;
string localBatchFileName = null;
if (!this.batchIdToFileMapper.TryGetValue(batchFileId, out localBatchFileName))
{
throw new FaultException<WebSyncFaultException>(new WebSyncFaultException("Unable to retrieve batch file for id." + batchFileId, null));
}
localFileStream = new FileStream(localBatchFileName, FileMode.Open, FileAccess.Read);
byte[] contents = new byte[localFileStream.Length];
localFileStream.Read(contents, 0, contents.Length);
return contents;
}
catch (Exception e)
{
throw new FaultException<WebSyncFaultException>(new WebSyncFaultException("Unable to read batch file for id " + batchFileId, e));
}
}
protected void Log(string p, params object[] paramArgs)
{
Console.WriteLine(p, paramArgs);
}
//Utility functions that the sub classes need to implement.
protected abstract RelationalSyncProvider ConfigureProvider(string scopeName, string hostName);
private void CheckAndCreateBatchingDirectory(string remotePeerId)
{
//Check to see if we have temp directory for this session.
if (sessionBatchingDirectory == null)
{
//Generate a unique Id for the directory
//We use the peer id of the store enumerating the changes so that the local temp directory is same for a given source
//across sync sessions. This enables us to restart a failed sync by not downloading already received files.
string sessionDir = Path.Combine(this.peerProvider.BatchingDirectory, "WebSync_" + remotePeerId);
sessionBatchingDirectory = new DirectoryInfo(sessionDir);
//Create the directory if it doesnt exist.
if (!sessionBatchingDirectory.Exists)
{
sessionBatchingDirectory.Create();
}
}
}
}
下列來自 RelationalProviderProxy
的程式碼範例會在 Web 服務上設定屬性及呼叫方法:
BatchingDirectory
:可讓應用程式設定中介層的批次處理目錄。EndSession
:從指定的目錄清除任何多工緩衝處理的檔案。GetChangeBatch
:藉由呼叫DownloadBatchFile
方法來下載變更批次。ProcessChangeBatch
:藉由呼叫UploadBatchFile
方法來上傳變更批次。
public abstract class RelationalProviderProxy : KnowledgeSyncProvider, IDisposable
{
protected IRelationalSyncContract proxy;
protected SyncIdFormatGroup idFormatGroup;
protected string scopeName;
protected DirectoryInfo localBatchingDirectory;
//Represents either the SQL server host name or the CE database file name. Sql database name
//is always peer1
//For this sample scopeName is always Sales
protected string hostName;
private string batchingDirectory = Environment.ExpandEnvironmentVariables("%TEMP%");
public string BatchingDirectory
{
get { return batchingDirectory; }
set
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException("value cannot be null or empty");
}
try
{
Uri uri = new Uri(value);
if (!uri.IsFile || uri.IsUnc)
{
throw new ArgumentException("value must be a local directory");
}
batchingDirectory = value;
}
catch (Exception e)
{
throw new ArgumentException("Invalid batching directory.", e);
}
}
}
public RelationalProviderProxy(string scopeName, string hostName)
{
this.scopeName = scopeName;
this.hostName = hostName;
this.CreateProxy();
this.proxy.Initialize(scopeName, hostName);
}
public override void BeginSession(SyncProviderPosition position, SyncSessionContext syncSessionContext)
{
this.proxy.BeginSession(position);
}
public override void EndSession(SyncSessionContext syncSessionContext)
{
proxy.EndSession();
if (this.localBatchingDirectory != null && this.localBatchingDirectory.Exists)
{
//Cleanup batching releated files from this session
this.localBatchingDirectory.Delete(true);
}
}
public override ChangeBatch GetChangeBatch(uint batchSize, SyncKnowledge destinationKnowledge, out object changeDataRetriever)
{
GetChangesParameters changesWrapper = proxy.GetChanges(batchSize, destinationKnowledge);
//Retrieve the ChangeDataRetriever and the ChangeBatch
changeDataRetriever = changesWrapper.DataRetriever;
DbSyncContext context = changeDataRetriever as DbSyncContext;
//Check to see if the data is batched.
if (context != null && context.IsDataBatched)
{
if (this.localBatchingDirectory == null)
{
//Retrieve the remote peer id from the MadeWithKnowledge.ReplicaId. MadeWithKnowledge is the local knowledge of the peer
//that is enumerating the changes.
string remotePeerId = context.MadeWithKnowledge.ReplicaId.ToString();
//Generate a unique Id for the directory.
//We use the peer id of the store enumerating the changes so that the local temp directory is same for a given source
//across sync sessions. This enables us to restart a failed sync by not downloading already received files.
string sessionDir = Path.Combine(this.batchingDirectory, "WebSync_" + remotePeerId);
this.localBatchingDirectory = new DirectoryInfo(sessionDir);
//Create the directory if it doesnt exist.
if (!this.localBatchingDirectory.Exists)
{
this.localBatchingDirectory.Create();
}
}
string localFileName = Path.Combine(this.localBatchingDirectory.FullName, context.BatchFileName);
FileInfo localFileInfo = new FileInfo(localFileName);
//Download the file only if doesnt exist
FileStream localFileStream = new FileStream(localFileName, FileMode.Create, FileAccess.Write);
if (!localFileInfo.Exists)
{
byte[] remoteFileContents = this.proxy.DownloadBatchFile(context.BatchFileName);
using (localFileStream)
{
localFileStream.Write(remoteFileContents, 0, remoteFileContents.Length);
}
}
//Set DbSyncContext.Batchfile name to the new local file name
context.BatchFileName = localFileName;
}
return changesWrapper.ChangeBatch;
}
public override FullEnumerationChangeBatch GetFullEnumerationChangeBatch(uint batchSize, SyncId lowerEnumerationBound, SyncKnowledge knowledgeForDataRetrieval, out object changeDataRetriever)
{
throw new NotImplementedException();
}
public override void GetSyncBatchParameters(out uint batchSize, out SyncKnowledge knowledge)
{
SyncBatchParameters wrapper = proxy.GetKnowledge();
batchSize = wrapper.BatchSize;
knowledge = wrapper.DestinationKnowledge;
}
public override SyncIdFormatGroup IdFormats
{
get
{
if (idFormatGroup == null)
{
idFormatGroup = new SyncIdFormatGroup();
//
// 1 byte change unit id (Harmonica default before flexible ids)
//
idFormatGroup.ChangeUnitIdFormat.IsVariableLength = false;
idFormatGroup.ChangeUnitIdFormat.Length = 1;
//
// Guid replica id
//
idFormatGroup.ReplicaIdFormat.IsVariableLength = false;
idFormatGroup.ReplicaIdFormat.Length = 16;
//
// Sync global id for item ids
//
idFormatGroup.ItemIdFormat.IsVariableLength = true;
idFormatGroup.ItemIdFormat.Length = 10 * 1024;
}
return idFormatGroup;
}
}
public override void ProcessChangeBatch(ConflictResolutionPolicy resolutionPolicy, ChangeBatch sourceChanges, object changeDataRetriever, SyncCallbacks syncCallbacks, SyncSessionStatistics sessionStatistics)
{
DbSyncContext context = changeDataRetriever as DbSyncContext;
if (context != null && context.IsDataBatched)
{
string fileName = new FileInfo(context.BatchFileName).Name;
//Retrieve the remote peer id from the MadeWithKnowledge.ReplicaId. MadeWithKnowledge is the local knowledge of the peer
//that is enumerating the changes.
string peerId = context.MadeWithKnowledge.ReplicaId.ToString();
//Check to see if service already has this file
if (!this.proxy.HasUploadedBatchFile(fileName, peerId))
{
//Upload this file to remote service
FileStream stream = new FileStream(context.BatchFileName, FileMode.Open, FileAccess.Read);
byte[] contents = new byte[stream.Length];
using (stream)
{
stream.Read(contents, 0, contents.Length);
}
this.proxy.UploadBatchFile(fileName, contents, peerId);
}
context.BatchFileName = fileName;
}
this.proxy.ApplyChanges(resolutionPolicy, sourceChanges, changeDataRetriever);
}
public override void ProcessFullEnumerationChangeBatch(ConflictResolutionPolicy resolutionPolicy, FullEnumerationChangeBatch sourceChanges, object changeDataRetriever, SyncCallbacks syncCallbacks, SyncSessionStatistics sessionStatistics)
{
throw new NotImplementedException();
}
protected abstract void CreateProxy();
#region IDisposable Members
public void Dispose()
{
this.proxy.Cleanup();
this.proxy = null;
GC.SuppressFinalize(this);
}
#endregion
}