Hi, first i want to thank you for this great framework.
I am playing with it in a small xamarin android application. The mobile use sqlite and the server use sql.
If my english is not perfect it is normal, it's not my primary language so excuse me for that.
I am facing a problem and it would be really nice if you could help me.
I use DatabaseTimeStamp and do PullThenPush on the mobile. Synchronization is working but when one mobile device receive an object modified by another mobile device, it push it to the server again.
I don't know if that make sense to you, i will describe the scenario :
Let say we have 2 phones : Phone 1 and Phone 2 and they use the same SynchronizationId "mySyncID"
- Phone 1 create a User, call save and then sync the data :
var user = new User { Email = "[email protected]", Password = "myPassword" };
user.Save();
await Task.Run(async () =>
{
await _syncManager.Synchronize();
});
-
Then Phone 2 do a synchro and receive User { Email = "[email protected]", Password = "myPassword" }
-
After receiving the user, Phone 2 edit the User and change the password to "1234"
and
call the synchronize function.
-
Once Phone 2 have finish its synchro process, Phone 1 init a synchronization and it is here i have a problem.
-Phone 1 will first receive the User with the new password (everything is fine here) and THEN
push the user again to the server. The server already know this data, so something is missing in my code, i think, or this is an issue or by design.
If you can help me understand.
Here is my implementation for the Mobile
In the IoC usin MVVMCross i have
List<Type> syncTypes = new List<Type>() { typeof(UserModel) };
SyncConfiguration syncConfiguration = new SyncConfiguration(syncTypes.ToArray(), SyncConfiguration.TimeStampStrategyEnum.DatabaseTimeStamp);
Mvx.IoCProvider.RegisterSingleton(syncConfiguration);
Model class
public class BaseModel
{
[PrimaryKey]
[SyncProperty(PropertyIndicator = SyncPropertyAttribute.PropertyIndicatorEnum.Id)]
public string Id { get; set; } = Guid.Empty.ToString();
public DateTime CreatedAt { get; set; }
[SyncProperty(PropertyIndicator = SyncPropertyAttribute.PropertyIndicatorEnum.LastUpdated)]
public long LastUpdated { get; set; }
[SyncProperty(PropertyIndicator = SyncPropertyAttribute.PropertyIndicatorEnum.Deleted)]
public bool Deleted { get; set; }
[SyncProperty(PropertyIndicator = SyncPropertyAttribute.PropertyIndicatorEnum.DatabaseInstanceId)]
public string DatabaseInstanceId { get; set; }
}
[SyncSchema(MapToClassName = "UserModel")]
internal class UserModel : BaseModel, IUserModel
{
public string Email { get; set; }
public string Password { get; set; }
}
public class Knowledge
{
[PrimaryKey]
public string DatabaseInstanceId { get; set; }
public bool IsLocal { get; set; }
public long MaxTimeStamp { get; set; }
public override string ToString()
{
return $"{nameof(DatabaseInstanceId)}: {DatabaseInstanceId}, {nameof(IsLocal)}: {IsLocal}, {nameof(MaxTimeStamp)}: {MaxTimeStamp}";
}
}
public class TimeStamp
{
[PrimaryKey]
public string Id { get; set; } = Guid.NewGuid().ToString();
public long Counter { get; set; }
public override string ToString()
{
return $"{nameof(Id)}: {Id}, {nameof(Counter)}: {Counter}";
}
}
The save function on the Mobile
public TType Save<TType>(TType remoteType) where TType : BaseModel, new()
{
try
{
_customSyncEngine.HookPreInsertOrUpdateDatabaseTimeStamp(remoteType, null, this.GetSynchronizationId(), null);
int count = 0;
if (Guid.Parse(remoteType.Id) == Guid.Empty)
{
remoteType.Id = Guid.NewGuid().ToString();
remoteType.CreatedAt = DateTime.Now;
count = MtSql.Helper.Insert(remoteType);
return remoteType;
}
count = MtSql.Helper.Update(remoteType);
return remoteType;
}
catch (Exception e)
{
throw e;
}
}
CustomSyncEngine on the Mobile
public class CustomSyncEngine : SyncEngine
{
private readonly Dictionary<Type, CustomContractResolver> customContractResolvers;
public CustomSyncEngine(SyncConfiguration syncConfiguration) : base(syncConfiguration)
{
customContractResolvers = new Dictionary<Type, CustomContractResolver>();
}
public override long GetNextTimeStamp()
{
TimeStamp timeStamp = MtSql.Helper.GetAll<TimeStamp>().FirstOrDefault();
if (timeStamp == null)
{
timeStamp = new TimeStamp();
MtSql.Helper.Insert(timeStamp);
timeStamp = MtSql.Helper.GetAll<TimeStamp>().First();
}
timeStamp.Counter++;
MtSql.Helper.Update(timeStamp);
return timeStamp.Counter;
}
public override List<KnowledgeInfo> GetAllKnowledgeInfos(string synchronizationId, Dictionary<string, object> customInfo)
{
List<Knowledge> knowledges = MtSql.Helper.GetAll<Knowledge>();
List<KnowledgeInfo> result = new List<KnowledgeInfo>();
for (int i = 0; i < knowledges.Count; i++)
{
result.Add(new KnowledgeInfo()
{
DatabaseInstanceId = knowledges[i].DatabaseInstanceId,
IsLocal = knowledges[i].IsLocal,
MaxTimeStamp = knowledges[i].MaxTimeStamp
});
}
return result;
}
public override void CreateOrUpdateKnowledgeInfo(KnowledgeInfo knowledgeInfo, string synchronizationId, Dictionary<string, object> customInfo)
{
Knowledge knowledge = MtSql.Helper.FirstOrDefault<Knowledge>(x => x.DatabaseInstanceId == knowledgeInfo.DatabaseInstanceId);
if (knowledge == null)
{
knowledge = new Knowledge();
knowledge.DatabaseInstanceId = knowledgeInfo.DatabaseInstanceId;
knowledge.IsLocal = knowledgeInfo.IsLocal;
knowledge.MaxTimeStamp = knowledgeInfo.MaxTimeStamp;
MtSql.Helper.Insert(knowledge);
knowledge = MtSql.Helper.FirstOrDefault<Knowledge>(x => x.DatabaseInstanceId == knowledgeInfo.DatabaseInstanceId);
}
knowledge.IsLocal = knowledgeInfo.IsLocal;
knowledge.MaxTimeStamp = knowledgeInfo.MaxTimeStamp;
MtSql.Helper.Update(knowledge);
}
public override IQueryable GetQueryable(Type classType, object transaction, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo)
{
if (classType == typeof(UserModel))
return MtSql.Helper.Conn.Table<UserModel>().AsQueryable();
throw new NotImplementedException();
}
public override string SerializeDataToJson(Type classType, object data, object transaction, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo)
{
if (!customContractResolvers.ContainsKey(classType))
customContractResolvers.Add(classType, new CustomContractResolver(classType));
customContractResolvers.Add(typeof(BaseModel), new CustomContractResolver(typeof(BaseModel)));
CustomContractResolver customContractResolver = customContractResolvers[classType];
string json = JsonConvert.SerializeObject(data, new JsonSerializerSettings() { ContractResolver = customContractResolver });
return json;
}
public override object DeserializeJsonToNewData(Type classType, JObject jObject, object transaction, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo)
{
object data = Activator.CreateInstance(classType);
JsonConvert.PopulateObject(jObject.ToString(), data);
return data;
}
public override object DeserializeJsonToExistingData(Type classType, JObject jObject, object data, object transaction, OperationType operationType, ConflictType conflictType, string synchronizationId, Dictionary<string, object> customInfo)
{
if (conflictType != ConflictType.NoConflict)
{
if (conflictType == ConflictType.ExistingDataIsNewerThanIncomingData)
{
return null;
}
}
JsonConvert.PopulateObject(jObject.ToString(), data);
return data;
}
public override void PersistData(Type classType, object data, bool isNew, object transaction, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo)
{
if (isNew)
{
MtSql.Helper.Insert(data);
}
else
{
MtSql.Helper.Update(data);
}
}
public override object TransformIdType(Type classType, JValue id, object transaction, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo)
{
return id.Value<string>();
}
public override void PostEventDelete(Type classType, object id, string synchronizationId, Dictionary<string, object> customInfo)
{
}
public class CustomContractResolver : DefaultContractResolver
{
private readonly Type rootType;
public CustomContractResolver(Type rootType)
{
this.rootType = rootType;
}
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
var list = base.CreateProperties(type, memberSerialization);
list = list.Where(w => w.DeclaringType.FullName == type.FullName).ToList();
list = list.Where(w => !(w.PropertyType.IsGenericType && w.PropertyType.GetGenericTypeDefinition() == typeof(IQueryable<>))).ToList();
if (type != rootType)
list = list.Where(w => w.PropertyName == "Id").ToList();
return list;
}
}
}
and my SynchronizationManager class on Mobile
public class SynchronizationManager
{
public event EventHandler<SynchroDoneEventArgs> SynchroDoneEvent;
#region Private Fields
private readonly IDataAccessService _dataAccessService;
private readonly SyncConfiguration _syncConfiguration;
private bool _isSynchronizing;
private string _log;
private string _serverUrl;
#endregion Private Fields
#region Public Constructors
public SynchronizationManager(IDataAccessService dataAccessService, SyncConfiguration syncConfiguration)
{
_dataAccessService = dataAccessService;
_syncConfiguration = syncConfiguration;
_serverUrl = _dataAccessService.GetServerUrl();
}
#endregion Public Constructors
#region Public Properties
public bool IsSynchronizing
{
get { return _isSynchronizing; }
set { _isSynchronizing = value; }
}
public string Log
{
get { return _log; }
set { _log = value; }
}
public string ServerUrl
{
get { return _serverUrl; }
set { _serverUrl = value; }
}
#endregion Public Properties
public async Task Synchronize()
{
if (IsSynchronizing)
{
Console.WriteLine("Already in synchro...");
return;
}
this.IsSynchronizing = true;
try
{
if (string.IsNullOrEmpty(this.ServerUrl))
throw new Exception("Please specify Server URL");
string synchronizationId = _dataAccessService.GetSynchronizationId();
if (string.IsNullOrEmpty(synchronizationId))
throw new NullReferenceException(nameof(synchronizationId));
CustomSyncEngine customSyncEngine = new CustomSyncEngine(_syncConfiguration);
SyncClient syncClient = new SyncClient(synchronizationId, customSyncEngine, ServerUrl);
this.Log = "";
SyncResult result = await syncClient.SynchronizeAsync(SynchronizationMethodEnum.PullThenPush);
string tempLog = "";
tempLog += $"Client Log: {Environment.NewLine}";
tempLog += $"Sent Changes Count: {result.ClientLog.SentChanges.Count}{Environment.NewLine}";
tempLog += $"Applied Changes Insert Count: {result.ClientLog.AppliedChanges.Inserts.Count}{Environment.NewLine}";
tempLog += $"Applied Changes Updates Count: {result.ClientLog.AppliedChanges.Updates.Count}{Environment.NewLine}";
tempLog += $"Applied Changes Deletes Count: {result.ClientLog.AppliedChanges.Deletes.Count}{Environment.NewLine}";
tempLog += $"Applied Changes Conflicts Count: {result.ClientLog.AppliedChanges.Conflicts.Count}{Environment.NewLine}";
tempLog += $"{Environment.NewLine}";
tempLog += $"Server Log: {Environment.NewLine}";
tempLog += $"Sent Changes Count: {result.ServerLog.SentChanges.Count}{Environment.NewLine}";
tempLog += $"Applied Changes Insert Count: {result.ServerLog.AppliedChanges.Inserts.Count}{Environment.NewLine}";
tempLog += $"Applied Changes Updates Count: {result.ServerLog.AppliedChanges.Updates.Count}{Environment.NewLine}";
tempLog += $"Applied Changes Deletes Count: {result.ServerLog.AppliedChanges.Deletes.Count}{Environment.NewLine}";
tempLog += $"Applied Changes Conflicts Count: {result.ServerLog.AppliedChanges.Conflicts.Count}{Environment.NewLine}";
tempLog += $"{Environment.NewLine}";
tempLog += $"Detail Log: {Environment.NewLine}";
for (int i = 0; i < result.Log.Count; i++)
{
tempLog += $"{result.Log[i]}{Environment.NewLine}";
}
Log = tempLog;
if (!string.IsNullOrEmpty(result.ErrorMessage))
throw new Exception($"Synchronization Error: {result.ErrorMessage}");
SynchroDoneEvent.Invoke(this, new SynchroDoneEventArgs());
Console.WriteLine(Log);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
finally
{
this.IsSynchronizing = false;
}
}
public class SynchroDoneEventArgs : EventArgs
{
}
}
On the Server part i am using EntityFrameworkCore
in the Startup class
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using NETCoreSync;
using SyncServer.Models;
using System;
using System.Collections.Generic;
namespace SyncServer
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DatabaseContext>(options =>
{
var connection = @"Data Source=SQLEXPRESS01; User Id=my_user; Password=blabla; Integrated Security=false; User Instance=false;Initial Catalog=MyDB;MultipleActiveResultSets=True;";
options.UseSqlServer(connection);
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
List<Type> syncTypes = new List<Type>() { typeof(UserModel) };
SyncConfiguration syncConfiguration = new SyncConfiguration(syncTypes.ToArray(), SyncConfiguration.TimeStampStrategyEnum.DatabaseTimeStamp);
services.AddSingleton(syncConfiguration);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Sync}/{action=Index}/{id?}");
});
}
}
}
Model
public class BaseModel
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[SyncProperty(PropertyIndicator = SyncPropertyAttribute.PropertyIndicatorEnum.Id)]
public Guid Id { get; set; }
[SyncProperty(PropertyIndicator = SyncPropertyAttribute.PropertyIndicatorEnum.LastUpdated)]
public long LastUpdated { get; set; }
[SyncProperty(PropertyIndicator = SyncPropertyAttribute.PropertyIndicatorEnum.Deleted)]
public bool Deleted { get; set; }
[SyncProperty(PropertyIndicator = SyncPropertyAttribute.PropertyIndicatorEnum.DatabaseInstanceId)]
public string DatabaseInstanceId { get; set; }
public string SynchronizationID { get; set; }
}
[SyncSchema(MapToClassName = "UserModel")]
public class UserModel : BaseModel
{
public string Email { get; set; }
public string Password { get; set; }
}
public class Knowledge
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public Guid DatabaseInstanceId { get; set; }
public string SynchronizationID { get; set; }
public bool IsLocal { get; set; }
public long MaxTimeStamp { get; set; }
}
public class DatabaseContext : DbContext
{
public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options)
{
}
public DbSet<Knowledge> Knowledges { get; set; }
public DbSet<UserModel> UserModels { get; set; }
public DbQuery<CustomSyncEngine.DbQueryTimeStampResult> DbQueryTimeStampResults { get; set; }
}
CustomSyncEngine implementation on Server
public class CustomSyncEngine : SyncEngine
{
private readonly DatabaseContext databaseContext;
private readonly Dictionary<Type, CustomContractResolver> customContractResolvers;
public CustomSyncEngine(DatabaseContext databaseContext, SyncConfiguration syncConfiguration) : base(syncConfiguration)
{
this.databaseContext = databaseContext;
customContractResolvers = new Dictionary<Type, CustomContractResolver>();
}
public override long GetNextTimeStamp()
{
DbQueryTimeStampResult result = databaseContext.DbQueryTimeStampResults.FromSql("SELECT CAST( @@DBTS as bigint) AS timestamp").First();
return result.timestamp;
}
public override List<KnowledgeInfo> GetAllKnowledgeInfos(string synchronizationId, Dictionary<string, object> customInfo)
{
var knowledge = databaseContext.Knowledges.Where(w => w.SynchronizationID == synchronizationId).Select(s => new KnowledgeInfo()
{
DatabaseInstanceId = s.DatabaseInstanceId.ToString(),
IsLocal = s.IsLocal,
MaxTimeStamp = s.MaxTimeStamp
}).ToList();
return knowledge;
}
public override void CreateOrUpdateKnowledgeInfo(KnowledgeInfo knowledgeInfo, string synchronizationId, Dictionary<string, object> customInfo)
{
Guid id = new Guid(knowledgeInfo.DatabaseInstanceId);
Knowledge knowledge = databaseContext.Knowledges.Where(w => w.SynchronizationID == synchronizationId && w.DatabaseInstanceId == id).FirstOrDefault();
if (knowledge == null)
{
knowledge = new Knowledge();
knowledge.DatabaseInstanceId = id;
knowledge.SynchronizationID = synchronizationId;
databaseContext.Add(knowledge);
databaseContext.SaveChanges();
knowledge = databaseContext.Knowledges.Where(w => w.SynchronizationID == synchronizationId && w.DatabaseInstanceId == id).First();
}
knowledge.IsLocal = knowledgeInfo.IsLocal;
knowledge.MaxTimeStamp = knowledgeInfo.MaxTimeStamp;
databaseContext.Update(knowledge);
databaseContext.SaveChanges();
}
public override object StartTransaction(Type classType, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo)
{
IDbContextTransaction transaction = null;
if (operationType == OperationType.ApplyChanges || operationType == OperationType.ProvisionKnowledge)
transaction = databaseContext.Database.BeginTransaction();
return transaction;
}
public override void CommitTransaction(Type classType, object transaction, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo)
{
if (transaction != null)
{
((IDbContextTransaction)transaction).Commit();
}
}
public override void RollbackTransaction(Type classType, object transaction, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo)
{
if (transaction != null)
{
((IDbContextTransaction)transaction).Rollback();
}
}
public override void EndTransaction(Type classType, object transaction, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo)
{
if (transaction != null)
{
((IDbContextTransaction)transaction).Dispose();
}
}
public override IQueryable GetQueryable(Type classType, object transaction, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo)
{
if (classType == typeof(UserModel))
return databaseContext.UserModels.Where(w => w.SynchronizationID == synchronizationId).AsQueryable();
throw new NotImplementedException();
}
public override string SerializeDataToJson(Type classType, object data, object transaction, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo)
{
List<string> ignoreProperties = new List<string>();
ignoreProperties.Add("SynchronizationID");
if (!customContractResolvers.ContainsKey(classType))
customContractResolvers.Add(classType, new CustomContractResolver(null, ignoreProperties));
CustomContractResolver customContractResolver = customContractResolvers[classType];
string json = JsonConvert.SerializeObject(data, new JsonSerializerSettings() { ContractResolver = customContractResolver });
return json;
}
public override object DeserializeJsonToNewData(Type classType, JObject jObject, object transaction, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo)
{
object data = Activator.CreateInstance(classType);
JsonConvert.PopulateObject(jObject.ToString(), data);
classType.GetProperty("SynchronizationID").SetValue(data, synchronizationId);
return data;
}
public override object DeserializeJsonToExistingData(Type classType, JObject jObject, object data, object transaction, OperationType operationType, ConflictType conflictType, string synchronizationId, Dictionary<string, object> customInfo)
{
if (conflictType != ConflictType.NoConflict)
{
if (conflictType == ConflictType.ExistingDataIsNewerThanIncomingData)
{
return null;
}
}
JsonConvert.PopulateObject(jObject.ToString(), data);
return data;
}
public override void PersistData(Type classType, object data, bool isNew, object transaction, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo)
{
if (isNew)
{
databaseContext.Add(data);
}
else
{
databaseContext.Update(data);
}
databaseContext.SaveChanges();
}
public override object TransformIdType(Type classType, JValue id, object transaction, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo)
{
return new Guid(id.Value<string>());
}
public override void PostEventDelete(Type classType, object id, string synchronizationId, Dictionary<string, object> customInfo)
{
}
public class CustomContractResolver : DefaultContractResolver
{
private readonly Dictionary<string, string> renameProperties;
private readonly List<string> ignoreProperties;
public CustomContractResolver(Dictionary<string, string> renameProperties, List<string> ignoreProperties)
{
this.renameProperties = renameProperties;
this.ignoreProperties = ignoreProperties;
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty jsonProperty = base.CreateProperty(member, memberSerialization);
if (renameProperties != null && renameProperties.ContainsKey(jsonProperty.PropertyName))
{
jsonProperty.PropertyName = renameProperties[jsonProperty.PropertyName];
}
if (ignoreProperties != null && ignoreProperties.Contains(jsonProperty.PropertyName))
{
jsonProperty.ShouldSerialize = i => false;
jsonProperty.Ignored = true;
}
return jsonProperty;
}
}
public class DbQueryTimeStampResult
{
public long timestamp { get; set; }
}
}
SyncController with Index function
public class SyncController : Controller
{
private readonly DatabaseContext _context;
private SyncConfiguration _syncConfiguration;
public SyncController(DatabaseContext context, SyncConfiguration syncConfiguration)
{
_context = context;
_syncConfiguration = syncConfiguration;
}
[HttpPost]
[Consumes("multipart/form-data")]
public IActionResult Index()
{
try
{
CustomSyncEngine customSyncEngine = new CustomSyncEngine(_context, _syncConfiguration);
NETCoreSync.SyncServer syncServer = new NETCoreSync.SyncServer(customSyncEngine);
IFormFile syncData = Request.Form.Files.FirstOrDefault();
if (syncData == null)
throw new NullReferenceException(nameof(syncData));
byte[] syncDataBytes = null;
using (var memoryStream = new MemoryStream())
{
syncData.CopyTo(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
syncDataBytes = new byte[memoryStream.Length];
memoryStream.Read(syncDataBytes, 0, syncDataBytes.Length);
}
JObject result = syncServer.Process(syncDataBytes);
return Json(result);
}
catch (Exception e)
{
return Json(NETCoreSync.SyncServer.JsonErrorResponse(e.Message));
}
}
}
I would really appreciate your help !
thank you !