diff --git a/SSync.LiteDB.sln b/SSync.LiteDB.sln
index 861d275..2583659 100644
--- a/SSync.LiteDB.sln
+++ b/SSync.LiteDB.sln
@@ -22,6 +22,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{36F96630-08DF-4386-AADB-C8E121988D5B}"
ProjectSection(SolutionItems) = preProject
.github\workflows\publish.yaml = .github\workflows\publish.yaml
+ readme-ptBR.md = readme-ptBR.md
readme.md = readme.md
EndProjectSection
EndProject
diff --git a/doc/ssync_thumb.png b/doc/ssync_thumb.png
new file mode 100644
index 0000000..cebd96e
Binary files /dev/null and b/doc/ssync_thumb.png differ
diff --git a/readme.md b/readme.md
index ece9e7f..9e74d44 100644
--- a/readme.md
+++ b/readme.md
@@ -1,5 +1,23 @@
-# About and how works:
-SSYNC.LiteDB aims to assist in the data synchronization flow between the backend, using .NET WebAPI/Minimal APIs, and the frontend, using .NET MAUI or Uno Platform, with LiteDB as the local database.
+![alt text](doc/ssync_thumb.png "Img thumb ssynclitedb")
+
+[![en](https://img.shields.io/badge/lang-en-red.svg)](https://github.com/salesHgabriel/SSync.LiteDB/blob/master/readme.md)
+[![pt-br](https://img.shields.io/badge/lang-pt--br-green.svg)](https://github.com/salesHgabriel/SSync.LiteDB/blob/master/readme.pt-br.md)
+
+## About:
+SSYNC.LiteDB aims to simplify implementing data synchronization between the frontend using LiteDB and the backend.
+
+## ⚠️ Important Notes:
+- Your local and server databases must always use:
+ - - GUID for identifiers
+ - - Tables requiring data synchronization must include the columns: CreatedAt, UpdatedAt, and DeletedAt (timestamps).
+ - - The DeletedAt column is a nullable datetime, meaning you will always work with soft deletes.
+ - - The timestamp 01-01T00:00:00.0000000 (ISO) or 1/1/0001 12:00:00 AM is used as a reference to load all server data.
+ - - Data transactions must always use consistent data formats (UTC or local), both for server and client.
+ - - Data structure (schemas) must be consistent between server and client e keys names.
+
+
+## 🔄️ Flow
+![alt text](doc/notes_ssync_en.png "Img Flow ssynclitedb en-us")
## To update local changes:
@@ -10,529 +28,549 @@ SSYNC.LiteDB aims to assist in the data synchronization flow between the backend
## ![alt text](doc/flow_update_server_changes.jpg "Img Update server changes")
-## Flow (en-us):
+## Flow (en=us):
![alt text](doc/notes_ssync_en.png "Img Flow ssynclitedb en-us")
+
+
🔙 Backend
-## Flow pt-br:
-![alt text](doc/notes_ssync_pt_br.png "Img Flow ssynclitedb pt-br")
+### Installation
-⚠️⚠️ Important Notes:
+[![Nuget](https://img.shields.io/nuget/v/SSync.Server.LitebDB)](https://www.nuget.org/packages/SSync.Server.LitebDB/)
-* Your local and server databases must contain ID fields of type GUID, as well as fields for creation date, update date, and deletion date. To assist with this, you should use ISSyncEntityRoot in the backend, an abstract class to be used in your entities.
+### ⛏️ Configuração
-* Example your api server:
+1. Set Up Your Data Models:
+Your model can inherit from ISSyncEntityRoot to automatically create the necessary columns for synchronization management.
```cs
// example entity from server
- public class Note : ISSyncEntityRoot
+public class Note : ISSyncEntityRoot
+{
+ public const string CollectionName = "ss_tb_note";
+
+ public Note()
{
- public const string CollectionName = nameof(Note);
- public Note()
- {
-
- }
- public Note(Guid id, Time time) : base(id, time)
- {
- }
-
- public string? Content { get; set; }
- public bool Completed { get; set; }
+
}
-```
-
-
-* Case your client to use SchemaSync
-
-```cs
-// example client with .NET Maui
- public class Note : SchemaSync
+ public Note(Guid id, Time time) : base(id, time)
{
- public Note(Guid id) : base(id, SSync.Client.LitebDB.Enums.Time.UTC)
- {
- }
-
- public string? Content { get; set; }
-
- public bool Completed { get; set; }
}
-```
-
-* Your server database você will work only soft delete.
-* In your client, you must perform CRUD operations while always updating the date fields. To assist with this, Synchronize provides insert, update, and delete methods to abstract these operations. Using these methods is optional, the date updates are performed.
+
+ public bool Completed { get; set; }
+ public string? Message { get; set; }
+ public Guid? UserId{ get; set; }
+ public virtual User? User { get; set; }
-* Your client if not change your database litedb, you need set first pull to get all changes from server e set last pullet At
+}
+```
+2. Configure Data Transfer Object (DTO):
+Define a schema that represents the synchronized data object.
```cs
-// example repository client in .NET Maui.
+// example dto to shared data
- public class NoteRepository : INoteRepository
+public class NoteSync : ISchema
+{
+ public NoteSync(Guid id) : base(id)
{
- private Synchronize? _sync;
- private readonly LiteDatabase? _db;
-
- public NoteRepository()
- {
- _db = new LiteDatabase(GetPath());
- _sync = new Synchronize(_db);
- }
-
-
- public Task Save(Note note)
- {
- _sync!.InsertSync(note,"Note");
-
- return Task.CompletedTask;
- }
-
- public Task Update(Note note)
- {
- _sync!.UpdateSync(note, "Note");
-
- return Task.CompletedTask;
- }
+ }
- public Task Delete(Note note)
- {
- _sync!.DeleteSync(note, "Note");
- return Task.CompletedTask;
- }
+ public bool Completed { get; set; }
- .....
+ public string? Message { get; set; }
+ public string? UserName { get; set; }
+}
```
-* If you do not use Synchronize to delete fields in your local database, it is important to note that you must update the data (updating the date and status) instead of deleting it. You can use methods such as entity.CreatedAt() or entity.DeletedAt().
+3. Configure Your DbContext:
+The DbContext must inherit from ISSyncDbContextTransaction.
+```cs
+//sample dbcontext with postgresql
-## Client:
+public class PocDbContext : DbContext, ISSyncDbContextTransaction
+{
+ private IDbContextTransaction? _transaction;
+ private readonly IConfiguration _configuration;
-### How install
+ public PocDbContext(DbContextOptions options, IConfiguration configuration) : base(options)
+ {
+ //fix :https://github.com/npgsql/npgsql/issues/4246
+ AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
+ AppContext.SetSwitch("Npgsql.DisableDateTimeInfinityConversions", true);
+ _configuration = configuration;
+ }
-[![Nuget](https://img.shields.io/nuget/v/SSync.Client.LitebDB)](https://www.nuget.org/packages/SSync.Client.LitebDB/)
+ public DbSet Users { get; set; }
+ public DbSet Notes { get; set; }
- * setup
- * To use methods to sync initialize the classe as:
-```cs
-public class SyncRepo
-{
-private Synchronize? _sync;
-private readonly LiteDatabase? _db;
+ public async Task BeginTransactionSyncAsync()
+ => _transaction = await Database.BeginTransactionAsync();
-public SyncRepo()
-{
- _db = new LiteDatabase(GetPath());
- _sync = new Synchronize(_db);
-}
+ public async Task CommitSyncAsync()
+ => await Database.CommitTransactionAsync();
-private string GetPath()
-{
- var path = FileSystem.Current.AppDataDirectory;
+ public Task CommitTransactionSyncAsync()
+ {
+ ArgumentNullException.ThrowIfNull(_transaction);
-#if WINDOWS
+ return _transaction.CommitAsync();
+ }
- return Path.Combine(path, "litedbwin.db");
+ public Task RollbackTransactionSyncAsync()
+ {
+ ArgumentNullException.ThrowIfNull(_transaction);
+ return _transaction.RollbackAsync();
+ }
-#else
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ optionsBuilder.UseNpgsql(_configuration.GetConnectionString("PocServerSync"));
+ }
- return Path.Combine(path, "litedb.db");
-
-#endif
-}
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity()
+ .HasOne(n => n.User)
+ .WithMany(n => n.Notes)
+ .OnDelete(DeleteBehavior.Restrict);
-public Task SetLastPulledAt(DateTime lastPulledAt)
-{
- _sync!.ReplaceLastPulledAt(lastPulledAt);
- return Task.CompletedTask;
-}
-public DateTime GetLastPulledAt()
-{
- return _sync!.GetLastPulledAt();
+
+ modelBuilder.Entity()
+ .HasMany(n => n.Notes)
+ .WithOne(n => n.User)
+ .OnDelete(DeleteBehavior.Restrict);
+ }
}
+```
+4. Create a Pull Handler:
+This class facilitates downloading the synchronization structure and implements the ISSyncPullRequest interface.
-// --------------- PUSH -------------------
-
+```cs
+// ~/Sync.Handlers.Pull/NotePullRequestHandler.cs
-//Load database server to my local
-public Task PushServerChangesToLocal(string jsonServerChanges)
+public class NotePullRequestHandler : ISSyncPullRequest
{
- var pushBuilder = new SyncPushBuilder(jsonServerChanges);
+ private readonly ILogger _logger;
+ private readonly PocDbContext _pocDbContext;
- pushBuilder
- .AddPushSchemaSync(change => _sync!.PushChangesResult(change), LiteDbCollection.Note)
- .AddPushSchemaSync(change => _sync!.PushChangesResult(change), LiteDbCollection.AnotherNameCollection)
- .Build();
-
- return Task.CompletedTask;
-}
-
-// --------------- PULL -------------------
-
- //send database local to server
-public string PullLocalChangesToServer()
-{
- var pullChangesBuilder = new SyncPullBuilder();
+ public NotePullRequestHandler(ILogger logger, PocDbContext pocDbContext)
+ {
+ _logger = logger;
+ _pocDbContext = pocDbContext;
+ }
- var lastPulledAtSync = _sync!.GetLastPulledAt();
+ public async Task> QueryAsync(SSyncParameter parameter)
+ {
+ _logger.LogInformation("Not sync pull");
-//Get Change from database litebd
- pullChangesBuilder
- .AddPullSync(() => _sync!.PullChangesResult(lastPulledAtSync, LiteDbCollection.Note))
- .AddPullSync(() => _sync!.PullChangesResult(lastPulledAtSync, LiteDbCollection.AnotherNameCollection))
- .Build();
+ var notes = _pocDbContext.Notes.AsQueryable();
- var databaseLocal = pullChangesBuilder.DatabaseLocalChanges;
- var jsonDatabaseLocal = pullChangesBuilder.JsonDatabaseLocalChanges;
+ if (parameter.UserId.HasValue)
+ {
+ notes = notes.Where(x => x.UserId == parameter.UserId);
+ }
- return jsonDatabaseLocal;
+ return await notes.Select(n => new NoteSync(n.Id)
+ {
+ Completed = n.Completed,
+ CreatedAt = n.CreatedAt,
+ UpdatedAt = n.UpdatedAt,
+ Message = n.Message,
+ DeletedAt = n.DeletedAt,
+ UserName = n.User!.Name
+ }).ToListAsync();
+ }
}
```
-
-## Sample my service and view model with flur (nuget package to work with api requests)
+5. Setup Push Handlers
+Now, create your Push Handler class to assist with CRUD operations for synchronization data structures. It must implement the interface ISSyncPushRequest.
```cs
- public class ApiService : IApiService
+// ~/Sync.Handlers.Push/NotePushRequestHandler.cs
+
+ public class NotePushRequestHandler(PocDbContext context) : ISSyncPushRequest
{
- private readonly ISyncRepo _syncService;
+ private readonly PocDbContext _context = context;
- public ApiService(SyncRepo syncService)
+ public async Task FindByIdAsync(Guid id)
{
- _syncService = syncService;
+ return await _context.Notes.Where(u => u.Id == id)
+ .Select(u => new NoteSync(id)
+ {
+ Completed = u.Completed,
+ Message = u.Message,
+ UserName = u.User!.Name,
+ CreatedAt = u.CreatedAt,
+ DeletedAt = u.DeletedAt,
+ UpdatedAt = u.UpdatedAt
+ }).FirstOrDefaultAsync();
}
- public async Task PushServer()
+ public async Task CreateAsync(NoteSync schema)
{
+ var userId = await _context.Users
+ .Where(s => s.Name == schema.UserName)
+ .Select(u => u.Id)
+ .FirstOrDefaultAsync();
- //get local database
- var time = _syncService.GetLastPulledAt();
- var localDatabaseChanges = _syncService.PullLocalChangesToServer(time);
-
- //send local database to server
- var result = await "https://my-api.com"
- .AppendPathSegment("api/Sync/Push")
- .AppendQueryParam("Colletions", LiteDbCollection.Note)
- .AppendQueryParam("Colletions", LiteDbCollection.AnoterCollectionName)
- .AppendQueryParam("Timestamp", time)
- .WithHeader("Accept", "application/json")
- .WithHeader("Content-type", "application/json")
- .PostStringAsync(localDatabaseChanges);
-
- var resp = await result.ResponseMessage.Content.ReadAsStringAsync();
+ var newNote = new Note(schema.Id, Time.UTC)
+ {
+ Completed = schema.Completed,
+ Message = schema.Message,
+ UserId = userId
+ };
- //always need set lastPulletAt from response to your client litedb to know last sync with your server
+ await _context.Notes.AddAsync(newNote);
- var dta = JsonSerializer.Deserialize(resp);
- await _syncService.SetLastPulledAt(dta.Date);
-
- return result.StatusCode;
+ return await Save();
}
- public async Task PullServer(bool firstPull)
+ public async Task UpdateAsync(NoteSync schema)
{
- //if true, get all change of server
- // get server database
- var time = firstPull ? DateTime.MinValue : _syncService.GetLastPulledAt();
+ var entity = await _context.Notes.FindAsync(schema.Id);
- var result = await "https://my-api.com"
- .AppendPathSegment("api/Sync/Pull")
- .AppendQueryParam("Colletions", LiteDbCollection.Note)
- .AppendQueryParam("Colletions", LiteDbCollection.AnoterCollectionName)
- .AppendQueryParam("Timestamp", time.ToString("o")) // to convert to iso
- .GetAsync();
+ entity!.Completed = schema.Completed;
+ entity.Message = schema.Message;
- var res = await result.ResponseMessage.Content.ReadAsStringAsync();
+ entity.SetUpdatedAt(DateTime.UtcNow);
- //update local database from server
+ _context.Notes.Update(entity);
- await _syncService.PushServerChangesToLocal(res);
+ return await Save();
}
- }
-}
+ public async Task DeleteAsync(NoteSync schema)
+ {
+ var entity = await _context.Notes.FindAsync(schema.Id);
- // ViewModel
+ entity!.Completed = schema.Completed;
+ entity.Message = schema.Message;
- public class NoteViewModel : INotifyPropertyChanged
- {
+ entity.SetDeletedAt(DateTime.UtcNow);
- private readonly IApiService _apiService;
+ _context.Notes.Update(entity);
-
- private void PullChangesNow()
- {
- _apiService.PullServer(firstPull:false);
+ return await Save();
}
- private void PullAllChanges()
+ private async Task Save()
{
- _apiService.PullServer(firstPull:true);
+ return await _context.SaveChangesAsync() > 0;
}
+ }
+```
- private void PushChanges()
- {
- _apiService.PushServer();
- }
+6. Setup your program.cs
+
+
+```cs
+
+builder.Services.AddSSyncSchemaCollection(
+ optionsPullChanges:(pullChangesConfig) =>
+ {
+ pullChangesConfig
+ .By(User.CollectionName)
+ .ThenBy(Note.CollectionName);
+ },
+ optionsPushChanges: (pushChangesConfig) =>
+ {
+ pushChangesConfig
+ .By(User.CollectionName)
+ .ThenBy(Note.CollectionName);
+ });
```
-# Backend
+7. Now, you can use the ISchemaCollection interface to perform pull or push operations in your controller or endpoint.
+Here's the translated implementation for the backend:
-### How install
+```cs
+ // endpoint
+ var syncGroup = app.MapGroup("api/sync").WithOpenApi();
-[![Nuget](https://img.shields.io/nuget/v/SSync.Server.LitebDB)](https://www.nuget.org/packages/SSync.Server.LitebDB/)
+syncGroup.MapGet("/pull", async ([AsParameters] SSyncParameter parameter, [FromServices] ISchemaCollection schemaCollection) =>
+{
+ var changes = await schemaCollection.PullChangesAsync(parameter);
+
+ return Results.Ok(changes);
+});
+
+syncGroup.MapPost("/push", async (HttpContext httpContext, [FromBody] JsonArray changes, [FromServices] ISchemaCollection schemaCollection) =>
+{
+ var query = httpContext.Request.Query;
+
+ var sucesso = Guid.TryParse(query["userId"], out var userId);
+ var parameter = new SSyncParameter
+ {
+ UserId = sucesso ? userId : null,
+ Colletions = query["collections"].ToArray()!,
+ Timestamp = DateTime.TryParse(query["timestamp"], out DateTime timestamp) ? timestamp : DateTime.MinValue
+ };
+
+
+ var now = await schemaCollection.PushChangesAsync(changes, parameter);
+
+ return Results.Ok(now);
+});
+
+
+ // controller
+[Route("[action]")]
+[HttpGet]
+public async Task Pull([FromQuery] SSyncParameter parameter, [FromServices] ISchemaCollection schemaCollection)
+{
+ return Ok(await schemaCollection.PullChangesAsync(parameter));
+}
+[Route("[action]")]
+[HttpGet]
+public IAsyncEnumerable
+
+
+
📱 Client
+### Como instalar
+
+[![Nuget](https://img.shields.io/nuget/v/SSync.Client.LitebDB)](https://www.nuget.org/packages/SSync.Client.LitebDB/)
-// Create Class as like dto to you client
+### ⛏️ Configuração
- public class NoteSync : ISchema
+
+1. Your entities must inherit from the SchemaSync class:c
+
+```cs
+ public class Note : SchemaSync
{
- public NoteSync(Guid id) : base(id)
+ public Note(Guid id) : base(id, SSync.Client.LitebDB.Enums.Time.UTC)
{
}
public string? Content { get; set; }
- public bool Completed { get; set; }
+ public bool Completed { get; set; }
}
+```
+(Optional) 1.1. Table names must be unique and match your backend:
-builder.Services.AddSSyncSchemaCollection(
- (pullChangesConfig) =>
- {
- pullChangesConfig
- .By(Note.CollectionName);
- }, (pushChangesConfig) =>
+```cs
+ public static class LiteDbCollection
{
- pushChangesConfig
- .By(Note.CollectionName);
- });
-
+ public const string Note = "ss_tb_note";
+ }
+}
-//Sample DbContext
-// Set interface ISSyncDbContextTransaction
+```
+2. Your data operations (CRUD) must use the Synchronize class:
+The NoteRepository class handles CRUD operations for the Note entity, ensuring that these operations are synchronized.
- public class PocDbContext : DbContext, ISSyncDbContextTransaction
+```cs
+ public class NoteRepository : INoteRepository
{
- private readonly IConfiguration _configuration;
- private IDbContextTransaction? transaction;
+ private Synchronize? _sync;
+ private readonly LiteDatabase? _db;
- public PocDbContext(DbContextOptions dbContextOptions, IConfiguration configuration) : base(dbContextOptions)
+ public NoteRepository()
{
- _configuration = configuration;
+ _db = new LiteDatabase(GetPath());
+ _sync = new Synchronize(_db);
}
- public DbSet Notes { get; set; }
-
+ public List GetAll()
+ {
+ return _db!.GetCollection().FindAll().OrderBy(s => s.CreatedAt).ToList();
+ }
- public async Task BeginTransactionSyncAsync()
- => transaction = await Database.BeginTransactionAsync();
+ public Task Save(Note note)
+ {
+ _sync!.InsertSync(note,"Note");
- public async Task CommitSyncAsync()
- => await Database.CommitTransactionAsync();
+ return Task.CompletedTask;
+ }
- public Task CommitTransactionSyncAsync()
+ public Task Update(Note note)
{
- ArgumentNullException.ThrowIfNull(transaction);
+ _sync!.UpdateSync(note, "Note");
- return transaction.CommitAsync();
+ return Task.CompletedTask;
}
- public Task RollbackTransactionSyncAsync()
+ public Task Delete(Note note)
{
- ArgumentNullException.ThrowIfNull(transaction);
- return transaction.RollbackAsync();
+ _sync!.DeleteSync(note, "Note");
+ return Task.CompletedTask;
}
-// ------------ PULL -----------------------
- public class PullNotesRequestHandler : ISSyncPullRequest
- {
- private readonly PocDbContext _db;
-
- public PullNotesRequestHandler(PocDbContext db)
+ private string GetPath()
{
- _db = db;
- }
+ var path = FileSystem.Current.AppDataDirectory;
- public async Task> QueryAsync(SSyncParameter parameter)
- {
- var notes = await _db.Notes.Select(n => new NoteSync(n.Id)
- {
- Content = n.Content,
- Completed = n.Completed,
- CreatedAt = n.CreatedAt,
- DeletedAt = n.DeletedAt,
- UpdatedAt = n.UpdatedAt
- }).ToListAsync();
-
- return notes;
+#if WINDOWS
+ return Path.Combine(path, "litedbwin.db");
+#else
+ return Path.Combine(path, "litedb.db");
+#endif
}
- }
-}
-// ------------ PUSH -----------------------
+ }
- public class PusNotesRequestHandler : ISSyncPushRequest
- {
- private readonly PocDbContext _db;
+```
- public PusNotesRequestHandler(PocDbContext db)
- {
- _db = db;
- }
- public async Task FindByIdAsync(Guid id)
- {
- return await _db.Notes
- .Where(n => n.Id == id)
- .Select(n => new NoteSync(id)
- {
- Content = n.Content,
- Completed = n.Completed,
- CreatedAt = n.CreatedAt,
- UpdatedAt = n.UpdatedAt,
- DeletedAt = n.DeletedAt
- }).FirstOrDefaultAsync();
- }
+3. Create a synchronization repository class:
+The SyncRepository class is responsible for managing synchronization between the local and server databases.
- public async Task CreateAsync(NoteSync schema)
- {
- var newNote = new Note(schema.Id, Time.UTC)
- {
- Content = schema.Content,
- Completed = schema.Completed
- };
+```cs
- await _db.Notes.AddAsync(newNote);
- return await _db.SaveChangesAsync() > 0;
- }
- public async Task UpdateAsync(NoteSync schema)
+public class SyncRepository : ISyncRepository
+ {
+ //send database local to server
+ public string PullLocalChangesToServer(DateTime lastPulledAt)
{
- var note = await _db.Notes.FirstOrDefaultAsync(n => n.Id == schema.Id);
-
- if (note is null)
- return false;
-
- note.Content = schema.Content;
+ var pullChangesBuilder = new SyncPullBuilder();
- note.Completed = schema.Completed;
+ var last = _sync!.GetLastPulledAt();
+ pullChangesBuilder
+ .AddPullSync(() => _sync!.PullChangesResult(last, LiteDbCollection.Note))
+ // if more table to get
+ .AddPullSync(() => _sync!.PullChangesResult(last, LiteDbCollection.AnotherTable))
+ .Build();
- note.SetUpdatedAt(DateTime.UtcNow);
+ var databaseLocal = pullChangesBuilder.DatabaseLocalChanges;
+ var jsonDatabaseLocal = pullChangesBuilder.JsonDatabaseLocalChanges;
- _db.Notes.Update(note);
- return await _db.SaveChangesAsync() > 0;
+ return jsonDatabaseLocal;
}
- //Always works soft delete
- public async Task DeleteAsync(NoteSync schema)
+ //Load database server to my local
+ public Task PushServerChangesToLocal(string jsonServerChanges)
{
- var note = await _db.Notes.FirstOrDefaultAsync(n => n.Id == schema.Id);
+ var pushBuilder = new SyncPushBuilder(jsonServerChanges);
- if (note is null)
- return false;
+ pushBuilder
+ .AddPushSchemaSync(change => _sync!.PushChangesResult(change), LiteDbCollection.Note)
+ // if more table to send
+ .AddPullSync(() => _sync!.PullChangesResult(last, LiteDbCollection.AnotherTable))
+ .Build();
+ return Task.CompletedTask;
+ }
- note.SetDeletedAt(DateTime.UtcNow);
+ // save last date did changes
+ public Task SetLastPulledAt(DateTime lastPulledAt)
+ {
+ _sync!.ReplaceLastPulledAt(lastPulledAt);
+ return Task.CompletedTask;
+ }
- _db.Notes.Update(note);
- return await _db.SaveChangesAsync() > 0;
+ // get last date did changes
+ public DateTime GetLastPulledAt()
+ {
+ return _sync!.GetLastPulledAt();
}
+
}
+```
- // Your Controle ou Endpoint inject ISchemaCollection to use methods pull and push
+4. Implement your synchronization service:
+The ApiService class manages the synchronization logic, communicating with the server and updating the local database.
- // Controller WebApi
- [Route("[action]")]
- [HttpGet]
- public async Task Pull([FromQuery] SSyncParameter parameter, [FromServices] ISchemaCollection schemaCollection)
+```cs
+
+ public class ApiService : IApiService
+ {
+ private readonly SyncRepository _syncService;
+
+ public ApiService(SyncRepository syncService)
{
- return Ok(await schemaCollection.PullChangesAsync(parameter));
+ _syncService = syncService;
}
- [Route("[action]")]
- [HttpPost]
- public async Task Push([FromQuery] SSyncParameter parameter, [FromBody] JsonArray changes, [FromServices] ISchemaCollection schemaCollection)
+ public async Task PushServer()
{
- if (changes is not null)
- {
- return Ok(await schemaCollection.PushChangesAsync(changes, parameter));
- }
- return BadRequest();
+ //get local database
+ var time = _syncService.GetLastPulledAt();
+ var localDatabaseChanges = _syncService.PullLocalChangesToServer(time);
+
+ //send local database to server
+ var result = await "https://api-backend.com"
+ .AppendPathSegment("api/Sync/Push")
+ .AppendQueryParam("Colletions", LiteDbCollection.Note)
+ .AppendQueryParam("Timestamp", time)
+ .WithHeader("Accept", "application/json")
+ .WithHeader("Content-type", "application/json")
+ .PostStringAsync(localDatabaseChanges);
+
+ var resp = await result.ResponseMessage.Content.ReadAsStringAsync();
+
+ var dta = JsonSerializer.Deserialize(resp);
+ await _syncService.SetLastPulledAt(dta.Date);
+
+ return result.StatusCode;
}
-
- // Minimal api
-
- app.MapGet("/pull", async ([AsParameters] PlayParamenter parameter, [FromServices] ISchemaCollection schemaCollection) =>
- {
- var pullChangesRemoter = await schemaCollection.PullChangesAsync(parameter, new SSyncOptions()
+
+ public async Task PullServer(bool all)
{
- Mode = Mode.DEBUG
- });
+ // get server database
+ var time = all ? DateTime.MinValue : _syncService.GetLastPulledAt();
+ var result = await "https://api-backend.com"
+ .AppendPathSegment("api/Sync/Pull")
+ .AppendQueryParam("Colletions", LiteDbCollection.Note)
+ .AppendQueryParam("Timestamp", time.ToString("o"))
+ .GetAsync();
- return Results.Ok(pullChangesRemoter);
- });
+ var res = await result.ResponseMessage.Content.ReadAsStringAsync();
- app.MapPost("/push", async (HttpContext httpContext, JsonArray changes, [FromServices] ISchemaCollection schemaCollection) =>
- {
- var query = httpContext.Request.Query;
+ //update local database from server
- var parameter = new PlayParamenter
- {
- Time = Convert.ToInt32(query["time"]),
- Colletions = query["colletions"].ToArray()!,
- Timestamp = DateTime.TryParse(query["timestamp"], out DateTime timestamp) ? timestamp : DateTime.MinValue
- };
+ await _syncService.PushServerChangesToLocal(res);
+ }
- var isOk = await schemaCollection.PushChangesAsync(changes, parameter, new SSyncOptions()
- {
- Mode = Mode.DEBUG
- });
+ }
- return Results.Ok(isOk);
- });
-``
+```
+
diff --git a/readme.pt-br.md b/readme.pt-br.md
new file mode 100644
index 0000000..77c0946
--- /dev/null
+++ b/readme.pt-br.md
@@ -0,0 +1,566 @@
+![alt text](doc/ssync_thumb.png "Img thumb ssynclitedb")
+
+## Sobre:
+SSYNC.LiteDB tem como objetivo facilitar a implementação de sincronização de dados entre frontend com litedb e backend.
+
+## ⚠️ Notas importante
+- Sua base de dados local e do servidor sempre usará:
+ - - Guid com identificadores
+ - - As tabelas que deverão realizar sincronia de dados deverão possui as seguintes colunas CreatedAt, UpdatedAt e DeletedAt? (timestamp)
+ - - A coluna DeletedAt é datetime nulável, logo você sempre trabalhará com softdelete
+ - - O valor do timestamp 01-01T00:00:00.0000000 (iso) ou 1/1/0001 12:00:00 AM é usado como refência para carregar todos os dados do servidor
+ - - Em suas transações de dados, você deverá sempre com tipo formato de dados (utc ou local), tanto para server e client
+ - - A estrutura de dados (schemas) devem seguir a mesma para server e client
+
+
+## 🔄️ Fluxo
+![alt text](doc/notes_ssync_pt_br.png "Img Flow ssynclitedb pt-br")
+
+
+## To update local changes:
+
+![alt text](doc/flow_update_local_changes.png "Img Update local changes")
+
+## To update server changes:
+
+## ![alt text](doc/flow_update_server_changes.jpg "Img Update server changes")
+
+## Flow (pt-br):
+![alt text](doc/notes_ssync_pt_br.png "Img Flow ssynclitedb pt-br")
+
+
+
🔙 Backend
+
+### Como instalar
+
+
+[![Nuget](https://img.shields.io/nuget/v/SSync.Server.LitebDB)](https://www.nuget.org/packages/SSync.Server.LitebDB/)
+
+### ⛏️ Configuração
+
+1. Para configuração dos seu modelo de dados você pode herdar ISSyncEntityRoot, será criado as colunas necessárias para gerencia sincronia
+
+```cs
+// example entity from server
+
+public class Note : ISSyncEntityRoot
+{
+ public const string CollectionName = "ss_tb_note";
+
+ public Note()
+ {
+
+ }
+ public Note(Guid id, Time time) : base(id, time)
+ {
+ }
+
+
+ public bool Completed { get; set; }
+ public string? Message { get; set; }
+ public Guid? UserId{ get; set; }
+ public virtual User? User { get; set; }
+
+}
+```
+
+2. Configurar sua modelo de classe (schema) que irá representar objeto de dados da sincronia
+
+```cs
+// example dto to shared data
+
+public class NoteSync : ISchema
+{
+ public NoteSync(Guid id) : base(id)
+ {
+ }
+
+ public bool Completed { get; set; }
+
+ public string? Message { get; set; }
+
+ public string? UserName { get; set; }
+}
+```
+
+
+3. Configurar Dbcontext, ele deverá herdar ISSyncDbContextTransaction
+
+```cs
+
+//sample dbcontext with postgresql
+
+public class PocDbContext : DbContext, ISSyncDbContextTransaction
+{
+ private IDbContextTransaction? _transaction;
+ private readonly IConfiguration _configuration;
+
+ public PocDbContext(DbContextOptions options, IConfiguration configuration) : base(options)
+ {
+ //fix :https://github.com/npgsql/npgsql/issues/4246
+ AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
+ AppContext.SetSwitch("Npgsql.DisableDateTimeInfinityConversions", true);
+ _configuration = configuration;
+ }
+
+ public DbSet Users { get; set; }
+ public DbSet Notes { get; set; }
+
+
+ public async Task BeginTransactionSyncAsync()
+ => _transaction = await Database.BeginTransactionAsync();
+
+ public async Task CommitSyncAsync()
+ => await Database.CommitTransactionAsync();
+
+ public Task CommitTransactionSyncAsync()
+ {
+ ArgumentNullException.ThrowIfNull(_transaction);
+
+ return _transaction.CommitAsync();
+ }
+
+ public Task RollbackTransactionSyncAsync()
+ {
+ ArgumentNullException.ThrowIfNull(_transaction);
+ return _transaction.RollbackAsync();
+ }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ optionsBuilder.UseNpgsql(_configuration.GetConnectionString("PocServerSync"));
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity()
+ .HasOne(n => n.User)
+ .WithMany(n => n.Notes)
+ .OnDelete(DeleteBehavior.Restrict);
+
+
+
+ modelBuilder.Entity()
+ .HasMany(n => n.Notes)
+ .WithOne(n => n.User)
+ .OnDelete(DeleteBehavior.Restrict);
+ }
+}
+
+```
+
+4. Agora você deverá criar suas classe pull handler, ela irá auxiliar no download da estrutura de sicronia, ela deve Implementar da interface ISSyncPullRequest
+
+```cs
+// ~/Sync.Handlers.Pull/NotePullRequestHandler.cs
+
+public class NotePullRequestHandler : ISSyncPullRequest
+{
+ private readonly ILogger _logger;
+ private readonly PocDbContext _pocDbContext;
+
+ public NotePullRequestHandler(ILogger logger, PocDbContext pocDbContext)
+ {
+ _logger = logger;
+ _pocDbContext = pocDbContext;
+ }
+
+ public async Task> QueryAsync(SSyncParameter parameter)
+ {
+ _logger.LogInformation("Not sync pull");
+
+ var notes = _pocDbContext.Notes.AsQueryable();
+
+ if (parameter.UserId.HasValue)
+ {
+ notes = notes.Where(x => x.UserId == parameter.UserId);
+ }
+
+ return await notes.Select(n => new NoteSync(n.Id)
+ {
+ Completed = n.Completed,
+ CreatedAt = n.CreatedAt,
+ UpdatedAt = n.UpdatedAt,
+ Message = n.Message,
+ DeletedAt = n.DeletedAt,
+ UserName = n.User!.Name
+ }).ToListAsync();
+ }
+}
+
+```
+5. Agora você deverá criar suas classe push handler, ela irá auxiliar no crud de dados da estrutura de sicronia, ela deve Implementar da interface ISSyncPushRequest
+
+```cs
+// ~/Sync.Handlers.Push/NotePushRequestHandler.cs
+
+ public class NotePushRequestHandler(PocDbContext context) : ISSyncPushRequest
+ {
+ private readonly PocDbContext _context = context;
+
+ public async Task FindByIdAsync(Guid id)
+ {
+ return await _context.Notes.Where(u => u.Id == id)
+ .Select(u => new NoteSync(id)
+ {
+ Completed = u.Completed,
+ Message = u.Message,
+ UserName = u.User!.Name,
+ CreatedAt = u.CreatedAt,
+ DeletedAt = u.DeletedAt,
+ UpdatedAt = u.UpdatedAt
+ }).FirstOrDefaultAsync();
+ }
+
+ public async Task CreateAsync(NoteSync schema)
+ {
+ var userId = await _context.Users
+ .Where(s => s.Name == schema.UserName)
+ .Select(u => u.Id)
+ .FirstOrDefaultAsync();
+
+ var newNote = new Note(schema.Id, Time.UTC)
+ {
+ Completed = schema.Completed,
+ Message = schema.Message,
+ UserId = userId
+ };
+
+ await _context.Notes.AddAsync(newNote);
+
+ return await Save();
+ }
+
+ public async Task UpdateAsync(NoteSync schema)
+ {
+ var entity = await _context.Notes.FindAsync(schema.Id);
+
+ entity!.Completed = schema.Completed;
+ entity.Message = schema.Message;
+
+ entity.SetUpdatedAt(DateTime.UtcNow);
+
+ _context.Notes.Update(entity);
+
+ return await Save();
+ }
+
+ public async Task DeleteAsync(NoteSync schema)
+ {
+ var entity = await _context.Notes.FindAsync(schema.Id);
+
+ entity!.Completed = schema.Completed;
+ entity.Message = schema.Message;
+
+ entity.SetDeletedAt(DateTime.UtcNow);
+
+ _context.Notes.Update(entity);
+
+ return await Save();
+ }
+
+ private async Task Save()
+ {
+ return await _context.SaveChangesAsync() > 0;
+ }
+ }
+
+```
+
+6. Agora você deve configurar seu program.cs
+
+
+```cs
+
+builder.Services.AddSSyncSchemaCollection(
+ optionsPullChanges:(pullChangesConfig) =>
+ {
+ pullChangesConfig
+ .By(User.CollectionName)
+ .ThenBy(Note.CollectionName);
+ },
+ optionsPushChanges: (pushChangesConfig) =>
+ {
+ pushChangesConfig
+ .By(User.CollectionName)
+ .ThenBy(Note.CollectionName);
+ });
+
+```
+
+
+
+7. Agora você pode utilizar as interface ISchemaCollection para realizar pull ou push no seu controller ou endpoint
+
+```cs
+ // endpoint
+ var syncGroup = app.MapGroup("api/sync").WithOpenApi();
+
+syncGroup.MapGet("/pull", async ([AsParameters] SSyncParameter parameter, [FromServices] ISchemaCollection schemaCollection) =>
+{
+ var changes = await schemaCollection.PullChangesAsync(parameter);
+
+ return Results.Ok(changes);
+});
+
+syncGroup.MapPost("/push", async (HttpContext httpContext, [FromBody] JsonArray changes, [FromServices] ISchemaCollection schemaCollection) =>
+{
+ var query = httpContext.Request.Query;
+
+ var sucesso = Guid.TryParse(query["userId"], out var userId);
+ var parameter = new SSyncParameter
+ {
+ UserId = sucesso ? userId : null,
+ Colletions = query["collections"].ToArray()!,
+ Timestamp = DateTime.TryParse(query["timestamp"], out DateTime timestamp) ? timestamp : DateTime.MinValue
+ };
+
+
+ var now = await schemaCollection.PushChangesAsync(changes, parameter);
+
+ return Results.Ok(now);
+});
+
+
+ // controller
+[Route("[action]")]
+[HttpGet]
+public async Task Pull([FromQuery] SSyncParameter parameter, [FromServices] ISchemaCollection schemaCollection)
+{
+ return Ok(await schemaCollection.PullChangesAsync(parameter));
+}
+
+[Route("[action]")]
+[HttpGet]
+public IAsyncEnumerable
+
+
+
📱 Client
+
+### Como instalar
+
+[![Nuget](https://img.shields.io/nuget/v/SSync.Client.LitebDB)](https://www.nuget.org/packages/SSync.Client.LitebDB/)
+
+### ⛏️ Configuração
+
+
+
+1. Suas entidades devem herdar da classe SchemaSync
+
+```cs
+ public class Note : SchemaSync
+ {
+ public Note(Guid id) : base(id, SSync.Client.LitebDB.Enums.Time.UTC)
+ {
+ }
+
+ public string? Content { get; set; }
+
+ public bool Completed { get; set; }
+ }
+```
+
+(Opcional) 1.1. Nome das tabelas devem ser unicos e igual do seu backend
+
+```cs
+ public static class LiteDbCollection
+ {
+ public const string Note = "ss_tb_note";
+ }
+}
+
+```
+2. Suas operações de dados (crud), devem ser aplicadas usando a classe Synchronize
+
+```cs
+ public class NoteRepository : INoteRepository
+ {
+ private Synchronize? _sync;
+ private readonly LiteDatabase? _db;
+
+ public NoteRepository()
+ {
+ _db = new LiteDatabase(GetPath());
+ _sync = new Synchronize(_db);
+ }
+
+ public List GetAll()
+ {
+ return _db!.GetCollection().FindAll().OrderBy(s => s.CreatedAt).ToList();
+ }
+
+ public Task Save(Note note)
+ {
+ _sync!.InsertSync(note,"Note");
+
+ return Task.CompletedTask;
+ }
+
+ public Task Update(Note note)
+ {
+ _sync!.UpdateSync(note, "Note");
+
+ return Task.CompletedTask;
+ }
+
+ public Task Delete(Note note)
+ {
+ _sync!.DeleteSync(note, "Note");
+ return Task.CompletedTask;
+ }
+
+
+ private string GetPath()
+ {
+ var path = FileSystem.Current.AppDataDirectory;
+
+#if WINDOWS
+ return Path.Combine(path, "litedbwin.db");
+#else
+ return Path.Combine(path, "litedb.db");
+#endif
+ }
+
+
+ }
+
+```
+
+
+3. Agora crie sua classe repository de sincronia, ela possuirá responsabilidade de carregar dados de sua base de dados local ou enviá-las
+
+```cs
+
+
+public class SyncRepository : ISyncRepository
+ {
+ //send database local to server
+ public string PullLocalChangesToServer(DateTime lastPulledAt)
+ {
+ var pullChangesBuilder = new SyncPullBuilder();
+
+ var last = _sync!.GetLastPulledAt();
+ pullChangesBuilder
+ .AddPullSync(() => _sync!.PullChangesResult(last, LiteDbCollection.Note))
+ // if more table to get
+ .AddPullSync(() => _sync!.PullChangesResult(last, LiteDbCollection.AnotherTable))
+ .Build();
+
+ var databaseLocal = pullChangesBuilder.DatabaseLocalChanges;
+ var jsonDatabaseLocal = pullChangesBuilder.JsonDatabaseLocalChanges;
+
+ return jsonDatabaseLocal;
+ }
+
+ //Load database server to my local
+ public Task PushServerChangesToLocal(string jsonServerChanges)
+ {
+ var pushBuilder = new SyncPushBuilder(jsonServerChanges);
+
+ pushBuilder
+ .AddPushSchemaSync(change => _sync!.PushChangesResult(change), LiteDbCollection.Note)
+ // if more table to send
+ .AddPullSync(() => _sync!.PullChangesResult(last, LiteDbCollection.AnotherTable))
+ .Build();
+
+ return Task.CompletedTask;
+ }
+
+ // save last date did changes
+ public Task SetLastPulledAt(DateTime lastPulledAt)
+ {
+ _sync!.ReplaceLastPulledAt(lastPulledAt);
+ return Task.CompletedTask;
+ }
+
+ // get last date did changes
+ public DateTime GetLastPulledAt()
+ {
+ return _sync!.GetLastPulledAt();
+ }
+
+ }
+
+```
+
+4. Agora implemetação do seu service de sincronia
+
+
+
+```cs
+
+ public class ApiService : IApiService
+ {
+ private readonly SyncRepository _syncService;
+
+ public ApiService(SyncRepository syncService)
+ {
+ _syncService = syncService;
+ }
+
+ public async Task PushServer()
+ {
+ //get local database
+ var time = _syncService.GetLastPulledAt();
+ var localDatabaseChanges = _syncService.PullLocalChangesToServer(time);
+
+ //send local database to server
+ var result = await "https://api-backend.com"
+ .AppendPathSegment("api/Sync/Push")
+ .AppendQueryParam("Colletions", LiteDbCollection.Note)
+ .AppendQueryParam("Timestamp", time)
+ .WithHeader("Accept", "application/json")
+ .WithHeader("Content-type", "application/json")
+ .PostStringAsync(localDatabaseChanges);
+
+ var resp = await result.ResponseMessage.Content.ReadAsStringAsync();
+
+ var dta = JsonSerializer.Deserialize(resp);
+ await _syncService.SetLastPulledAt(dta.Date);
+
+ return result.StatusCode;
+ }
+
+ public async Task PullServer(bool all)
+ {
+ // get server database
+ var time = all ? DateTime.MinValue : _syncService.GetLastPulledAt();
+ var result = await "https://api-backend.com"
+ .AppendPathSegment("api/Sync/Pull")
+ .AppendQueryParam("Colletions", LiteDbCollection.Note)
+ .AppendQueryParam("Timestamp", time.ToString("o"))
+ .GetAsync();
+
+ var res = await result.ResponseMessage.Content.ReadAsStringAsync();
+
+ //update local database from server
+
+ await _syncService.PushServerChangesToLocal(res);
+ }
+
+ }
+
+
+```
+
+
+
+
+