NETCoreSync is a database synchronization framework where each client's local offline database

Overview

NETCoreSync

NETCoreSync is a database synchronization framework where each client's local offline database (on each client's multiple devices) can be synchronized on-demand via network into a single centralized database hosted on a server. Data which are stored locally within each device of a single client can all be synchronized after each device have successfully performed the synchronization operation.

This framework has two components that needs to be implemented in each of your client and server projects, and this framework has two versions that is determined by how you've built your client projects.

Flutter Version

build codecov

Client Client Generator Server
netcoresync_moor version netcoresync_moor_generator version Nuget

If the client is built using Flutter, use the Flutter version of NETCoreSync. This version provides netcoresync_moor package for the client side, and the NETCorSyncServer package for the server side. The client's netcoresync_moor package is built on top of Flutter's Moor library, and the server's NETCoreSyncServer package is built using Microsoft .NET 5.0 ASP .NET Core Middleware.

Repository Folders

The repository folders are

Xamarin Version

Client + Server
NETCoreSync version

If the client is built using Xamarin, use the Xamarin version of NETCoreSync. This version provides NETCoreSync package for both client and server side. The NETCoreSync package is built using Microsoft .NET Standard 2.0.

Repository Folders

The repository folders are

  • NETCoreSync: Client-side and Server-side .NET Standard 2.0 package
  • Samples/GlobalTimeStamp: Client-side Xamarin and Server-Side .NET Core 3.1 project example for the GlobalTimeStamp synchronization approach
  • Samples/DatabaseTimeStamp: Client-side Xamarin and Server-Side .NET Core 3.1 project example for the DatabaseTimeStamp synchronization approach

Characteristics

The following lists the characteristics of this framework (applies for both Flutter and Xamarin versions):

  • It's database-agnostic, means, it can be used with any kind of database technology, as long as you can direct it (technically by subclassing its engine) to the correct implementation on how to do this-and-that in your specific database.
    • For Flutter version, the client side framework is built on top of Moor and will use any database that is configured with it, so therefore the client side is not database-agnostic, while the server side framework is still database-agnostic.
    • For Xamarin version, the client and server side framework are fully database-agnostic.
  • Not like other synchronization frameworks, NETCoreSync doesn't use tracking tables and tombstone tables (I hate them, because most likely they will double up your row count and take storage space), but, you need to add some additional columns to your tables.
  • Not like other synchronization frameworks, NETCoreSync doesn't use triggers (I also hate triggers, because not all database technology support triggers, and it always feels like I have some unused left-over triggers somewhere in my tables...), but, you have to modify your application's insert/update/delete data functions.
    • For Flutter version, you will have to change the standard calls to Moor's insert/update/delete methods in the client project into this framework ones.
    • For Xamarin version, you have to call some hook methods in the client project before persisting the data into the table.
  • This framework requires that all of your data in your database to have a unique primary key.
    • For Flutter version, your Moor table's primary key is expected to be a TextColumn type and should contain unique Uuid values (probably generated from the uuid package).
    • For Xamarin version, the type of your table's primary key is not enforced to a specific data type, but commonly it should use Guid values (and therefore the table's column type is usually a string) to ensure its uniqueness.
  • Your database design must use the Soft Delete approach when deleting record from tables, means that for any delete operation, the data is not physically deleted from the table, but only flag the record with a boolean column that indicates whether this record is already deleted or not.

Version Comparison

The Flutter version is actually newer than the Xamarin version (the Xamarin version was the initial framework when NETCoreSync was built). Flutter has become one of the hottest framework nowadays to easily build a beautiful, performant, single code-base application that works on ALL platforms (android, ios, web, windows, macos, linux), so naturally, NETCoreSync is adapted to work with Flutter. There are several advantages of the Flutter version:

  • The Flutter version has a feature called "linkedSyncId" where a single user account can be linked with several different user accounts to allow them to share data among themselves (they can modify each other data), and those changes can still be synchronized back to each user account devices.
  • The client-side component for Flutter version (which is built on top of Moor library) has much less integration work compared to the Xamarin version, so it requires only minimal modification on the client project. Of course this makes the client project depended on the Moor library, but by doing this, the integration is very minimal, and Moor itself is considered as one of the leading sqlite database framework for Flutter.
  • The server-side component for Flutter version has been rewritten using the latest .NET 5.0 (as per writing), and also rewritten to use WebSockets to allow efficient network communication, and implemented as an ASP .NET Core Middleware component to also minimize the integration work.

Moving forward, the Flutter version will be the primary development to have periodical updates and improvements, while the Xamarin version will remain as a backward-compatible solution only.

To read more about the Flutter version of NETCoreSync, visit the netcoresync_moor here.

To read more about the Xamarin version of NETCoreSync, visit the NETCoreSync here.

You might also like...

A collection of simple, bare-bones Flutter apps that each demonstrate a concept

flutter-by-example A collection of simple, bare-bones Flutter apps that each demonstrate a concept The apps are slowly being updated to Dart 2; be sur

Dec 10, 2022

Flutter Andriod app to track cases in each country for Corona Virus Covid-19

Flutter Andriod app to track cases in each country for Corona Virus Covid-19

corova_virus_app Flutter Andriod app to track cases in each country for Corona Virus Covid-19 app support historical data/dark mode/search by country/

Mar 5, 2022

Flutter Music Player - First Open Source Flutter based material design music player with audio plugin to play local music files.

Flutter Music Player - First Open Source Flutter based material design music player with audio plugin to play local music files.

Flutter Music Player First Open Source Flutter based Beautiful Material Design Music Player(Online Radio will be added soon.) Demo App Play Store BETA

Jan 8, 2023

๐ŸŽต Elegant music app to play local music & YouTube music. Distributes music into albums & artists. Has playlists & lyrics.

๐ŸŽต Elegant music app to play local music & YouTube music. Distributes music into albums & artists. Has playlists & lyrics.

Harmonoid Elegant music app to play local music & YouTube music. Download Now ๐Ÿ Feel free to report bugs & issues. We'll be there to fix. Loving the

Dec 30, 2022

Help your local business flourish!

Help your local business flourish!

Grow Green ๐Ÿ˜ƒ Hit that โญ button to show some โค๏ธ INSPIRATION โญ We were inspired, in part, by a statistic published by Mint, an Indian financial newspap

Dec 24, 2021

Super easy mood tracking app to demonstrate use of the Firebase Local Emulator Suite

Super easy mood tracking app to demonstrate use of the Firebase Local Emulator Suite

Mood Tracker Example App in Flutter This is a simple example app showing how to use Cloud Functions and the Firebase Local Emulator inside a Flutter a

Oct 14, 2022

using local db sqflite CRUD operations

todo A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is

Nov 11, 2021

Flutter Local Notification Test APP

Local Notification Test App For Testing and Learning Local Notification Information I made it by referring to the site below [flutter] local notificat

Nov 20, 2021

๐ŸŽต Elegant music app to play local music & YouTube music. Distributes music into albums & artists. Has playlists & lyrics. Windows + Linux + Android.

๐ŸŽต Elegant music app to play local music & YouTube music. Distributes music into albums & artists. Has playlists & lyrics. Windows + Linux + Android.

Harmonoid Elegant music app to play local music & YouTube music. Download Now ๐Ÿ Windows, Linux & Android. Feel free to report bugs & issues. Loving t

Aug 10, 2022
Comments
  • Object get pull then push again to server.

    Object get pull then push again to server.

    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 !

    opened by nyrull 15
  • Use of JWT token or other kind of token and HTTPS possible ?

    Use of JWT token or other kind of token and HTTPS possible ?

    Hi, it is me again ! haha

    I am looking at different approach to secure my web api and wanted to know if it could be possible to implement JWT Token or another kind of token in NetCoreSync.

    As https it should be possible right ?

    Thank you again.

    opened by nyrull 4
  • Bump Newtonsoft.Json from 13.0.1 to 13.0.2 in /NETCoreSync

    Bump Newtonsoft.Json from 13.0.1 to 13.0.2 in /NETCoreSync

    Bumps Newtonsoft.Json from 13.0.1 to 13.0.2.

    Release notes

    Sourced from Newtonsoft.Json's releases.

    13.0.2

    • New feature - Add support for DateOnly and TimeOnly
    • New feature - Add UnixDateTimeConverter.AllowPreEpoch property
    • New feature - Add copy constructor to JsonSerializerSettings
    • New feature - Add JsonCloneSettings with property to disable copying annotations
    • Change - Add nullable annotation to JToken.ToObject(Type, JsonSerializer)
    • Change - Reduced allocations by reusing boxed values
    • Fix - Fixed MaxDepth when used with ToObject inside of a JsonConverter
    • Fix - Fixed deserializing mismatched JToken types in properties
    • Fix - Fixed merging enumerable content and validate content
    • Fix - Fixed using $type with arrays of more than two dimensions
    • Fix - Fixed rare race condition in name table when deserializing on device with ARM processors
    • Fix - Fixed deserializing via constructor with ignored base type properties
    • Fix - Fixed MaxDepth not being used with ISerializable deserialization
    Commits

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    dependencies 
    opened by dependabot[bot] 0
Releases(v1.2.2)
  • v1.2.2(May 23, 2022)

  • flutter-v1.0.2(Aug 30, 2021)

    Flutter Version 1.0.2 Release Notes

    netcoresync_moor

    • Updated with latest netcoresync_moor_generator release.

    netcoresync_moor_generator

    • Downgrade analyzer from 2.0.0 to 1.7.0 because flutter still depends on meta 1.3.0.
    Source code(tar.gz)
    Source code(zip)
  • flutter-v1.0.1(Aug 28, 2021)

    Flutter Version 1.0.1 Release Notes

    netcoresync_moor

    • Provide example and shorten pubspec.yaml description to increase pub points.

    netcoresync_moor_generator

    • Fix generator to support multiple database classes code generation in a single project.
    • Provide example and update dependencies to increase pub points.
    • Fix wrong link description.

    NETCoreSyncServer

    • Fix wrong link description.
    Source code(tar.gz)
    Source code(zip)
  • flutter-v1.0.0(Aug 28, 2021)

  • v1.2.1(Sep 8, 2019)

  • v1.2.0(Aug 9, 2019)

    v1.2.0 Release Notes

    Added Features

    • Support conflict handling during DeserializeJsonToExistingData method
    • Support suppressing Exception during hook calls if updated with older time stamp (GlobalTimeStamp only)

    Breaking Changes

    • DeserializeJsonToExistingData method signature change: adds conflictType parameter
    Source code(tar.gz)
    Source code(zip)
Owner
Aldy J
Aldy J
An extension of flutter local notification, to simplify local notifications

Locally flutter local notification Locally helps developers create local notification with flutter on both Android and IOS platforms, it depends on th

Samuel Ezedi 20 Oct 10, 2022
A simple Flutter Note Taking app with local database.

Flutter Simple & Lightweight Note App Flutter Simple & Lightweight Note App UI/UX Credit: https://dribbble.com/shots/11875872-A-simple-and-lightweight

Ahmad Amin 63 Nov 10, 2022
Todo Flutter application with sqflite as a local database and bloc state management.

Todo App A Flutter application developed to add todo tasks and handles it I used Sqflite as a local database to store all the tasks I used flutter_sli

Muhhamed Sallam 14 Oct 17, 2022
๐Ÿš€ Full-Stack Flutter application, encoded using Dart. Utilizes native device features ๐Ÿ“ท and stores user data on a local SQFLite database. โœ”

great_places_app A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you started

Soumyadeep Das 2 Jan 24, 2022
โœˆ๏ธ A tidy utility to handle offline/online connectivity like a Boss

โœˆ๏ธ Flutter Offline A tidy utility to handle offline/online connectivity like a Boss. It provides support for both iOS and Android platforms (offcourse

Jeremiah Ogbomo 845 Jan 2, 2023
Fully functional Twitter clone built in flutter framework using Firebase realtime database and storage

Fwitter - Twitter clone in flutter A working Twitter clone written in Flutter using Firebase auth,realtime,firestore database and storage. Download Ap

Sonu Sharma 2.4k Jan 8, 2023
Dig is a hub blockchain that interconnects physical plots of land, which will each be governed by a locally operated blockchain.

Dig is a hub blockchain that interconnects physical plots of land, which will each be governed by a locally operated blockchain. Due to the regulatory challenges involved, dig splits itself up into many chains which can each follow appropriate legislation. This is the beginning of the "Dig Network."

notional-labs 183 Dec 17, 2022
Developed a Group chat application using Flutter and Firebase, where users can register and create groups or join already existing groups and start conversing with each other.

GroupChatApp About Developed a Group chat application using Flutter and Firebase, where users can register and create groups or join already existing

Ahmed Gulab Khan 221 Dec 1, 2022
An app to help students and teachers connect each other.

korek An app to help students and teachers connect each other. Technologies: Project is created with: React.JS (Typescript) Express (Typescript) Flutt

Bruno Dziฤ™cielski 3 Jan 10, 2022
The application helps the patient to follow up on medication schedules, and each patient has his own profile. The application is connected to Bluetooth to help the patient's dependents follow up on the patient.

spoon_medicines A new Flutter application. Getting Started This project is a starting point for a Flutter application. A few resources to get you star

null 0 Nov 27, 2021