Skip to content

Commit 36ece83

Browse files
committed
Adds Conflict Handling on updates, and allow hooks updates on older System's Date Time option (GlobalTimeStamp only)
1 parent 2fa0d75 commit 36ece83

File tree

11 files changed

+92
-31
lines changed

11 files changed

+92
-31
lines changed

NETCoreSync/NETCoreSync.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<PackageTags>database sync synchronization framework NETCoreApp NETStandard Xamarin Forms</PackageTags>
1212
<PackageReleaseNotes />
1313
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
14-
<Version>1.1.0</Version>
14+
<Version>1.2.0</Version>
1515
</PropertyGroup>
1616

1717
<ItemGroup>

NETCoreSync/SyncConfiguration.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public enum TimeStampStrategyEnum
2020
internal TimeStampStrategyEnum TimeStampStrategy = TimeStampStrategyEnum.GlobalTimeStamp;
2121
internal readonly List<Type> SyncTypes = new List<Type>();
2222
internal readonly Dictionary<Type, SchemaInfo> SyncSchemaInfos = new Dictionary<Type, SchemaInfo>();
23+
internal readonly Options SyncConfigurationOptions = new Options();
2324

2425
public SyncConfiguration(Assembly[] assemblies) : this(assemblies, TimeStampStrategyEnum.GlobalTimeStamp)
2526
{
@@ -94,6 +95,17 @@ private void Build(Type[] types, TimeStampStrategyEnum timeStampStrategy)
9495
}
9596
}
9697

98+
public SyncConfiguration SetOptions(Action<Options> options)
99+
{
100+
options(SyncConfigurationOptions);
101+
return this;
102+
}
103+
104+
public class Options
105+
{
106+
public bool GlobalTimeStampAllowHooksToUpdateWithOlderSystemDateTime { get; set; } = false;
107+
}
108+
97109
internal class SchemaInfo
98110
{
99111
public SyncSchemaAttribute SyncSchemaAttribute { get; set; }

NETCoreSync/SyncEngine.cs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -451,49 +451,48 @@ internal void ApplyChangesByKnowledge(ApplyChangesByKnowledgeParameter parameter
451451

452452
if (!isDeleted)
453453
{
454+
ConflictType updateConflictType = ConflictType.NoConflict;
454455
long localLastUpdated = (long)localData.GetType().GetProperty(localSchemaInfo.PropertyInfoLastUpdated.Name).GetValue(localData);
455-
string localDatabaseInstanceId = null;
456-
bool shouldUpdate = false;
457456
if (SyncConfiguration.TimeStampStrategy == SyncConfiguration.TimeStampStrategyEnum.GlobalTimeStamp)
458457
{
459-
if (lastUpdated > localLastUpdated) shouldUpdate = true;
458+
if (localLastUpdated > lastUpdated) updateConflictType = ConflictType.ExistingDataIsNewerThanIncomingData;
460459
}
461460
if (SyncConfiguration.TimeStampStrategy == SyncConfiguration.TimeStampStrategyEnum.DatabaseTimeStamp)
462461
{
463-
localDatabaseInstanceId = (string)localData.GetType().GetProperty(localSchemaInfo.PropertyInfoDatabaseInstanceId.Name).GetValue(localData);
462+
string localDatabaseInstanceId = (string)localData.GetType().GetProperty(localSchemaInfo.PropertyInfoDatabaseInstanceId.Name).GetValue(localData);
464463
string correctDatabaseInstanceId = GetCorrectDatabaseInstanceId(databaseInstanceId, sourceDatabaseInstanceId, destinationDatabaseInstanceId, null, 0);
465464
if (localDatabaseInstanceId == correctDatabaseInstanceId)
466465
{
467-
if (lastUpdated > localLastUpdated) shouldUpdate = true;
466+
if (localLastUpdated > lastUpdated) updateConflictType = ConflictType.ExistingDataIsNewerThanIncomingData;
468467
}
469468
else
470469
{
471-
shouldUpdate = true;
470+
updateConflictType = ConflictType.ExistingDataIsUpdatedByDifferentDatabaseInstanceId;
472471
}
473472
}
474-
if (shouldUpdate)
473+
474+
object existingData = InvokeDeserializeJsonToExistingData(localSyncType, jObjectData, localData, localId, transaction, operationType, updateConflictType, synchronizationId, customInfo, localSchemaInfo);
475+
if (existingData == null && updateConflictType == ConflictType.NoConflict) throw new SyncEngineConstraintException($"{nameof(DeserializeJsonToExistingData)} must not return null for conflictType equals to {ConflictType.NoConflict.ToString()}");
476+
if (existingData != null)
475477
{
476-
object existingData = InvokeDeserializeJsonToExistingData(localSyncType, jObjectData, localData, localId, transaction, operationType, synchronizationId, customInfo, localSchemaInfo);
477478
existingData.GetType().GetProperty(localSchemaInfo.PropertyInfoLastUpdated.Name).SetValue(existingData, lastUpdated);
478-
479479
if (SyncConfiguration.TimeStampStrategy == SyncConfiguration.TimeStampStrategyEnum.DatabaseTimeStamp)
480480
{
481481
existingData.GetType().GetProperty(localSchemaInfo.PropertyInfoDatabaseInstanceId.Name).SetValue(existingData, GetCorrectDatabaseInstanceId(databaseInstanceId, sourceDatabaseInstanceId, destinationDatabaseInstanceId, databaseInstanceMaxTimeStamps, lastUpdated));
482482
}
483-
484483
PersistData(localSyncType, existingData, false, transaction, operationType, synchronizationId, customInfo);
485484
if (!appliedIds.Contains(localId)) appliedIds.Add(localId);
486485
updates.Add(SyncLog.SyncLogData.FromJObject(InvokeSerializeDataToJson(localSyncType, existingData, localSchemaInfo, transaction, operationType, synchronizationId, customInfo), localSyncType, localSchemaInfo));
487486
}
488487
else
489488
{
490-
log.Add($"CONFLICT Detected: Target Data is newer than Source Data. Id: {id}");
491-
conflicts.Add(new SyncLog.SyncLogConflict(SyncLog.SyncLogConflict.ConflictTypeEnum.TargetDataIsNewerThanSource, SyncLog.SyncLogData.FromJObject(jObjectData, localSyncType, schemaInfo)));
489+
log.Add($"CONFLICT Detected: {updateConflictType.ToString()}. Id: {id}");
490+
conflicts.Add(new SyncLog.SyncLogConflict(updateConflictType, SyncLog.SyncLogData.FromJObject(jObjectData, localSyncType, schemaInfo)));
492491
}
493492
}
494493
else
495494
{
496-
object existingData = InvokeDeserializeJsonToExistingData(localSyncType, jObjectData, localData, localId, transaction, operationType, synchronizationId, customInfo, localSchemaInfo);
495+
object existingData = InvokeDeserializeJsonToExistingData(localSyncType, jObjectData, localData, localId, transaction, operationType, ConflictType.NoConflict, synchronizationId, customInfo, localSchemaInfo);
497496

498497
if (SyncConfiguration.TimeStampStrategy == SyncConfiguration.TimeStampStrategyEnum.GlobalTimeStamp)
499498
{
@@ -632,10 +631,11 @@ private object InvokeDeserializeJsonToNewData(Type classType, JObject jObject, o
632631
return newData;
633632
}
634633

635-
private object InvokeDeserializeJsonToExistingData(Type classType, JObject jObject, object data, object localId, object transaction, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo, SyncConfiguration.SchemaInfo localSchemaInfo)
634+
private object InvokeDeserializeJsonToExistingData(Type classType, JObject jObject, object data, object localId, object transaction, OperationType operationType, ConflictType conflictType, string synchronizationId, Dictionary<string, object> customInfo, SyncConfiguration.SchemaInfo localSchemaInfo)
636635
{
637-
object existingData = DeserializeJsonToExistingData(classType, jObject, data, transaction, operationType, synchronizationId, customInfo);
638-
if (existingData == null) throw new SyncEngineConstraintException($"{nameof(DeserializeJsonToExistingData)} must not return null");
636+
object existingData = DeserializeJsonToExistingData(classType, jObject, data, transaction, operationType, conflictType, synchronizationId, customInfo);
637+
if (conflictType != ConflictType.NoConflict && existingData == null) return null;
638+
if (existingData == null) throw new SyncEngineConstraintException($"{nameof(DeserializeJsonToExistingData)} must not return null for {nameof(conflictType)} equals to {conflictType.ToString()}");
639639
if (existingData.GetType().FullName != classType.FullName) throw new SyncEngineConstraintException($"Expected returned Type: {classType.FullName} during {nameof(DeserializeJsonToExistingData)}, but Type: {existingData.GetType().FullName} is returned instead.");
640640
object existingDataId = classType.GetProperty(localSchemaInfo.PropertyInfoId.Name).GetValue(existingData);
641641
if (!existingDataId.Equals(localId)) throw new SyncEngineConstraintException($"The returned Object Id ({existingDataId}) is different than the existing data Id: {localId}");

NETCoreSync/SyncEngineClasses.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ public enum OperationType
2323
ProvisionKnowledge = 3
2424
}
2525

26+
public enum ConflictType
27+
{
28+
NoConflict,
29+
ExistingDataIsNewerThanIncomingData,
30+
ExistingDataIsUpdatedByDifferentDatabaseInstanceId
31+
}
32+
2633
public class KnowledgeInfo
2734
{
2835
public string DatabaseInstanceId { get; set; }

NETCoreSync/SyncEngineInterfaces.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public virtual void EndTransaction(Type classType, object transaction, Operation
7474

7575
public abstract object DeserializeJsonToNewData(Type classType, JObject jObject, object transaction, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo);
7676

77-
public abstract object DeserializeJsonToExistingData(Type classType, JObject jObject, object data, object transaction, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo);
77+
public abstract object DeserializeJsonToExistingData(Type classType, JObject jObject, object data, object transaction, OperationType operationType, ConflictType conflictType, string synchronizationId, Dictionary<string, object> customInfo);
7878

7979
public abstract void PersistData(Type classType, object data, bool isNew, object transaction, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo);
8080

@@ -97,7 +97,7 @@ public void HookPreInsertOrUpdateGlobalTimeStamp(object data)
9797
if (!IsServerEngine())
9898
{
9999
long lastSync = GetClientLastSync();
100-
if (nowTicks <= lastSync) throw new SyncEngineConstraintException("System Date and Time is older than the lastSync value");
100+
if (nowTicks <= lastSync && !SyncConfiguration.SyncConfigurationOptions.GlobalTimeStampAllowHooksToUpdateWithOlderSystemDateTime) throw new SyncEngineConstraintException("System Date and Time is older than the lastSync value");
101101
}
102102
data.GetType().GetProperty(schemaInfo.PropertyInfoLastUpdated.Name).SetValue(data, nowTicks);
103103
}
@@ -117,7 +117,7 @@ public void HookPreDeleteGlobalTimeStamp(object data)
117117
if (!IsServerEngine())
118118
{
119119
long lastSync = GetClientLastSync();
120-
if (nowTicks <= lastSync) throw new SyncEngineConstraintException("System Date and Time is older than the lastSync value");
120+
if (nowTicks <= lastSync && !SyncConfiguration.SyncConfigurationOptions.GlobalTimeStampAllowHooksToUpdateWithOlderSystemDateTime) throw new SyncEngineConstraintException("System Date and Time is older than the lastSync value");
121121
}
122122
data.GetType().GetProperty(schemaInfo.PropertyInfoDeleted.Name).SetValue(data, nowTicks);
123123
}

NETCoreSync/SyncLog.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,10 @@ internal static SyncLogData FromJObject(JObject jObject, Type syncType, SyncConf
4646

4747
public class SyncLogConflict
4848
{
49-
public enum ConflictTypeEnum
50-
{
51-
TargetDataIsNewerThanSource
52-
}
53-
54-
public ConflictTypeEnum ConflictType { get; set; }
49+
public SyncEngine.ConflictType ConflictType { get; set; }
5550
public SyncLogData Data { get; set; }
5651

57-
public SyncLogConflict(ConflictTypeEnum conflictType, SyncLogData data)
52+
public SyncLogConflict(SyncEngine.ConflictType conflictType, SyncLogData data)
5853
{
5954
ConflictType = conflictType;
6055
Data = data;

Samples/DatabaseTimeStamp/MobileSample/Models/CustomSyncEngine.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,18 @@ public override object DeserializeJsonToNewData(Type classType, JObject jObject,
121121
return data;
122122
}
123123

124-
public override object DeserializeJsonToExistingData(Type classType, JObject jObject, object data, object transaction, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo)
124+
public override object DeserializeJsonToExistingData(Type classType, JObject jObject, object data, object transaction, OperationType operationType, ConflictType conflictType, string synchronizationId, Dictionary<string, object> customInfo)
125125
{
126+
if (conflictType != ConflictType.NoConflict)
127+
{
128+
// Here you can react accordingly if there's a conflict during Updates.
129+
// For DatabaseTimeStamp, the possibilities of conflict types are:
130+
// 1. ConflictType.ExistingDataIsNewerThanIncomingData, means that the ExistingData (parameter: data) timestamp is newer than the IncomingData (parameter: jObject).
131+
// 2. ConflictType.ExistingDataIsUpdatedByDifferentDatabaseInstanceId, means that the ExistingData (parameter: data) is updated by different Database Instance Id (perhaps updated by other devices) than the IncomingData (parameter: jObject) Database Instance Id.
132+
//
133+
// If you return null here, then the update will be canceled and the conflict will be registered in the SyncResult's Conflict Log.
134+
// In this example, the conflict is ignored and continue with the data update.
135+
}
126136
ConvertServerObjectToLocal(classType, jObject, data);
127137
JsonConvert.PopulateObject(jObject.ToString(), data);
128138
return data;

Samples/DatabaseTimeStamp/WebSample/Models/CustomSyncEngine.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,18 @@ public override object DeserializeJsonToNewData(Type classType, JObject jObject,
118118
return data;
119119
}
120120

121-
public override object DeserializeJsonToExistingData(Type classType, JObject jObject, object data, object transaction, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo)
121+
public override object DeserializeJsonToExistingData(Type classType, JObject jObject, object data, object transaction, OperationType operationType, ConflictType conflictType, string synchronizationId, Dictionary<string, object> customInfo)
122122
{
123+
if (conflictType != ConflictType.NoConflict)
124+
{
125+
// Here you can react accordingly if there's a conflict during Updates.
126+
// For DatabaseTimeStamp, the possibilities of conflict types are:
127+
// 1. ConflictType.ExistingDataIsNewerThanIncomingData, means that the ExistingData (parameter: data) timestamp is newer than the IncomingData (parameter: jObject).
128+
// 2. ConflictType.ExistingDataIsUpdatedByDifferentDatabaseInstanceId, means that the ExistingData (parameter: data) is updated by different Database Instance Id (perhaps updated by other devices) than the IncomingData (parameter: jObject) Database Instance Id.
129+
//
130+
// If you return null here, then the update will be canceled and the conflict will be registered in the SyncResult's Conflict Log.
131+
// In this example, the conflict is ignored and continue with the data update.
132+
}
123133
ConvertClientObjectToLocal(classType, jObject, data);
124134
JsonConvert.PopulateObject(jObject.ToString(), data);
125135
return data;

Samples/GlobalTimeStamp/MobileSample/App.xaml.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ public App()
3737

3838
List<Type> syncTypes = new List<Type>() { typeof(Department), typeof(Employee) };
3939
SyncConfiguration syncConfiguration = new SyncConfiguration(syncTypes.ToArray(), SyncConfiguration.TimeStampStrategyEnum.GlobalTimeStamp);
40+
syncConfiguration.SetOptions(options =>
41+
{
42+
// On this example, we set the GlobalTimeStampAllowHooksToUpdateWithOlderSystemDateTime to true,
43+
// This allows the mobile apps to continue executing HookPreInsertOrUpdateGlobalTimeStamp and HookPreDeleteGlobalTimeStamp without raising errors if the device's Date Time is older than the last sync value.
44+
// FYI, the mobile system's Date and Time is actually CAN be older if the mobile users are deliberately changing its system's Date Time settings to an older Date Time.
45+
// So if this option is set to true, the hooks will not raise errors, BUT, conflicts can happened during synchronization. You should handle the conflicts accordingly in the DeserializeJsonToExistingData method in the server's SyncEngine subclass.
46+
// By default, this option is set to false.
47+
options.GlobalTimeStampAllowHooksToUpdateWithOlderSystemDateTime = true;
48+
});
4049
builder.RegisterInstance(syncConfiguration);
4150

4251
Container = builder.Build();

Samples/GlobalTimeStamp/MobileSample/Models/CustomSyncEngine.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,17 @@ public override object DeserializeJsonToNewData(Type classType, JObject jObject,
6464
return data;
6565
}
6666

67-
public override object DeserializeJsonToExistingData(Type classType, JObject jObject, object data, object transaction, OperationType operationType, string synchronizationId, Dictionary<string, object> customInfo)
67+
public override object DeserializeJsonToExistingData(Type classType, JObject jObject, object data, object transaction, OperationType operationType, ConflictType conflictType, string synchronizationId, Dictionary<string, object> customInfo)
6868
{
69+
if (conflictType != ConflictType.NoConflict)
70+
{
71+
// Here you can react accordingly if there's a conflict during Updates.
72+
// For GlobalTimeStamp, the possibilities of conflict types are:
73+
// 1. ConflictType.ExistingDataIsNewerThanIncomingData, means that the ExistingData (parameter: data) timestamp is newer than the IncomingData (parameter: jObject).
74+
//
75+
// If you return null here, then the update will be canceled and the conflict will be registered in the SyncResult's Conflict Log.
76+
// In this example, the conflict is ignored and continue with the data update.
77+
}
6978
JsonConvert.PopulateObject(jObject.ToString(), data);
7079
return data;
7180
}

0 commit comments

Comments
 (0)