feat: Implementato sistema di associazioni chiave per prevenire duplicati nel data coupling

BREAKING CHANGE: Rimosso completamente il vecchio sistema RecordAssociation

Modifiche principali:
- Sostituito RecordAssociation con KeyAssociation basato sui valori delle chiavi
- Implementata logica robusta di UPDATE vs INSERT basata su associazioni esistenti
- Aggiunta normalizzazione delle chiavi (.Trim()) per consistenza
- Implementato fallback nella ricerca associazioni per maggiore affidabilità
- Sostituita verifica pre-UPDATE con tentativo diretto più efficiente

Componenti modificati:
- Nuovo modello: KeyAssociation.cs con campi ottimizzati
- Nuovo servizio: KeyAssociationService.cs con metodi completi
- Aggiornato: DataCoupler.razor con logica migliorata di gestione associazioni
- Aggiornato: CredentialDbContext per gestire solo KeyAssociations
- Aggiornati: tutti i servizi di interfaccia per supportare il nuovo sistema
- Creata: pagina KeyAssociations.razor per gestione associazioni
- Aggiornato: NavMenu.razor con link alla gestione associazioni

Miglioramenti tecnici:
- Logica di UPDATE più robusta: tenta direttamente l'aggiornamento invece di verificare prima l'esistenza
- Gestione errori migliorata con cleanup automatico delle associazioni non valide
- Debug logging estensivo per troubleshooting
- Fallback nella ricerca associazioni se parametri specifici falliscono
- Normalizzazione valori chiave per prevenire problemi di whitespace

Risultato:
Il sistema ora previene correttamente i duplicati utilizzando le associazioni per decidere
se fare INSERT (nuovo record) o UPDATE (record esistente) basandosi sui valori delle chiavi.

Database:
- Creata migrazione EF per rimuovere RecordAssociations e aggiungere KeyAssociations
- Eliminati file e codice legacy non più necessari
This commit is contained in:
2025-06-29 20:44:20 +02:00
parent 2238ddc4bf
commit 04f0403f12
23 changed files with 2051 additions and 1161 deletions
@@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CredentialManager.Migrations
{
/// <inheritdoc />
public partial class AddRestServiceType : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "RestServiceType",
table: "Credentials",
type: "TEXT",
maxLength: 50,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "RestServiceType",
table: "Credentials");
}
}
}
@@ -1,73 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace CredentialManager.Migrations
{
/// <summary>
/// Aggiunge la tabella RecordAssociations per tracciare le associazioni tra record sorgente e destinazione
/// </summary>
public partial class AddRecordAssociations : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "RecordAssociations",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
SourceName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
SourceType = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
SourceKey = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false),
DestinationEntity = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
DestinationId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
RestCredentialName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')"),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
IsActive = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: true),
AdditionalInfo = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_RecordAssociations", x => x.Id);
});
// Indici per migliorare le performance
migrationBuilder.CreateIndex(
name: "IX_RecordAssociations_Unique",
table: "RecordAssociations",
columns: new[] { "SourceName", "SourceKey", "DestinationEntity" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_RecordAssociations_SourceType",
table: "RecordAssociations",
column: "SourceType");
migrationBuilder.CreateIndex(
name: "IX_RecordAssociations_DestinationEntity",
table: "RecordAssociations",
column: "DestinationEntity");
migrationBuilder.CreateIndex(
name: "IX_RecordAssociations_RestCredentialName",
table: "RecordAssociations",
column: "RestCredentialName");
migrationBuilder.CreateIndex(
name: "IX_RecordAssociations_IsActive",
table: "RecordAssociations",
column: "IsActive");
migrationBuilder.CreateIndex(
name: "IX_RecordAssociations_CreatedAt",
table: "RecordAssociations",
column: "CreatedAt");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "RecordAssociations");
}
}
}
@@ -0,0 +1,211 @@
// <auto-generated />
using System;
using CredentialManager.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CredentialManager.Migrations
{
[DbContext(typeof(CredentialDbContext))]
[Migration("20250629181214_ReplaceRecordAssociationsWithKeyAssociations")]
partial class ReplaceRecordAssociationsWithKeyAssociations
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
modelBuilder.Entity("CredentialManager.Models.CredentialEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AdditionalParameters")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<int>("CommandTimeout")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(30);
b.Property<string>("ConnectionString")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("CreatedBy")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("DatabaseName")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("DatabaseType")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("EncryptedApiKey")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("EncryptedAuthToken")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("EncryptedPassword")
.HasColumnType("TEXT");
b.Property<string>("Headers")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<string>("Host")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<bool>("IgnoreSslErrors")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false);
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true);
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int?>("Port")
.HasColumnType("INTEGER");
b.Property<string>("RestServiceType")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<int>("TimeoutSeconds")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(100);
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Username")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DatabaseType");
b.HasIndex("IsActive");
b.HasIndex("Name")
.IsUnique();
b.HasIndex("Type");
b.ToTable("Credentials", (string)null);
});
modelBuilder.Entity("CredentialManager.Models.KeyAssociation", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AdditionalInfo")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("DestinationEntity")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("DestinationId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("DestinationKeyField")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true);
b.Property<string>("KeyValue")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<DateTime?>("LastVerifiedAt")
.HasColumnType("TEXT");
b.Property<string>("RestCredentialName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("SourceKeyField")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("SourcesInfo")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.HasIndex("DestinationEntity");
b.HasIndex("IsActive");
b.HasIndex("KeyValue")
.HasDatabaseName("IX_KeyAssociations_KeyValue");
b.HasIndex("LastVerifiedAt");
b.HasIndex("RestCredentialName");
b.HasIndex("KeyValue", "DestinationEntity", "RestCredentialName")
.IsUnique()
.HasDatabaseName("IX_KeyAssociations_Unique");
b.ToTable("KeyAssociations", (string)null);
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,139 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CredentialManager.Migrations
{
/// <inheritdoc />
public partial class ReplaceRecordAssociationsWithKeyAssociations : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Credentials",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
Type = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
DatabaseType = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
ConnectionString = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
Host = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
Port = table.Column<int>(type: "INTEGER", nullable: true),
DatabaseName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
Username = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
EncryptedPassword = table.Column<string>(type: "TEXT", nullable: true),
EncryptedApiKey = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
EncryptedAuthToken = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
CommandTimeout = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 30),
TimeoutSeconds = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 100),
IgnoreSslErrors = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
RestServiceType = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
Headers = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
AdditionalParameters = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
CreatedBy = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
IsActive = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Credentials", x => x.Id);
});
migrationBuilder.CreateTable(
name: "KeyAssociations",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
KeyValue = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false),
SourceKeyField = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
DestinationKeyField = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
DestinationEntity = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
DestinationId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
RestCredentialName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
LastVerifiedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
IsActive = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: true),
SourcesInfo = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
AdditionalInfo = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_KeyAssociations", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Credentials_DatabaseType",
table: "Credentials",
column: "DatabaseType");
migrationBuilder.CreateIndex(
name: "IX_Credentials_IsActive",
table: "Credentials",
column: "IsActive");
migrationBuilder.CreateIndex(
name: "IX_Credentials_Name",
table: "Credentials",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Credentials_Type",
table: "Credentials",
column: "Type");
migrationBuilder.CreateIndex(
name: "IX_KeyAssociations_CreatedAt",
table: "KeyAssociations",
column: "CreatedAt");
migrationBuilder.CreateIndex(
name: "IX_KeyAssociations_DestinationEntity",
table: "KeyAssociations",
column: "DestinationEntity");
migrationBuilder.CreateIndex(
name: "IX_KeyAssociations_IsActive",
table: "KeyAssociations",
column: "IsActive");
migrationBuilder.CreateIndex(
name: "IX_KeyAssociations_KeyValue",
table: "KeyAssociations",
column: "KeyValue");
migrationBuilder.CreateIndex(
name: "IX_KeyAssociations_LastVerifiedAt",
table: "KeyAssociations",
column: "LastVerifiedAt");
migrationBuilder.CreateIndex(
name: "IX_KeyAssociations_RestCredentialName",
table: "KeyAssociations",
column: "RestCredentialName");
migrationBuilder.CreateIndex(
name: "IX_KeyAssociations_Unique",
table: "KeyAssociations",
columns: new[] { "KeyValue", "DestinationEntity", "RestCredentialName" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Credentials");
migrationBuilder.DropTable(
name: "KeyAssociations");
}
}
}
@@ -0,0 +1,208 @@
// <auto-generated />
using System;
using CredentialManager.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CredentialManager.Migrations
{
[DbContext(typeof(CredentialDbContext))]
partial class CredentialDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
modelBuilder.Entity("CredentialManager.Models.CredentialEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AdditionalParameters")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<int>("CommandTimeout")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(30);
b.Property<string>("ConnectionString")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("CreatedBy")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("DatabaseName")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("DatabaseType")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("EncryptedApiKey")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("EncryptedAuthToken")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("EncryptedPassword")
.HasColumnType("TEXT");
b.Property<string>("Headers")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<string>("Host")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<bool>("IgnoreSslErrors")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false);
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true);
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int?>("Port")
.HasColumnType("INTEGER");
b.Property<string>("RestServiceType")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<int>("TimeoutSeconds")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(100);
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Username")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DatabaseType");
b.HasIndex("IsActive");
b.HasIndex("Name")
.IsUnique();
b.HasIndex("Type");
b.ToTable("Credentials", (string)null);
});
modelBuilder.Entity("CredentialManager.Models.KeyAssociation", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AdditionalInfo")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("DestinationEntity")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("DestinationId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("DestinationKeyField")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true);
b.Property<string>("KeyValue")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<DateTime?>("LastVerifiedAt")
.HasColumnType("TEXT");
b.Property<string>("RestCredentialName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("SourceKeyField")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("SourcesInfo")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.HasIndex("DestinationEntity");
b.HasIndex("IsActive");
b.HasIndex("KeyValue")
.HasDatabaseName("IX_KeyAssociations_KeyValue");
b.HasIndex("LastVerifiedAt");
b.HasIndex("RestCredentialName");
b.HasIndex("KeyValue", "DestinationEntity", "RestCredentialName")
.IsUnique()
.HasDatabaseName("IX_KeyAssociations_Unique");
b.ToTable("KeyAssociations", (string)null);
});
#pragma warning restore 612, 618
}
}
}