//-------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // File: SqlExpressClientSyncProvider.cs // // Description: Generic client synchronization provider. // //-------------------------------------------------------------------------- using Microsoft.Synchronization.Data; using Microsoft.Synchronization.Data.Server; using System; using System.Collections.Generic; using System.Data; using System.Data.Sql; using System.Data.SqlClient; using System.IO; using System.Text; namespace Microsoft.Samples.Synchronization.Data.SqlExpress { /// /// A generic client sync provider that can connect to SQL Express /// /// /// SqlExpressClientSyncProvider inherits from ClientSyncProvider, and it is a generic /// implementation of ServerSyncProvider. SqlExpressClientSyncProvider uses the mechanisms /// of DbServerSyncProvider to connect to the client (ie using a DBConnection) /// public class SqlExpressClientSyncProvider : ClientSyncProvider { // wraps a DbServerSyncProvider private DbServerSyncProvider _dbSyncProvider; private Guid _clientId; private int _refCntSession; private IDbTransaction _transaction; private const string GuidTableName = "guid"; private const string AnchorTableName = "anchor"; public event EventHandler ApplyChangeFailed; /// /// Default constructor /// public SqlExpressClientSyncProvider() { _dbSyncProvider = new DbServerSyncProvider(); _dbSyncProvider.ApplyingChanges += new EventHandler(_dbSyncProvider_ApplyingChanges); _dbSyncProvider.ApplyChangeFailed += new EventHandler(_dbSyncProvider_ApplyChangeFailed); _clientId = Guid.Empty; _refCntSession = 0; _transaction = null; } void _dbSyncProvider_ApplyChangeFailed(object sender, ApplyChangeFailedEventArgs e) { if (ApplyChangeFailed != null) { ApplyChangeFailed(sender, e); } } /// /// Roll ApplyChanges() modifications done by inner _dbSyncProvider /// into one transaction with anchor changes /// /// Event params /// Event params void _dbSyncProvider_ApplyingChanges(object sender, ApplyingChangesEventArgs e) { if (_transaction != null) e.Transaction = _transaction; } /// /// Apply changes downloaded from the server. /// /// /// Inner _dbSyncProvider will take care of applying changes to actual /// data, but we need to take care of updating anchor metadata. /// /// Contains table metadata info /// Contains changes to be applied /// Current sync session /// SyncContext object to Sync Agent /* public override SyncContext ApplyChanges(SyncGroupMetadata groupMetadata, DataSet dataSet, SyncSession syncSession) { SyncContext syncContext = _dbSyncProvider.ApplyChanges(groupMetadata, dataSet, syncSession); foreach (SyncTableMetadata table in groupMetadata.TablesMetadata) { SetTableReceivedAnchor(table.TableName, groupMetadata.NewAnchor); } return syncContext; } */ public override SyncContext ApplyChanges(SyncGroupMetadata groupMetadata, DataSet dataSet, SyncSession syncSession) { //Map SyncDirection from client POV to our internal server POV foreach (SyncTableMetadata tableMetadata in groupMetadata.TablesMetadata) { if (tableMetadata.SyncDirection == SyncDirection.DownloadOnly || tableMetadata.SyncDirection == SyncDirection.Snapshot) { //This SyncDirection DownloadOnly/Snapshot is from a Client point of view. But our client is inturn a Server provider. Hence switch this to UploadOnly tableMetadata.SyncDirection = SyncDirection.UploadOnly; } else if (tableMetadata.SyncDirection == SyncDirection.UploadOnly) { //This SyncDirection UploadOnly is from Client POV. But our client is inturn a Server provider. Hence switch this to DownloadOnly tableMetadata.SyncDirection = SyncDirection.DownloadOnly; } } // neet to set the LastReceivedAnchor as the LastSentAnchor since // DbServerSyncProvider operates from the server's perspective, so // we swap the two fields temporarily. // Note that even if we do this, the NewAnchor value will be the one // from the server, not local which is invalid since the client and server // clocks are always at least the tiniest bit misaligned foreach (SyncTableMetadata metaTable in groupMetadata.TablesMetadata) { SyncAnchor temp = metaTable.LastReceivedAnchor; metaTable.LastReceivedAnchor = metaTable.LastSentAnchor; metaTable.LastSentAnchor = temp; } // SyncTracer.Verbose("New Anchor: " + DeserializeAnchorValue(groupMetadata.NewAnchor.Anchor)); SyncContext syncContext = _dbSyncProvider.ApplyChanges(groupMetadata, dataSet, syncSession); // SyncTracer.Verbose("New Anchor: " + DeserializeAnchorValue(groupMetadata.NewAnchor.Anchor)); //swap them back for consistency foreach (SyncTableMetadata metaTable in groupMetadata.TablesMetadata) { SyncAnchor temp = metaTable.LastReceivedAnchor; metaTable.LastReceivedAnchor = metaTable.LastSentAnchor; metaTable.LastSentAnchor = temp; } foreach (SyncTableMetadata table in groupMetadata.TablesMetadata) { SetTableReceivedAnchor(table.TableName, groupMetadata.NewAnchor); } return syncContext; } /// /// Creates the database schema on client database -- NOT IMPLEMENTED /// /// /// In the current implementation of this class, we assume that the /// client already has the same schema as the server (run the demo scripts). /// public override void CreateSchema(SyncTable syncTable, SyncSchema syncSchema) { throw new NotSupportedException("Create Schema is not supported in this version." + "Please make sure client and server have same schema!"); } /// /// Gets the changes made on the client since last sync. /// /// Contains table metadata /// The current sync session /// SyncContext populated with the incremental changes public override SyncContext GetChanges(SyncGroupMetadata groupMetadata, SyncSession syncSession) { // neet to set the LastReceivedAnchor as the LastSentAnchor since // DbServerSyncProvider operates from the server's perspective, so // we swap the two fields temporarily. foreach (SyncTableMetadata metaTable in groupMetadata.TablesMetadata) { SyncAnchor temp = metaTable.LastReceivedAnchor; metaTable.LastReceivedAnchor = metaTable.LastSentAnchor; metaTable.LastSentAnchor = temp; } SyncContext context = _dbSyncProvider.GetChanges(groupMetadata, syncSession); //swap them back for consistency foreach (SyncTableMetadata metaTable in groupMetadata.TablesMetadata) { SyncAnchor temp = metaTable.LastReceivedAnchor; metaTable.LastReceivedAnchor = metaTable.LastSentAnchor; metaTable.LastSentAnchor = temp; } return context; } /// /// Gets the client's ID /// /// /// This function currently just reads the value in a database table /// named 'guid,' which is initialized upon client database creation /// (see demo script). /// /// A Guid object containing client's ID public Guid GetClientId() { if (_clientId == Guid.Empty) { IDbCommand guidCom = null; IDataReader reader = null; try { BeginTransaction(null); string queryStr = "SELECT Guid FROM " + GuidTableName; guidCom = new SqlCommand(queryStr); guidCom.Connection = _dbSyncProvider.Connection; guidCom.CommandType = CommandType.Text; guidCom.Transaction = _transaction; reader = guidCom.ExecuteReader(); if (reader.Read()) { _clientId = reader.GetGuid(0); } } catch { _clientId = Guid.Empty; throw; } finally { if (reader != null && !reader.IsClosed) reader.Close(); reader.Dispose(); guidCom.Dispose(); EndTransaction(true, null); } } return _clientId; } /// /// Retrieves the last received anchor from the 'anchor' metatable. /// /// The name of the table which we want the anchor for. /// A sync anchor object containing the last received anchor. public override SyncAnchor GetTableReceivedAnchor(string tableName) { string queryStr = "SELECT ReceivedAnchor FROM " + AnchorTableName + " WHERE TableName = '" + tableName + "'"; IDbCommand receivedAnchorCom = new SqlCommand(queryStr); receivedAnchorCom.Connection = _dbSyncProvider.Connection; receivedAnchorCom.CommandType = CommandType.Text; receivedAnchorCom.Transaction = _transaction; object anchorVal = null; bool commandPassed = false; try { BeginTransaction(null); anchorVal = receivedAnchorCom.ExecuteScalar(); commandPassed = true; } finally { receivedAnchorCom.Dispose(); EndTransaction(commandPassed, null); } if (anchorVal == null || anchorVal == System.DBNull.Value) return new SyncAnchor(); else { return new SyncAnchor(SerializeAnchorValue(anchorVal)); } } private byte[] SerializeAnchorValue(object anchorVal) { //if (anchorVal is byte[]) //{ // return (byte[])anchorVal; //} //else //{ MemoryStream serializationStream = new MemoryStream(); new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter().Serialize(serializationStream, anchorVal); byte[] ret = serializationStream.ToArray(); serializationStream.Dispose(); return ret; //} } private object DeserializeAnchorValue(byte[] anchor) { MemoryStream serializationStream = new MemoryStream(anchor); object ret = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter().Deserialize(serializationStream); serializationStream.Dispose(); return ret; } /// /// Retrieves the last sent anchor from the 'anchor' metatable. /// /// The name of the table for which we want the anchor. /// A sync anchor object containing the last sent anchor. public override SyncAnchor GetTableSentAnchor(string tableName) { string queryStr = "SELECT SentAnchor FROM " + AnchorTableName + " WHERE TableName = '" + tableName + "'"; IDbCommand sentAnchorCom = new SqlCommand(queryStr); sentAnchorCom.Connection = _dbSyncProvider.Connection; sentAnchorCom.CommandType = CommandType.Text; sentAnchorCom.Transaction = _transaction; object anchorVal = null; bool commandPassed = false; try { BeginTransaction(null); anchorVal = sentAnchorCom.ExecuteScalar(); commandPassed = true; } finally { sentAnchorCom.Dispose(); EndTransaction(commandPassed, null); } if (anchorVal == null || anchorVal == System.DBNull.Value) return new SyncAnchor(); else return new SyncAnchor(SerializeAnchorValue(anchorVal)); } /// /// Sets the last received anchor in the 'anchor' metatable /// /// The name of the table for which we want to set the anchor /// SyncAnchor object containing the anchor. public override void SetTableReceivedAnchor(string tableName, SyncAnchor anchor) { string queryStr = "UPDATE " + AnchorTableName + " SET ReceivedAnchor = @anchor WHERE TableName = '" + tableName + "'"; SqlCommand anchorCom = new SqlCommand(queryStr); anchorCom.Parameters.AddWithValue("@anchor", DeserializeAnchorValue(anchor.Anchor)); anchorCom.Connection = (SqlConnection)_dbSyncProvider.Connection; anchorCom.Transaction = (SqlTransaction)_transaction; bool commandPassed = false; try { BeginTransaction(null); if (anchorCom.ExecuteNonQuery() == 0) throw new Exception("SetTableReceivedAnchor() had no effect"); commandPassed = true; } finally { anchorCom.Dispose(); EndTransaction(commandPassed, null); } } /// /// Sets teh last sent anchor in the 'anchor' metatable /// /// The name of the table for whcih we want to set the anchor /// SyncAnchor object containing the anchor. public override void SetTableSentAnchor(string tableName, SyncAnchor anchor) { string queryStr = "UPDATE " + AnchorTableName + " SET SentAnchor = @anchor WHERE TableName = '" + tableName + "'"; SqlCommand anchorCom = new SqlCommand(queryStr); anchorCom.Parameters.AddWithValue("@anchor", DeserializeAnchorValue(anchor.Anchor)); anchorCom.Connection = (SqlConnection)_dbSyncProvider.Connection; anchorCom.Transaction = (SqlTransaction)_transaction; bool commandPassed = false; try { BeginTransaction(null); if (anchorCom.ExecuteNonQuery() == 0) throw new Exception("SetTableSentAnchor() had no effect"); commandPassed = true; } finally { anchorCom.Dispose(); EndTransaction(commandPassed, null); } } /// /// Begin a transaction. This method is invoked to mark atomic operations. /// /// SyncSession information public override void BeginTransaction(SyncSession syncSession) { _refCntSession++; if (Connection.State == ConnectionState.Closed) { Connection.Open(); } if (_transaction == null) { _transaction = _dbSyncProvider.Connection.BeginTransaction(); } } /// /// End transaction. This method is called by the agent at to conclude an atomic operation. /// /// Commit/Abort SyncSession /// SyncSession information public override void EndTransaction(bool commit, SyncSession syncSession) { _refCntSession--; if (_refCntSession == 0) { if (commit) _transaction.Commit(); else _transaction.Rollback(); _transaction.Dispose(); _transaction = null; Connection.Close(); } if (_refCntSession < 0) _refCntSession = 0; //else if (_refCntSession < 0) // throw new SyncException("Transaction error: _refCntSession < 0"); } /// /// Disposes this Client Sync Provider instance /// public override void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } /// /// Disposes this Client Sync Provider instance /// /// True if explicit finalization, false if through GC //[SuppressMessage("Microsoft.Reliability", "CA2004:RemoveCallsToGCKeepAlive")] protected virtual void Dispose(bool disposing) { _dbSyncProvider.Dispose(); } #region public properties /// /// Gets or sets the command that will return the new anchor value. /// /// /// A IDbCommand-inheritated object. /// public IDbCommand SelectNewAnchorCommand { get { return _dbSyncProvider.SelectNewAnchorCommand; } set { _dbSyncProvider.SelectNewAnchorCommand = value; } } /// /// Gets or sets the server connection object. /// /// /// A DbConnection object. /// public IDbConnection Connection { get { return _dbSyncProvider.Connection; } set { if (value == null) { throw new ArgumentNullException("value"); } if (value.State == ConnectionState.Closed) { // giving the connection to the _dbSyncPRovider as open // will prevent it from closing off the connection at end // of operations value.Open(); _dbSyncProvider.Connection = value; value.Close(); } else _dbSyncProvider.Connection = value; } } /// /// Gets the collection of SyncAdapter. /// /// /// A SyncAdapterCollection object. /// public SyncAdapterCollection SyncAdapters { get { return _dbSyncProvider.SyncAdapters; } } /// /// Gets and sets ClientId /// /// /// A new client is generated and saved if none is present /// /// /// A Guid object. /// public override Guid ClientId { get { return GetClientId(); } set { _clientId = value; } } #endregion } }