# Database Sync Manager con CBDDC.Net - Analisi Tecnica ## πŸ“‹ Panoramica del Progetto Creazione di un **software generico di sincronizzazione database** che permette agli utenti di: 1. βœ… Connettersi a database esistenti (SQL Server, PostgreSQL, MySQL, SQLite) 2. βœ… Selezionare tabelle da sincronizzare tramite UI 3. βœ… Analizzare automaticamente schema e relazioni (FK, PK) 4. βœ… Generare mapping JSON ↔ EntitΓ  a runtime 5. βœ… Avviare sincronizzazione P2P senza scrivere codice --- ## πŸ—οΈ Architettura del Sistema ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ CBDDC Sync Manager (Desktop App) β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ 1. Database Connection Manager β”‚ β”‚ - Connection string builder UI β”‚ β”‚ - Test connection β”‚ β”‚ - Multi-provider support β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ 2. Schema Discovery Engine β”‚ β”‚ - INFORMATION_SCHEMA queries β”‚ β”‚ - EF Core metadata reader β”‚ β”‚ - FK/PK detection β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ 3. Table Selection UI β”‚ β”‚ - Checkbox list con preview β”‚ β”‚ - Filtri per system tables β”‚ β”‚ - Stima dimensioni dati β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ 4. Dynamic DocumentStore Generator β”‚ β”‚ - Runtime entity generation β”‚ β”‚ - JSON serialization mapping β”‚ β”‚ - Conflict resolution config β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ 5. Sync Configuration β”‚ β”‚ - Node ID, port, cluster key β”‚ β”‚ - Known peers (IP:Port) β”‚ β”‚ - Conflict resolver (LWW/Merge) β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ 6. Real-Time Sync Monitor β”‚ β”‚ - Connected peers status β”‚ β”‚ - Oplog entries display β”‚ β”‚ - Network traffic stats β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` --- ## πŸ”§ Componenti Tecnici Principali ### 1. Schema Discovery Engine **ResponsabilitΓ :** - Connessione a database esistenti - Lettura metadati da `INFORMATION_SCHEMA` - Rilevamento constraint (PK, FK, UNIQUE) - Identificazione relazioni tra tabelle **Implementazione:** ````csharp // filepath: tools/database-sync-manager/Services/DatabaseSchemaAnalyzer.cs using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; public class DatabaseSchemaAnalyzer { private readonly DbContext _context; public DatabaseSchemaAnalyzer(string connectionString, DatabaseProvider provider) { var optionsBuilder = new DbContextOptionsBuilder(); switch (provider) { case DatabaseProvider.SqlServer: optionsBuilder.UseSqlServer(connectionString); break; case DatabaseProvider.PostgreSQL: optionsBuilder.UseNpgsql(connectionString); break; case DatabaseProvider.MySQL: optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)); break; case DatabaseProvider.SQLite: optionsBuilder.UseSqlite(connectionString); break; } _context = new DbContext(optionsBuilder.Options); } public async Task> DiscoverTablesAsync() { var tables = new List(); // Query INFORMATION_SCHEMA o EF metadata var entityTypes = _context.Model.GetEntityTypes(); foreach (var entityType in entityTypes) { var table = new TableInfo { Name = entityType.GetTableName(), Schema = entityType.GetSchema() ?? "dbo", Columns = GetColumns(entityType), PrimaryKey = GetPrimaryKey(entityType), ForeignKeys = GetForeignKeys(entityType), RowCount = await GetRowCountAsync(entityType.GetTableName()) }; tables.Add(table); } return tables; } private List GetColumns(IEntityType entityType) { return entityType.GetProperties().Select(p => new ColumnInfo { Name = p.GetColumnName(), DataType = p.GetColumnType(), IsNullable = p.IsNullable, MaxLength = p.GetMaxLength(), ClrType = GetClrType(p) }).ToList(); } private PrimaryKeyInfo GetPrimaryKey(IEntityType entityType) { var pk = entityType.FindPrimaryKey(); return new PrimaryKeyInfo { ConstraintName = pk?.GetName(), Columns = pk?.Properties.Select(p => p.GetColumnName()).ToList() ?? new() }; } private List GetForeignKeys(IEntityType entityType) { return entityType.GetForeignKeys().Select(fk => new ForeignKeyInfo { ConstraintName = fk.GetConstraintName(), ReferencedTable = fk.PrincipalEntityType.GetTableName(), Columns = fk.Properties.Select(p => p.GetColumnName()).ToList(), ReferencedColumns = fk.PrincipalKey.Properties .Select(p => p.GetColumnName()).ToList() }).ToList(); } private async Task GetRowCountAsync(string tableName) { var sql = $"SELECT COUNT(*) FROM {tableName}"; return await _context.Database.ExecuteSqlRawAsync(sql); } private Type GetClrType(IProperty property) { var clrType = property.ClrType; return property.IsNullable && clrType.IsValueType ? typeof(Nullable<>).MakeGenericType(clrType) : clrType; } } ```` **Modelli Dati:** ````csharp // filepath: tools/database-sync-manager/Models/SchemaModels.cs public record TableInfo { public string Name { get; init; } public string Schema { get; init; } public List Columns { get; init; } public PrimaryKeyInfo PrimaryKey { get; init; } public List ForeignKeys { get; init; } public long RowCount { get; init; } public bool IsSelected { get; set; } } public record ColumnInfo { public string Name { get; init; } public string DataType { get; init; } public bool IsNullable { get; init; } public int? MaxLength { get; init; } public Type ClrType { get; init; } } public record PrimaryKeyInfo { public string ConstraintName { get; init; } public List Columns { get; init; } } public record ForeignKeyInfo { public string ConstraintName { get; init; } public string ReferencedTable { get; init; } public List Columns { get; init; } public List ReferencedColumns { get; init; } } public enum DatabaseProvider { SqlServer, PostgreSQL, MySQL, SQLite } ```` --- ### 2. Dynamic Entity Generator **ResponsabilitΓ :** - Generazione classi C# runtime da schema SQL - Creazione proprietΓ  tipizzate - Supporto nullable types - Mapping SQL types β†’ CLR types **Implementazione:** ````csharp // filepath: tools/database-sync-manager/Services/DynamicEntityGenerator.cs using System.Reflection; using System.Reflection.Emit; public class DynamicEntityGenerator { private readonly ModuleBuilder _moduleBuilder; private readonly Dictionary _generatedTypes = new(); public DynamicEntityGenerator() { var assemblyName = new AssemblyName("DynamicEntities"); var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly( assemblyName, AssemblyBuilderAccess.Run); _moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule"); } public Type GenerateEntity(TableInfo table) { // Cache per evitare rigenerazione if (_generatedTypes.TryGetValue(table.Name, out var cachedType)) return cachedType; var typeBuilder = _moduleBuilder.DefineType( $"Dynamic_{table.Name}", TypeAttributes.Public | TypeAttributes.Class); // Aggiungi proprietΓ  per ogni colonna foreach (var column in table.Columns) { CreateProperty(typeBuilder, column); } var generatedType = typeBuilder.CreateType(); _generatedTypes[table.Name] = generatedType; return generatedType; } private void CreateProperty(TypeBuilder typeBuilder, ColumnInfo column) { var propertyType = column.ClrType; var fieldBuilder = typeBuilder.DefineField( $"_{column.Name.ToLower()}", propertyType, FieldAttributes.Private); var propertyBuilder = typeBuilder.DefineProperty( column.Name, PropertyAttributes.HasDefault, propertyType, null); // Getter var getterBuilder = typeBuilder.DefineMethod( $"get_{column.Name}", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyType, Type.EmptyTypes); var getterIL = getterBuilder.GetILGenerator(); getterIL.Emit(OpCodes.Ldarg_0); getterIL.Emit(OpCodes.Ldfld, fieldBuilder); getterIL.Emit(OpCodes.Ret); // Setter var setterBuilder = typeBuilder.DefineMethod( $"set_{column.Name}", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, null, new[] { propertyType }); var setterIL = setterBuilder.GetILGenerator(); setterIL.Emit(OpCodes.Ldarg_0); setterIL.Emit(OpCodes.Ldarg_1); setterIL.Emit(OpCodes.Stfld, fieldBuilder); setterIL.Emit(OpCodes.Ret); propertyBuilder.SetGetMethod(getterBuilder); propertyBuilder.SetSetMethod(setterBuilder); } public Dictionary GetGeneratedTypes() => _generatedTypes; } ```` --- ### 3. Dynamic DocumentStore **ResponsabilitΓ :** - Implementazione `IDocumentStore` di CBDDC - Mapping runtime SQL β†’ JSON - CRUD operations su tabelle selezionate - Change tracking integration **Implementazione:** ````csharp // filepath: tools/database-sync-manager/Services/DynamicEfCoreDocumentStore.cs using ZB.MOM.WW.CBDDC.Core.Storage; using Microsoft.EntityFrameworkCore; using System.Text.Json; public class DynamicEfCoreDocumentStore : IDocumentStore { private readonly DbContext _dbContext; private readonly Dictionary _entityTypes; private readonly Dictionary _tableSchemas; public DynamicEfCoreDocumentStore( DbContext dbContext, Dictionary entityTypes, List selectedTables) { _dbContext = dbContext; _entityTypes = entityTypes; _tableSchemas = selectedTables.ToDictionary(t => t.Name); } public async Task GetAsync( string collection, string key, CancellationToken ct = default) { if (!_entityTypes.TryGetValue(collection, out var entityType)) return null; var schema = _tableSchemas[collection]; var pkColumn = schema.PrimaryKey.Columns.First(); // Ottieni DbSet dinamicamente var dbSet = GetDbSet(entityType); // Query dinamica con EF.Property var entity = await ((IQueryable)dbSet) .FirstOrDefaultAsync(e => EF.Property(e, pkColumn) == key, ct); if (entity == null) return null; // Serializza a JSON var json = JsonSerializer.Serialize(entity, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); return new Document { Collection = collection, Key = key, Content = JsonDocument.Parse(json).RootElement, Timestamp = HlcTimestamp.Now(_nodeId), IsDeleted = false }; } public async Task PutAsync(Document document, CancellationToken ct = default) { if (!_entityTypes.TryGetValue(document.Collection, out var entityType)) throw new InvalidOperationException($"Collection {document.Collection} not found"); var schema = _tableSchemas[document.Collection]; var pkColumn = schema.PrimaryKey.Columns.First(); // Deserializza JSON β†’ entity var entity = JsonSerializer.Deserialize( document.Content.GetRawText(), entityType); if (entity == null) throw new InvalidOperationException("Failed to deserialize document"); var dbSet = GetDbSet(entityType); var pkValue = entityType.GetProperty(pkColumn)?.GetValue(entity); // Verifica se esiste giΓ  var existing = await ((IQueryable)dbSet) .FirstOrDefaultAsync(e => EF.Property(e, pkColumn).Equals(pkValue), ct); if (existing != null) { // Update _dbContext.Entry(existing).CurrentValues.SetValues(entity); } else { // Insert ((dynamic)dbSet).Add(entity); } await _dbContext.SaveChangesAsync(ct); } public async Task DeleteAsync( string collection, string key, CancellationToken ct = default) { if (!_entityTypes.TryGetValue(collection, out var entityType)) return; var schema = _tableSchemas[collection]; var pkColumn = schema.PrimaryKey.Columns.First(); var dbSet = GetDbSet(entityType); var entity = await ((IQueryable)dbSet) .FirstOrDefaultAsync(e => EF.Property(e, pkColumn) == key, ct); if (entity != null) { _dbContext.Remove(entity); await _dbContext.SaveChangesAsync(ct); } } private object GetDbSet(Type entityType) { return _dbContext.GetType() .GetMethod("Set", Type.EmptyTypes) ?.MakeGenericMethod(entityType) ?.Invoke(_dbContext, null) ?? throw new InvalidOperationException($"Cannot get DbSet for {entityType.Name}"); } // Implementa altri metodi IDocumentStore... public Task> ListAsync( string collection, CancellationToken ct = default) => throw new NotImplementedException(); public Task ExistsAsync( string collection, string key, CancellationToken ct = default) => throw new NotImplementedException(); } ```` --- ### 4. Sync Configuration Manager **ResponsabilitΓ :** - Gestione configurazione nodi P2P - Salvataggio/caricamento config da file - Validazione impostazioni **Implementazione:** ````csharp // filepath: tools/database-sync-manager/Services/SyncConfigurationManager.cs using System.Text.Json; public class SyncConfigurationManager { private readonly string _configPath; public SyncConfigurationManager(string configPath = "sync-config.json") { _configPath = configPath; } public async Task LoadAsync() { if (!File.Exists(_configPath)) return new SyncConfiguration(); var json = await File.ReadAllTextAsync(_configPath); return JsonSerializer.Deserialize(json) ?? new SyncConfiguration(); } public async Task SaveAsync(SyncConfiguration config) { var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true }); await File.WriteAllTextAsync(_configPath, json); } public bool Validate(SyncConfiguration config, out List errors) { errors = new List(); if (string.IsNullOrWhiteSpace(config.NodeId)) errors.Add("Node ID is required"); if (config.TcpPort < 1024 || config.TcpPort > 65535) errors.Add("TCP port must be between 1024 and 65535"); if (string.IsNullOrWhiteSpace(config.ConnectionString)) errors.Add("Connection string is required"); if (!config.SelectedTables.Any()) errors.Add("At least one table must be selected"); return !errors.Any(); } } public record SyncConfiguration { public string NodeId { get; init; } = Environment.MachineName; public string ConnectionString { get; init; } = string.Empty; public DatabaseProvider Provider { get; init; } = DatabaseProvider.SqlServer; public List SelectedTables { get; init; } = new(); public int TcpPort { get; init; } = 5000; public int UdpPort { get; init; } = 5001; public string ClusterKey { get; init; } = "default-cluster"; public List KnownPeers { get; init; } = new(); public ConflictResolutionStrategy ConflictStrategy { get; init; } = ConflictResolutionStrategy.LastWriteWins; } public record PeerNode { public string NodeId { get; init; } public string Address { get; init; } // IP:Port } public enum ConflictResolutionStrategy { LastWriteWins, RecursiveMerge, Manual } ```` --- ### 5. Sync Engine Initialization **ResponsabilitΓ :** - Bootstrap CBDDC con configurazione runtime - Registrazione servizi DI - Avvio sync loop **Implementazione:** ````csharp // filepath: tools/database-sync-manager/Services/SyncEngineBootstrapper.cs using ZB.MOM.WW.CBDDC.Core; using CBDDC.Network; using Microsoft.Extensions.DependencyInjection; public class SyncEngineBootstrapper { public async Task InitializeAsync( SyncConfiguration config, Dictionary entityTypes, List selectedTables) { var services = new ServiceCollection(); // 1. Configura DbContext var dbContextOptions = CreateDbContextOptions(config); var dbContext = new DbContext(dbContextOptions); services.AddSingleton(dbContext); // 2. Crea DocumentStore dinamico var documentStore = new DynamicEfCoreDocumentStore( dbContext, entityTypes, selectedTables); services.AddSingleton(documentStore); // 3. Registra CBDDC Core services.AddCBDDCCore(opts => { opts.NodeId = config.NodeId; opts.ClusterKey = config.ClusterKey; }); // 4. Registra CBDDC Network services.AddCBDDCNetwork(opts => { opts.TcpPort = config.TcpPort; opts.UdpDiscoveryPort = config.UdpPort; opts.KnownPeers = config.KnownPeers .Select(p => new NetworkPeer { NodeId = p.NodeId, Address = p.Address }) .ToList(); }); // 5. Configura conflict resolver switch (config.ConflictStrategy) { case ConflictResolutionStrategy.LastWriteWins: services.AddSingleton(); break; case ConflictResolutionStrategy.RecursiveMerge: services.AddSingleton(); break; } var serviceProvider = services.BuildServiceProvider(); // 6. Avvia sync service var syncService = serviceProvider.GetRequiredService(); await syncService.StartAsync(CancellationToken.None); return serviceProvider; } private DbContextOptions CreateDbContextOptions(SyncConfiguration config) { var optionsBuilder = new DbContextOptionsBuilder(); switch (config.Provider) { case DatabaseProvider.SqlServer: optionsBuilder.UseSqlServer(config.ConnectionString); break; case DatabaseProvider.PostgreSQL: optionsBuilder.UseNpgsql(config.ConnectionString); break; case DatabaseProvider.MySQL: optionsBuilder.UseMySql(config.ConnectionString, ServerVersion.AutoDetect(config.ConnectionString)); break; case DatabaseProvider.SQLite: optionsBuilder.UseSqlite(config.ConnectionString); break; } return optionsBuilder.Options; } } ```` --- ## πŸ–₯️ User Interface (Avalonia UI) ### Main Window Layout ````xml