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