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
+21 -15
View File
@@ -9,7 +9,7 @@ namespace CredentialManager.Data;
public class CredentialDbContext : DbContext
{
public DbSet<CredentialEntity> Credentials { get; set; }
public DbSet<RecordAssociation> RecordAssociations { get; set; }
public DbSet<KeyAssociation> KeyAssociations { get; set; }
public CredentialDbContext(DbContextOptions<CredentialDbContext> options) : base(options)
{
@@ -86,24 +86,24 @@ public class CredentialDbContext : DbContext
entity.HasIndex(e => e.IsActive);
});
// Configurazione della tabella RecordAssociations
modelBuilder.Entity<RecordAssociation>(entity =>
// Configurazione della tabella KeyAssociations
modelBuilder.Entity<KeyAssociation>(entity =>
{
entity.ToTable("RecordAssociations");
entity.ToTable("KeyAssociations");
entity.HasKey(e => e.Id);
entity.Property(e => e.SourceName)
entity.Property(e => e.KeyValue)
.IsRequired()
.HasMaxLength(500);
entity.Property(e => e.SourceKeyField)
.IsRequired()
.HasMaxLength(200);
entity.Property(e => e.SourceType)
entity.Property(e => e.DestinationKeyField)
.IsRequired()
.HasMaxLength(50);
entity.Property(e => e.SourceKey)
.IsRequired()
.HasMaxLength(500);
.HasMaxLength(200);
entity.Property(e => e.DestinationEntity)
.IsRequired()
@@ -117,6 +117,9 @@ public class CredentialDbContext : DbContext
.IsRequired()
.HasMaxLength(100);
entity.Property(e => e.SourcesInfo)
.HasMaxLength(2000);
entity.Property(e => e.AdditionalInfo)
.HasMaxLength(2000);
@@ -125,15 +128,18 @@ public class CredentialDbContext : DbContext
.HasDefaultValue(true);
// Indici
entity.HasIndex(e => new { e.SourceName, e.SourceKey, e.DestinationEntity })
.IsUnique()
.HasDatabaseName("IX_RecordAssociations_Unique");
entity.HasIndex(e => e.KeyValue)
.HasDatabaseName("IX_KeyAssociations_KeyValue");
entity.HasIndex(e => new { e.KeyValue, e.DestinationEntity, e.RestCredentialName })
.IsUnique()
.HasDatabaseName("IX_KeyAssociations_Unique");
entity.HasIndex(e => e.SourceType);
entity.HasIndex(e => e.DestinationEntity);
entity.HasIndex(e => e.RestCredentialName);
entity.HasIndex(e => e.IsActive);
entity.HasIndex(e => e.CreatedAt);
entity.HasIndex(e => e.LastVerifiedAt);
});
}
}
@@ -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
}
}
}
@@ -3,33 +3,38 @@ using System.ComponentModel.DataAnnotations;
namespace CredentialManager.Models;
/// <summary>
/// Entità per memorizzare le associazioni tra record sorgente e destinazione
/// Entità per memorizzare le associazioni basate sui valori delle chiavi
/// Un'associazione lega un valore di chiave a un record di destinazione,
/// indipendentemente dalla sorgente che ha generato quel valore
/// </summary>
public class RecordAssociation
public class KeyAssociation
{
[Key]
public int Id { get; set; }
/// <summary>
/// Nome della sorgente dati (nome tabella/file/foglio)
/// </summary>
[Required]
[MaxLength(200)]
public string SourceName { get; set; } = string.Empty;
/// <summary>
/// Tipo di sorgente (database, file)
/// </summary>
[Required]
[MaxLength(50)]
public string SourceType { get; set; } = string.Empty;
/// <summary>
/// Chiave del record sorgente (può essere un ID o una combinazione di campi)
/// Valore della chiave che identifica univocamente l'oggetto business
/// (es: "CUST001", "12345", "ABC-DEF-GHI")
/// </summary>
[Required]
[MaxLength(500)]
public string SourceKey { get; set; } = string.Empty;
public string KeyValue { get; set; } = string.Empty;
/// <summary>
/// Nome del campo chiave nella sorgente
/// (es: "CustomerCode", "ID", "ArticleNumber")
/// </summary>
[Required]
[MaxLength(200)]
public string SourceKeyField { get; set; } = string.Empty;
/// <summary>
/// Nome del campo chiave nella destinazione
/// (es: "CardCode", "DocEntry", "ItemCode")
/// </summary>
[Required]
[MaxLength(200)]
public string DestinationKeyField { get; set; } = string.Empty;
/// <summary>
/// Nome dell'entità di destinazione
@@ -46,7 +51,7 @@ public class RecordAssociation
public string DestinationId { get; set; } = string.Empty;
/// <summary>
/// Nome della credenziale REST utilizzata
/// Nome della credenziale REST utilizzata per la destinazione
/// </summary>
[Required]
[MaxLength(100)]
@@ -62,11 +67,22 @@ public class RecordAssociation
/// </summary>
public DateTime? UpdatedAt { get; set; }
/// <summary>
/// Data e ora dell'ultima verifica che il record di destinazione esiste ancora
/// </summary>
public DateTime? LastVerifiedAt { get; set; }
/// <summary>
/// Indica se l'associazione è ancora attiva
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// Informazioni aggiuntive sui record che hanno contribuito a questa associazione
/// </summary>
[MaxLength(2000)]
public string? SourcesInfo { get; set; }
/// <summary>
/// Informazioni aggiuntive in formato JSON
/// </summary>
@@ -68,16 +68,16 @@ public class DatabaseInitializer : IDatabaseInitializer
await _context.Credentials.CountAsync();
_logger.LogInformation("Tabella Credentials verificata con successo");
// Verifica se la tabella RecordAssociations esiste, se non esiste la crea senza ricreare tutto il database
// Verifica se la tabella KeyAssociations esiste, se non esiste la crea senza ricreare tutto il database
try
{
await _context.RecordAssociations.CountAsync();
_logger.LogInformation("Tabella RecordAssociations verificata con successo");
await _context.KeyAssociations.CountAsync();
_logger.LogInformation("Tabella KeyAssociations verificata con successo");
}
catch (Exception)
{
_logger.LogInformation("Tabella RecordAssociations non trovata, creazione tramite migrazione...");
await CreateRecordAssociationsTableAsync();
_logger.LogInformation("Tabella KeyAssociations non trovata, creazione tramite migrazione...");
await CreateKeyAssociationsTableAsync();
}
}
catch (Exception)
@@ -170,60 +170,78 @@ public class DatabaseInitializer : IDatabaseInitializer
_logger.LogInformation("Colonna RestServiceType aggiunta con successo");
}
// Migrazione 2: Verifica se la tabella RecordAssociations esiste
// Migrazione 2: Elimina vecchia tabella RecordAssociations se esiste e crea KeyAssociations
try
{
// Prova a eliminare la vecchia tabella se esiste
await _context.Database.ExecuteSqlRawAsync("DROP TABLE IF EXISTS RecordAssociations");
_logger.LogInformation("Vecchia tabella RecordAssociations eliminata");
}
catch (Exception)
{
// Ignora errori se la tabella non esiste
}
// Verifica se la tabella KeyAssociations esiste
try
{
await _context.Database.ExecuteSqlRawAsync(
"SELECT COUNT(*) FROM RecordAssociations LIMIT 1");
_logger.LogInformation("Tabella RecordAssociations già presente");
"SELECT COUNT(*) FROM KeyAssociations LIMIT 1");
_logger.LogInformation("Tabella KeyAssociations già presente");
}
catch (Microsoft.Data.Sqlite.SqliteException)
{
// La tabella non esiste, la creiamo
_logger.LogInformation("Creazione tabella RecordAssociations...");
_logger.LogInformation("Creazione tabella KeyAssociations...");
// Crea la tabella
await _context.Database.ExecuteSqlRawAsync(@"
CREATE TABLE RecordAssociations (
CREATE TABLE KeyAssociations (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
SourceName TEXT NOT NULL,
SourceType TEXT NOT NULL,
SourceKey TEXT NOT NULL,
KeyValue TEXT NOT NULL,
SourceKeyField TEXT NOT NULL,
DestinationKeyField TEXT NOT NULL,
DestinationEntity TEXT NOT NULL,
DestinationId TEXT NOT NULL,
RestCredentialName TEXT NOT NULL,
CreatedAt TEXT NOT NULL DEFAULT (datetime('now')),
UpdatedAt TEXT,
LastVerifiedAt TEXT,
IsActive INTEGER NOT NULL DEFAULT 1,
SourcesInfo TEXT,
AdditionalInfo TEXT
)");
// Crea gli indici
await _context.Database.ExecuteSqlRawAsync(@"
CREATE UNIQUE INDEX IX_RecordAssociations_Unique
ON RecordAssociations (SourceName, SourceKey, DestinationEntity)");
CREATE INDEX IX_KeyAssociations_KeyValue
ON KeyAssociations (KeyValue)");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_RecordAssociations_SourceType
ON RecordAssociations (SourceType)");
CREATE UNIQUE INDEX IX_KeyAssociations_Unique
ON KeyAssociations (KeyValue, DestinationEntity, RestCredentialName)");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_RecordAssociations_DestinationEntity
ON RecordAssociations (DestinationEntity)");
CREATE INDEX IX_KeyAssociations_DestinationEntity
ON KeyAssociations (DestinationEntity)");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_RecordAssociations_RestCredentialName
ON RecordAssociations (RestCredentialName)");
CREATE INDEX IX_KeyAssociations_RestCredentialName
ON KeyAssociations (RestCredentialName)");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_RecordAssociations_IsActive
ON RecordAssociations (IsActive)");
CREATE INDEX IX_KeyAssociations_IsActive
ON KeyAssociations (IsActive)");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_RecordAssociations_CreatedAt
ON RecordAssociations (CreatedAt)");
CREATE INDEX IX_KeyAssociations_CreatedAt
ON KeyAssociations (CreatedAt)");
_logger.LogInformation("Tabella RecordAssociations creata con successo");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_KeyAssociations_LastVerifiedAt
ON KeyAssociations (LastVerifiedAt)");
_logger.LogInformation("Tabella KeyAssociations creata con successo");
}
}
catch (Exception ex)
@@ -233,58 +251,67 @@ public class DatabaseInitializer : IDatabaseInitializer
}
}
private async Task CreateRecordAssociationsTableAsync()
private async Task CreateKeyAssociationsTableAsync()
{
try
{
_logger.LogInformation("Creazione tabella RecordAssociations...");
_logger.LogInformation("Creazione tabella KeyAssociations...");
// Crea la tabella
// Elimina la vecchia tabella se esiste
await _context.Database.ExecuteSqlRawAsync("DROP TABLE IF EXISTS RecordAssociations");
// Crea la nuova tabella
await _context.Database.ExecuteSqlRawAsync(@"
CREATE TABLE RecordAssociations (
CREATE TABLE KeyAssociations (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
SourceName TEXT NOT NULL,
SourceType TEXT NOT NULL,
SourceKey TEXT NOT NULL,
KeyValue TEXT NOT NULL,
SourceKeyField TEXT NOT NULL,
DestinationKeyField TEXT NOT NULL,
DestinationEntity TEXT NOT NULL,
DestinationId TEXT NOT NULL,
RestCredentialName TEXT NOT NULL,
CreatedAt TEXT NOT NULL DEFAULT (datetime('now')),
UpdatedAt TEXT,
LastVerifiedAt TEXT,
IsActive INTEGER NOT NULL DEFAULT 1,
SourcesInfo TEXT,
AdditionalInfo TEXT
)");
// Crea gli indici
await _context.Database.ExecuteSqlRawAsync(@"
CREATE UNIQUE INDEX IX_RecordAssociations_Unique
ON RecordAssociations (SourceName, SourceKey, DestinationEntity)");
CREATE INDEX IX_KeyAssociations_KeyValue
ON KeyAssociations (KeyValue)");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_RecordAssociations_SourceType
ON RecordAssociations (SourceType)");
CREATE UNIQUE INDEX IX_KeyAssociations_Unique
ON KeyAssociations (KeyValue, DestinationEntity, RestCredentialName)");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_RecordAssociations_DestinationEntity
ON RecordAssociations (DestinationEntity)");
CREATE INDEX IX_KeyAssociations_DestinationEntity
ON KeyAssociations (DestinationEntity)");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_RecordAssociations_RestCredentialName
ON RecordAssociations (RestCredentialName)");
CREATE INDEX IX_KeyAssociations_RestCredentialName
ON KeyAssociations (RestCredentialName)");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_RecordAssociations_IsActive
ON RecordAssociations (IsActive)");
CREATE INDEX IX_KeyAssociations_IsActive
ON KeyAssociations (IsActive)");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_RecordAssociations_CreatedAt
ON RecordAssociations (CreatedAt)");
CREATE INDEX IX_KeyAssociations_CreatedAt
ON KeyAssociations (CreatedAt)");
_logger.LogInformation("Tabella RecordAssociations creata con successo");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_KeyAssociations_LastVerifiedAt
ON KeyAssociations (LastVerifiedAt)");
_logger.LogInformation("Tabella KeyAssociations creata con successo");
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella creazione della tabella RecordAssociations");
_logger.LogError(ex, "Errore nella creazione della tabella KeyAssociations");
throw;
}
}
@@ -0,0 +1,109 @@
using CredentialManager.Models;
namespace CredentialManager.Services;
/// <summary>
/// Interfaccia per il servizio di gestione delle associazioni basate sui valori delle chiavi
/// </summary>
public interface IKeyAssociationService
{
/// <summary>
/// Salva una nuova associazione o aggiorna una esistente
/// </summary>
Task<int> SaveAssociationAsync(KeyAssociation association);
/// <summary>
/// Cerca un'associazione esistente tramite valore chiave
/// </summary>
Task<KeyAssociation?> FindAssociationByKeyValueAsync(string keyValue, string destinationEntity, string restCredentialName);
/// <summary>
/// Cerca un'associazione esistente tramite valore chiave, indipendentemente dalla destinazione
/// </summary>
Task<KeyAssociation?> FindAssociationByKeyValueAsync(string keyValue);
/// <summary>
/// Ottiene tutte le associazioni per un'entità di destinazione specifica
/// </summary>
Task<List<KeyAssociation>> GetAssociationsByDestinationAsync(string destinationEntity, string restCredentialName);
/// <summary>
/// Ottiene tutte le associazioni attive
/// </summary>
Task<List<KeyAssociation>> GetAllActiveAssociationsAsync();
/// <summary>
/// Ottiene tutte le associazioni (attive e non)
/// </summary>
Task<List<KeyAssociation>> GetAllAssociationsAsync();
/// <summary>
/// Aggiorna un'associazione esistente
/// </summary>
Task<bool> UpdateAssociationAsync(KeyAssociation association);
/// <summary>
/// Disattiva un'associazione
/// </summary>
Task<bool> DeactivateAssociationAsync(int id);
/// <summary>
/// Elimina definitivamente un'associazione
/// </summary>
Task<bool> DeleteAssociationAsync(int id);
/// <summary>
/// Pulisce le associazioni più vecchie di un determinato periodo
/// </summary>
Task<int> CleanupOldAssociationsAsync(TimeSpan olderThan);
/// <summary>
/// Elimina tutte le associazioni per una specifica combinazione entità-credenziale
/// </summary>
Task<int> ClearAssociationsAsync(string destinationEntity, string restCredentialName);
/// <summary>
/// Elimina tutte le associazioni nel sistema
/// </summary>
Task<int> ClearAllAssociationsAsync();
/// <summary>
/// Verifica se un ID di destinazione esiste ancora nel sistema target
/// </summary>
Task<bool> ValidateDestinationIdAsync(string destinationId, string destinationEntity, string restCredentialName);
/// <summary>
/// Ottiene tutte le associazioni con ID di destinazione non validi
/// </summary>
Task<List<KeyAssociation>> GetInvalidAssociationsAsync(string destinationEntity, string restCredentialName);
/// <summary>
/// Pulisce le associazioni con ID di destinazione non più validi
/// </summary>
Task<int> CleanupInvalidAssociationsAsync(string destinationEntity, string restCredentialName);
/// <summary>
/// Aggiorna la data di ultima verifica per un'associazione
/// </summary>
Task<bool> UpdateLastVerifiedAsync(int id);
/// <summary>
/// Ottiene statistiche sulle associazioni
/// </summary>
Task<AssociationStatistics> GetStatisticsAsync();
}
/// <summary>
/// Statistiche sulle associazioni
/// </summary>
public class AssociationStatistics
{
public int TotalAssociations { get; set; }
public int ActiveAssociations { get; set; }
public int InactiveAssociations { get; set; }
public int UniqueKeyValues { get; set; }
public int UniqueDestinationEntities { get; set; }
public DateTime? OldestAssociation { get; set; }
public DateTime? NewestAssociation { get; set; }
public Dictionary<string, int> AssociationsByEntity { get; set; } = new();
}
@@ -1,79 +0,0 @@
using CredentialManager.Models;
namespace CredentialManager.Services;
/// <summary>
/// Interfaccia per il servizio di gestione delle associazioni record
/// </summary>
public interface IRecordAssociationService
{
/// <summary>
/// Salva una nuova associazione tra record sorgente e destinazione
/// </summary>
Task<int> SaveAssociationAsync(RecordAssociation association);
/// <summary>
/// Cerca un'associazione esistente tramite chiave sorgente
/// </summary>
Task<RecordAssociation?> FindAssociationAsync(string sourceName, string sourceKey, string destinationEntity);
/// <summary>
/// Ottiene tutte le associazioni per una sorgente specifica
/// </summary>
Task<List<RecordAssociation>> GetAssociationsBySourceAsync(string sourceName, string sourceType);
/// <summary>
/// Ottiene tutte le associazioni per un'entità di destinazione specifica
/// </summary>
Task<List<RecordAssociation>> GetAssociationsByDestinationAsync(string destinationEntity, string restCredentialName);
/// <summary>
/// Ottiene tutte le associazioni attive
/// </summary>
Task<List<RecordAssociation>> GetAllActiveAssociationsAsync();
/// <summary>
/// Aggiorna un'associazione esistente
/// </summary>
Task<bool> UpdateAssociationAsync(RecordAssociation association);
/// <summary>
/// Disattiva un'associazione (soft delete)
/// </summary>
Task<bool> DeactivateAssociationAsync(int id);
/// <summary>
/// Elimina definitivamente un'associazione
/// </summary>
Task<bool> DeleteAssociationAsync(int id);
/// <summary>
/// Pulisce le associazioni obsolete (opzionale)
/// </summary>
Task<int> CleanupOldAssociationsAsync(TimeSpan olderThan);
/// <summary>
/// Elimina tutte le associazioni per una specifica combinazione sorgente-destinazione
/// </summary>
Task<int> ClearAssociationsAsync(string sourceName, string destinationEntity, string restCredentialName);
/// <summary>
/// Elimina tutte le associazioni nel sistema
/// </summary>
Task<int> ClearAllAssociationsAsync();
/// <summary>
/// Verifica se un ID di destinazione esiste ancora nel sistema target
/// </summary>
Task<bool> ValidateDestinationIdAsync(string destinationId, string destinationEntity, string restCredentialName);
/// <summary>
/// Ottiene tutte le associazioni con ID di destinazione non validi
/// </summary>
Task<List<RecordAssociation>> GetInvalidAssociationsAsync(string destinationEntity, string restCredentialName);
/// <summary>
/// Pulisce le associazioni con ID di destinazione non più validi
/// </summary>
Task<int> CleanupInvalidAssociationsAsync(string destinationEntity, string restCredentialName);
}
@@ -0,0 +1,494 @@
using CredentialManager.Data;
using CredentialManager.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace CredentialManager.Services;
/// <summary>
/// Servizio per la gestione delle associazioni basate sui valori delle chiavi
/// </summary>
public class KeyAssociationService : IKeyAssociationService
{
private readonly CredentialDbContext _context;
private readonly ILogger<KeyAssociationService> _logger;
public KeyAssociationService(
CredentialDbContext context,
ILogger<KeyAssociationService> logger)
{
_context = context;
_logger = logger;
}
public async Task<int> SaveAssociationAsync(KeyAssociation association)
{
try
{
_logger.LogInformation("DEBUG: Tentativo salvataggio associazione - KeyValue: '{KeyValue}', DestinationEntity: '{DestinationEntity}', DestinationId: '{DestinationId}', RestCredentialName: '{RestCredentialName}'",
association.KeyValue, association.DestinationEntity, association.DestinationId, association.RestCredentialName);
// Controlla se esiste già un'associazione per questo valore chiave e destinazione
var existing = await _context.KeyAssociations
.FirstOrDefaultAsync(ka =>
ka.KeyValue == association.KeyValue &&
ka.DestinationEntity == association.DestinationEntity &&
ka.RestCredentialName == association.RestCredentialName &&
ka.IsActive);
_logger.LogInformation("DEBUG: Controllo associazione esistente: {Found}. ID: {Id}",
existing != null, existing?.Id);
if (existing != null)
{
// Aggiorna l'associazione esistente
existing.DestinationId = association.DestinationId;
existing.SourceKeyField = association.SourceKeyField;
existing.DestinationKeyField = association.DestinationKeyField;
existing.UpdatedAt = DateTime.UtcNow;
existing.LastVerifiedAt = DateTime.UtcNow;
existing.AdditionalInfo = association.AdditionalInfo;
// Aggiorna le informazioni sulle sorgenti
UpdateSourcesInfo(existing, association);
_context.KeyAssociations.Update(existing);
await _context.SaveChangesAsync();
_logger.LogInformation("Associazione aggiornata: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}",
association.KeyValue, association.DestinationEntity, association.DestinationId);
return existing.Id;
}
else
{
// Crea nuova associazione
association.CreatedAt = DateTime.UtcNow;
association.LastVerifiedAt = DateTime.UtcNow;
_context.KeyAssociations.Add(association);
await _context.SaveChangesAsync();
_logger.LogInformation("Nuova associazione creata: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}",
association.KeyValue, association.DestinationEntity, association.DestinationId);
return association.Id;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel salvare l'associazione: KeyValue={KeyValue} -> {DestinationEntity}",
association.KeyValue, association.DestinationEntity);
throw;
}
}
public async Task<KeyAssociation?> FindAssociationByKeyValueAsync(string keyValue, string destinationEntity, string restCredentialName)
{
try
{
_logger.LogInformation("DEBUG: Ricerca associazione con parametri - KeyValue: '{KeyValue}', DestinationEntity: '{DestinationEntity}', RestCredentialName: '{RestCredentialName}'",
keyValue, destinationEntity, restCredentialName);
var result = await _context.KeyAssociations
.FirstOrDefaultAsync(ka =>
ka.KeyValue == keyValue &&
ka.DestinationEntity == destinationEntity &&
ka.RestCredentialName == restCredentialName &&
ka.IsActive);
_logger.LogInformation("DEBUG: Risultato ricerca associazione: {Found}. ID: {Id}, DestinationId: '{DestinationId}'",
result != null, result?.Id, result?.DestinationId);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella ricerca dell'associazione: KeyValue={KeyValue} -> {DestinationEntity}",
keyValue, destinationEntity);
throw;
}
}
public async Task<KeyAssociation?> FindAssociationByKeyValueAsync(string keyValue)
{
try
{
return await _context.KeyAssociations
.Where(ka => ka.KeyValue == keyValue && ka.IsActive)
.OrderByDescending(ka => ka.UpdatedAt ?? ka.CreatedAt)
.FirstOrDefaultAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella ricerca dell'associazione per KeyValue={KeyValue}", keyValue);
throw;
}
}
public async Task<List<KeyAssociation>> GetAssociationsByDestinationAsync(string destinationEntity, string restCredentialName)
{
try
{
return await _context.KeyAssociations
.Where(ka => ka.DestinationEntity == destinationEntity &&
ka.RestCredentialName == restCredentialName &&
ka.IsActive)
.OrderByDescending(ka => ka.CreatedAt)
.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel recupero delle associazioni per destinazione: {DestinationEntity} ({RestCredentialName})",
destinationEntity, restCredentialName);
throw;
}
}
public async Task<List<KeyAssociation>> GetAllActiveAssociationsAsync()
{
try
{
return await _context.KeyAssociations
.Where(ka => ka.IsActive)
.OrderByDescending(ka => ka.CreatedAt)
.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel recupero di tutte le associazioni attive");
throw;
}
}
public async Task<List<KeyAssociation>> GetAllAssociationsAsync()
{
try
{
return await _context.KeyAssociations
.OrderByDescending(ka => ka.CreatedAt)
.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel recupero di tutte le associazioni");
throw;
}
}
public async Task<bool> UpdateAssociationAsync(KeyAssociation association)
{
try
{
var existing = await _context.KeyAssociations.FindAsync(association.Id);
if (existing == null)
{
_logger.LogWarning("Associazione con ID {Id} non trovata per l'aggiornamento", association.Id);
return false;
}
existing.KeyValue = association.KeyValue;
existing.SourceKeyField = association.SourceKeyField;
existing.DestinationKeyField = association.DestinationKeyField;
existing.DestinationId = association.DestinationId;
existing.RestCredentialName = association.RestCredentialName;
existing.UpdatedAt = DateTime.UtcNow;
existing.AdditionalInfo = association.AdditionalInfo;
existing.SourcesInfo = association.SourcesInfo;
existing.IsActive = association.IsActive;
_context.KeyAssociations.Update(existing);
await _context.SaveChangesAsync();
_logger.LogInformation("Associazione aggiornata: ID {Id}", association.Id);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nell'aggiornamento dell'associazione: ID {Id}", association.Id);
throw;
}
}
public async Task<bool> DeactivateAssociationAsync(int id)
{
try
{
var association = await _context.KeyAssociations.FindAsync(id);
if (association == null)
{
_logger.LogWarning("Associazione con ID {Id} non trovata per la disattivazione", id);
return false;
}
association.IsActive = false;
association.UpdatedAt = DateTime.UtcNow;
_context.KeyAssociations.Update(association);
await _context.SaveChangesAsync();
_logger.LogInformation("Associazione disattivata: ID {Id}", id);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella disattivazione dell'associazione: ID {Id}", id);
throw;
}
}
public async Task<bool> DeleteAssociationAsync(int id)
{
try
{
var association = await _context.KeyAssociations.FindAsync(id);
if (association == null)
{
_logger.LogWarning("Associazione con ID {Id} non trovata per l'eliminazione", id);
return false;
}
_context.KeyAssociations.Remove(association);
await _context.SaveChangesAsync();
_logger.LogInformation("Associazione eliminata: ID {Id}", id);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nell'eliminazione dell'associazione: ID {Id}", id);
throw;
}
}
public async Task<int> CleanupOldAssociationsAsync(TimeSpan olderThan)
{
try
{
var cutoffDate = DateTime.UtcNow - olderThan;
var oldAssociations = await _context.KeyAssociations
.Where(ka => ka.CreatedAt < cutoffDate && !ka.IsActive)
.ToListAsync();
if (oldAssociations.Any())
{
_context.KeyAssociations.RemoveRange(oldAssociations);
await _context.SaveChangesAsync();
_logger.LogInformation("Pulite {Count} associazioni obsolete più vecchie di {Cutoff}",
oldAssociations.Count, cutoffDate);
}
return oldAssociations.Count;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella pulizia delle associazioni obsolete");
throw;
}
}
public async Task<int> ClearAssociationsAsync(string destinationEntity, string restCredentialName)
{
try
{
var associationsToDelete = await _context.KeyAssociations
.Where(ka => ka.DestinationEntity == destinationEntity &&
ka.RestCredentialName == restCredentialName)
.ToListAsync();
if (associationsToDelete.Any())
{
_context.KeyAssociations.RemoveRange(associationsToDelete);
await _context.SaveChangesAsync();
_logger.LogInformation("Eliminate {Count} associazioni per {DestinationEntity}",
associationsToDelete.Count, destinationEntity);
}
return associationsToDelete.Count;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella cancellazione delle associazioni per {DestinationEntity}",
destinationEntity);
throw;
}
}
public async Task<int> ClearAllAssociationsAsync()
{
try
{
var allAssociations = await _context.KeyAssociations.ToListAsync();
var count = allAssociations.Count;
if (allAssociations.Any())
{
_context.KeyAssociations.RemoveRange(allAssociations);
await _context.SaveChangesAsync();
_logger.LogWarning("Eliminate TUTTE le {Count} associazioni dal sistema", count);
}
return count;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella cancellazione di tutte le associazioni");
throw;
}
}
public async Task<bool> ValidateDestinationIdAsync(string destinationId, string destinationEntity, string restCredentialName)
{
// Questa implementazione base restituisce sempre true
// Dovrebbe essere estesa per verificare effettivamente l'esistenza nel sistema REST
try
{
// TODO: Implementare la logica di validazione effettiva con il servizio REST
// Per ora assumiamo che l'ID sia valido
_logger.LogDebug("Validazione ID destinazione {DestinationId} per entità {DestinationEntity} - Non implementata",
destinationId, destinationEntity);
return await Task.FromResult(true);
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella validazione dell'ID destinazione {DestinationId}", destinationId);
return false;
}
}
public async Task<List<KeyAssociation>> GetInvalidAssociationsAsync(string destinationEntity, string restCredentialName)
{
try
{
var associations = await _context.KeyAssociations
.Where(ka => ka.DestinationEntity == destinationEntity &&
ka.RestCredentialName == restCredentialName &&
ka.IsActive)
.ToListAsync();
var invalidAssociations = new List<KeyAssociation>();
// Verifica ogni associazione
foreach (var association in associations)
{
var isValid = await ValidateDestinationIdAsync(association.DestinationId, destinationEntity, restCredentialName);
if (!isValid)
{
invalidAssociations.Add(association);
}
}
_logger.LogInformation("Trovate {Invalid}/{Total} associazioni non valide per {DestinationEntity}",
invalidAssociations.Count, associations.Count, destinationEntity);
return invalidAssociations;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel recupero delle associazioni non valide per {DestinationEntity}", destinationEntity);
throw;
}
}
public async Task<int> CleanupInvalidAssociationsAsync(string destinationEntity, string restCredentialName)
{
try
{
var invalidAssociations = await GetInvalidAssociationsAsync(destinationEntity, restCredentialName);
if (invalidAssociations.Any())
{
_context.KeyAssociations.RemoveRange(invalidAssociations);
await _context.SaveChangesAsync();
_logger.LogWarning("Eliminate {Count} associazioni non valide per {DestinationEntity}",
invalidAssociations.Count, destinationEntity);
}
return invalidAssociations.Count;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella pulizia delle associazioni non valide per {DestinationEntity}", destinationEntity);
throw;
}
}
public async Task<bool> UpdateLastVerifiedAsync(int id)
{
try
{
var association = await _context.KeyAssociations.FindAsync(id);
if (association == null)
{
_logger.LogWarning("Associazione con ID {Id} non trovata per l'aggiornamento della verifica", id);
return false;
}
association.LastVerifiedAt = DateTime.UtcNow;
_context.KeyAssociations.Update(association);
await _context.SaveChangesAsync();
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nell'aggiornamento della verifica per associazione: ID {Id}", id);
throw;
}
}
public async Task<AssociationStatistics> GetStatisticsAsync()
{
try
{
var allAssociations = await _context.KeyAssociations.ToListAsync();
var stats = new AssociationStatistics
{
TotalAssociations = allAssociations.Count,
ActiveAssociations = allAssociations.Count(a => a.IsActive),
InactiveAssociations = allAssociations.Count(a => !a.IsActive),
UniqueKeyValues = allAssociations.Select(a => a.KeyValue).Distinct().Count(),
UniqueDestinationEntities = allAssociations.Select(a => a.DestinationEntity).Distinct().Count(),
OldestAssociation = allAssociations.Any() ? allAssociations.Min(a => a.CreatedAt) : null,
NewestAssociation = allAssociations.Any() ? allAssociations.Max(a => a.CreatedAt) : null,
AssociationsByEntity = allAssociations
.GroupBy(a => a.DestinationEntity)
.ToDictionary(g => g.Key, g => g.Count())
};
return stats;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel calcolo delle statistiche delle associazioni");
throw;
}
}
private void UpdateSourcesInfo(KeyAssociation existing, KeyAssociation newAssociation)
{
try
{
var sourcesInfo = existing.SourcesInfo ?? "";
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm");
var newSourceInfo = $"{timestamp}: {newAssociation.SourceKeyField}";
if (!sourcesInfo.Contains(newSourceInfo))
{
existing.SourcesInfo = string.IsNullOrEmpty(sourcesInfo)
? newSourceInfo
: $"{sourcesInfo}; {newSourceInfo}";
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Errore nell'aggiornamento delle informazioni sulle sorgenti");
}
}
}
@@ -1,381 +0,0 @@
using CredentialManager.Data;
using CredentialManager.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace CredentialManager.Services;
/// <summary>
/// Servizio per la gestione delle associazioni tra record sorgente e destinazione
/// </summary>
public class RecordAssociationService : IRecordAssociationService
{
private readonly CredentialDbContext _context;
private readonly ILogger<RecordAssociationService> _logger;
public RecordAssociationService(
CredentialDbContext context,
ILogger<RecordAssociationService> logger)
{
_context = context;
_logger = logger;
}
public async Task<int> SaveAssociationAsync(RecordAssociation association)
{
try
{
// Controlla se esiste già un'associazione per questa combinazione
var existing = await _context.RecordAssociations
.FirstOrDefaultAsync(ra =>
ra.SourceName == association.SourceName &&
ra.SourceKey == association.SourceKey &&
ra.DestinationEntity == association.DestinationEntity &&
ra.IsActive);
if (existing != null)
{
// Aggiorna l'associazione esistente
existing.DestinationId = association.DestinationId;
existing.RestCredentialName = association.RestCredentialName;
existing.UpdatedAt = DateTime.UtcNow;
existing.AdditionalInfo = association.AdditionalInfo;
_context.RecordAssociations.Update(existing);
await _context.SaveChangesAsync();
_logger.LogInformation("Associazione aggiornata: {SourceName}/{SourceKey} -> {DestinationEntity}/{DestinationId}",
association.SourceName, association.SourceKey, association.DestinationEntity, association.DestinationId);
return existing.Id;
}
else
{
// Crea nuova associazione
_context.RecordAssociations.Add(association);
await _context.SaveChangesAsync();
_logger.LogInformation("Nuova associazione creata: {SourceName}/{SourceKey} -> {DestinationEntity}/{DestinationId}",
association.SourceName, association.SourceKey, association.DestinationEntity, association.DestinationId);
return association.Id;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel salvare l'associazione: {SourceName}/{SourceKey} -> {DestinationEntity}",
association.SourceName, association.SourceKey, association.DestinationEntity);
throw;
}
}
public async Task<RecordAssociation?> FindAssociationAsync(string sourceName, string sourceKey, string destinationEntity)
{
try
{
return await _context.RecordAssociations
.FirstOrDefaultAsync(ra =>
ra.SourceName == sourceName &&
ra.SourceKey == sourceKey &&
ra.DestinationEntity == destinationEntity &&
ra.IsActive);
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella ricerca dell'associazione: {SourceName}/{SourceKey} -> {DestinationEntity}",
sourceName, sourceKey, destinationEntity);
throw;
}
}
public async Task<List<RecordAssociation>> GetAssociationsBySourceAsync(string sourceName, string sourceType)
{
try
{
return await _context.RecordAssociations
.Where(ra => ra.SourceName == sourceName && ra.SourceType == sourceType && ra.IsActive)
.OrderByDescending(ra => ra.CreatedAt)
.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel recupero delle associazioni per sorgente: {SourceName} ({SourceType})",
sourceName, sourceType);
throw;
}
}
public async Task<List<RecordAssociation>> GetAssociationsByDestinationAsync(string destinationEntity, string restCredentialName)
{
try
{
return await _context.RecordAssociations
.Where(ra => ra.DestinationEntity == destinationEntity &&
ra.RestCredentialName == restCredentialName &&
ra.IsActive)
.OrderByDescending(ra => ra.CreatedAt)
.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel recupero delle associazioni per destinazione: {DestinationEntity} ({RestCredentialName})",
destinationEntity, restCredentialName);
throw;
}
}
public async Task<List<RecordAssociation>> GetAllActiveAssociationsAsync()
{
try
{
return await _context.RecordAssociations
.Where(ra => ra.IsActive)
.OrderByDescending(ra => ra.CreatedAt)
.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel recupero di tutte le associazioni attive");
throw;
}
}
public async Task<bool> UpdateAssociationAsync(RecordAssociation association)
{
try
{
var existing = await _context.RecordAssociations.FindAsync(association.Id);
if (existing == null)
{
_logger.LogWarning("Associazione con ID {Id} non trovata per l'aggiornamento", association.Id);
return false;
}
existing.DestinationId = association.DestinationId;
existing.RestCredentialName = association.RestCredentialName;
existing.UpdatedAt = DateTime.UtcNow;
existing.AdditionalInfo = association.AdditionalInfo;
existing.IsActive = association.IsActive;
_context.RecordAssociations.Update(existing);
await _context.SaveChangesAsync();
_logger.LogInformation("Associazione aggiornata: ID {Id}", association.Id);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nell'aggiornamento dell'associazione: ID {Id}", association.Id);
throw;
}
}
public async Task<bool> DeactivateAssociationAsync(int id)
{
try
{
var association = await _context.RecordAssociations.FindAsync(id);
if (association == null)
{
_logger.LogWarning("Associazione con ID {Id} non trovata per la disattivazione", id);
return false;
}
association.IsActive = false;
association.UpdatedAt = DateTime.UtcNow;
_context.RecordAssociations.Update(association);
await _context.SaveChangesAsync();
_logger.LogInformation("Associazione disattivata: ID {Id}", id);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella disattivazione dell'associazione: ID {Id}", id);
throw;
}
}
public async Task<bool> DeleteAssociationAsync(int id)
{
try
{
var association = await _context.RecordAssociations.FindAsync(id);
if (association == null)
{
_logger.LogWarning("Associazione con ID {Id} non trovata per l'eliminazione", id);
return false;
}
_context.RecordAssociations.Remove(association);
await _context.SaveChangesAsync();
_logger.LogInformation("Associazione eliminata: ID {Id}", id);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nell'eliminazione dell'associazione: ID {Id}", id);
throw;
}
}
public async Task<int> CleanupOldAssociationsAsync(TimeSpan olderThan)
{
try
{
var cutoffDate = DateTime.UtcNow - olderThan;
var oldAssociations = await _context.RecordAssociations
.Where(ra => ra.CreatedAt < cutoffDate && !ra.IsActive)
.ToListAsync();
if (oldAssociations.Any())
{
_context.RecordAssociations.RemoveRange(oldAssociations);
await _context.SaveChangesAsync();
_logger.LogInformation("Pulite {Count} associazioni obsolete più vecchie di {Cutoff}",
oldAssociations.Count, cutoffDate);
}
return oldAssociations.Count;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella pulizia delle associazioni obsolete");
throw;
}
}
public async Task<int> ClearAssociationsAsync(string sourceName, string destinationEntity, string restCredentialName)
{
try
{
var associationsToDelete = await _context.RecordAssociations
.Where(ra => ra.SourceName == sourceName &&
ra.DestinationEntity == destinationEntity &&
ra.RestCredentialName == restCredentialName)
.ToListAsync();
if (associationsToDelete.Any())
{
_context.RecordAssociations.RemoveRange(associationsToDelete);
await _context.SaveChangesAsync();
_logger.LogInformation("Eliminate {Count} associazioni per {SourceName} -> {DestinationEntity}",
associationsToDelete.Count, sourceName, destinationEntity);
}
return associationsToDelete.Count;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella cancellazione delle associazioni per {SourceName} -> {DestinationEntity}",
sourceName, destinationEntity);
throw;
}
}
public async Task<int> ClearAllAssociationsAsync()
{
try
{
var allAssociations = await _context.RecordAssociations.ToListAsync();
var count = allAssociations.Count;
if (allAssociations.Any())
{
_context.RecordAssociations.RemoveRange(allAssociations);
await _context.SaveChangesAsync();
_logger.LogWarning("Eliminate TUTTE le {Count} associazioni dal sistema", count);
}
return count;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella cancellazione di tutte le associazioni");
throw;
}
}
public async Task<bool> ValidateDestinationIdAsync(string destinationId, string destinationEntity, string restCredentialName)
{
// Questa implementazione base restituisce sempre true
// Dovrebbe essere estesa per verificare effettivamente l'esistenza nel sistema REST
try
{
// TODO: Implementare la logica di validazione effettiva con il servizio REST
// Per ora assumiamo che l'ID sia valido
_logger.LogDebug("Validazione ID destinazione {DestinationId} per entità {DestinationEntity} - Non implementata",
destinationId, destinationEntity);
return await Task.FromResult(true);
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella validazione dell'ID destinazione {DestinationId}", destinationId);
return false;
}
}
public async Task<List<RecordAssociation>> GetInvalidAssociationsAsync(string destinationEntity, string restCredentialName)
{
try
{
var associations = await _context.RecordAssociations
.Where(ra => ra.DestinationEntity == destinationEntity &&
ra.RestCredentialName == restCredentialName &&
ra.IsActive)
.ToListAsync();
var invalidAssociations = new List<RecordAssociation>();
// Verifica ogni associazione
foreach (var association in associations)
{
var isValid = await ValidateDestinationIdAsync(association.DestinationId, destinationEntity, restCredentialName);
if (!isValid)
{
invalidAssociations.Add(association);
}
}
_logger.LogInformation("Trovate {Invalid}/{Total} associazioni non valide per {DestinationEntity}",
invalidAssociations.Count, associations.Count, destinationEntity);
return invalidAssociations;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel recupero delle associazioni non valide per {DestinationEntity}", destinationEntity);
throw;
}
}
public async Task<int> CleanupInvalidAssociationsAsync(string destinationEntity, string restCredentialName)
{
try
{
var invalidAssociations = await GetInvalidAssociationsAsync(destinationEntity, restCredentialName);
if (invalidAssociations.Any())
{
_context.RecordAssociations.RemoveRange(invalidAssociations);
await _context.SaveChangesAsync();
_logger.LogWarning("Eliminate {Count} associazioni non valide per {DestinationEntity}",
invalidAssociations.Count, destinationEntity);
}
return invalidAssociations.Count;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella pulizia delle associazioni non valide per {DestinationEntity}", destinationEntity);
throw;
}
}
}
@@ -1,4 +1,5 @@
using CredentialManager.Models;
using CredentialManager.Services;
namespace DataConnection.CredentialManagement.Interfaces;
@@ -57,17 +58,20 @@ public interface IDataConnectionCredentialService
Task<(bool Success, string Message)> TestSalesforceConnectionAsync(string credentialName);
Task<(bool Success, string Message)> TestSalesforceConnectionAsync(SalesforceCredential credential);
// Record associations
Task<int> SaveRecordAssociationAsync(RecordAssociation association);
Task<RecordAssociation?> FindRecordAssociationAsync(string sourceName, string sourceKey, string destinationEntity);
Task<List<RecordAssociation>> GetRecordAssociationsBySourceAsync(string sourceName, string sourceType);
Task<List<RecordAssociation>> GetRecordAssociationsByDestinationAsync(string destinationEntity, string restCredentialName);
Task<List<RecordAssociation>> GetAllActiveRecordAssociationsAsync();
Task<bool> UpdateRecordAssociationAsync(RecordAssociation association);
Task<bool> DeactivateRecordAssociationAsync(int id);
Task<bool> DeleteRecordAssociationAsync(int id);
Task<int> ClearRecordAssociationsAsync(string sourceName, string destinationEntity, string restCredentialName);
Task<int> ClearAllRecordAssociationsAsync();
Task<List<RecordAssociation>> GetInvalidRecordAssociationsAsync(string destinationEntity, string restCredentialName);
Task<int> CleanupInvalidRecordAssociationsAsync(string destinationEntity, string restCredentialName);
// Key associations
Task<int> SaveKeyAssociationAsync(KeyAssociation association);
Task<KeyAssociation?> FindKeyAssociationByValueAsync(string keyValue, string destinationEntity, string restCredentialName);
Task<KeyAssociation?> FindKeyAssociationByValueAsync(string keyValue);
Task<List<KeyAssociation>> GetKeyAssociationsByDestinationAsync(string destinationEntity, string restCredentialName);
Task<List<KeyAssociation>> GetAllActiveKeyAssociationsAsync();
Task<List<KeyAssociation>> GetAllKeyAssociationsAsync();
Task<bool> UpdateKeyAssociationAsync(KeyAssociation association);
Task<bool> DeactivateKeyAssociationAsync(int id);
Task<bool> DeleteKeyAssociationAsync(int id);
Task<int> ClearKeyAssociationsAsync(string destinationEntity, string restCredentialName);
Task<int> ClearAllKeyAssociationsAsync();
Task<List<KeyAssociation>> GetInvalidKeyAssociationsAsync(string destinationEntity, string restCredentialName);
Task<int> CleanupInvalidKeyAssociationsAsync(string destinationEntity, string restCredentialName);
Task<bool> UpdateKeyAssociationLastVerifiedAsync(int id);
Task<AssociationStatistics> GetKeyAssociationStatisticsAsync();
}
@@ -38,8 +38,8 @@ public static class ServiceCollectionExtensions
// Aggiungi i servizi base di CredentialManager
services.AddCredentialManager(databasePath);
// Aggiungi il servizio di gestione associazioni record
services.AddScoped<IRecordAssociationService, RecordAssociationService>();
// Aggiungi il servizio di gestione associazioni per chiavi
services.AddScoped<IKeyAssociationService, KeyAssociationService>();
// Aggiungi il servizio di integrazione DataConnection
services.AddScoped<IDataConnectionCredentialService, DataConnectionCredentialService>();
@@ -15,16 +15,16 @@ namespace DataConnection.CredentialManagement.Services;
public class DataConnectionCredentialService : IDataConnectionCredentialService
{
private readonly ICredentialService _credentialService;
private readonly IRecordAssociationService _recordAssociationService;
private readonly IKeyAssociationService _keyAssociationService;
private readonly ILogger<DataConnectionCredentialService> _logger;
public DataConnectionCredentialService(
ICredentialService credentialService,
IRecordAssociationService recordAssociationService,
IKeyAssociationService keyAssociationService,
ILogger<DataConnectionCredentialService> logger)
{
_credentialService = credentialService;
_recordAssociationService = recordAssociationService;
_keyAssociationService = keyAssociationService;
_logger = logger;
}
@@ -859,66 +859,81 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService
#endregion
#region Record Associations
#region Key Associations
public async Task<int> SaveRecordAssociationAsync(RecordAssociation association)
public async Task<int> SaveKeyAssociationAsync(KeyAssociation association)
{
return await _recordAssociationService.SaveAssociationAsync(association);
return await _keyAssociationService.SaveAssociationAsync(association);
}
public async Task<RecordAssociation?> FindRecordAssociationAsync(string sourceName, string sourceKey, string destinationEntity)
public async Task<KeyAssociation?> FindKeyAssociationByValueAsync(string keyValue, string destinationEntity, string restCredentialName)
{
return await _recordAssociationService.FindAssociationAsync(sourceName, sourceKey, destinationEntity);
return await _keyAssociationService.FindAssociationByKeyValueAsync(keyValue, destinationEntity, restCredentialName);
}
public async Task<List<RecordAssociation>> GetRecordAssociationsBySourceAsync(string sourceName, string sourceType)
public async Task<KeyAssociation?> FindKeyAssociationByValueAsync(string keyValue)
{
return await _recordAssociationService.GetAssociationsBySourceAsync(sourceName, sourceType);
return await _keyAssociationService.FindAssociationByKeyValueAsync(keyValue);
}
public async Task<List<RecordAssociation>> GetRecordAssociationsByDestinationAsync(string destinationEntity, string restCredentialName)
public async Task<List<KeyAssociation>> GetKeyAssociationsByDestinationAsync(string destinationEntity, string restCredentialName)
{
return await _recordAssociationService.GetAssociationsByDestinationAsync(destinationEntity, restCredentialName);
return await _keyAssociationService.GetAssociationsByDestinationAsync(destinationEntity, restCredentialName);
}
public async Task<List<RecordAssociation>> GetAllActiveRecordAssociationsAsync()
public async Task<List<KeyAssociation>> GetAllActiveKeyAssociationsAsync()
{
return await _recordAssociationService.GetAllActiveAssociationsAsync();
return await _keyAssociationService.GetAllActiveAssociationsAsync();
}
public async Task<bool> UpdateRecordAssociationAsync(RecordAssociation association)
public async Task<List<KeyAssociation>> GetAllKeyAssociationsAsync()
{
return await _recordAssociationService.UpdateAssociationAsync(association);
return await _keyAssociationService.GetAllAssociationsAsync();
}
public async Task<bool> DeactivateRecordAssociationAsync(int id)
public async Task<bool> UpdateKeyAssociationAsync(KeyAssociation association)
{
return await _recordAssociationService.DeactivateAssociationAsync(id);
return await _keyAssociationService.UpdateAssociationAsync(association);
}
public async Task<bool> DeleteRecordAssociationAsync(int id)
public async Task<bool> DeactivateKeyAssociationAsync(int id)
{
return await _recordAssociationService.DeleteAssociationAsync(id);
return await _keyAssociationService.DeactivateAssociationAsync(id);
}
public async Task<int> ClearRecordAssociationsAsync(string sourceName, string destinationEntity, string restCredentialName)
public async Task<bool> DeleteKeyAssociationAsync(int id)
{
return await _recordAssociationService.ClearAssociationsAsync(sourceName, destinationEntity, restCredentialName);
return await _keyAssociationService.DeleteAssociationAsync(id);
}
public async Task<int> ClearAllRecordAssociationsAsync()
public async Task<int> ClearKeyAssociationsAsync(string destinationEntity, string restCredentialName)
{
return await _recordAssociationService.ClearAllAssociationsAsync();
return await _keyAssociationService.ClearAssociationsAsync(destinationEntity, restCredentialName);
}
public async Task<List<RecordAssociation>> GetInvalidRecordAssociationsAsync(string destinationEntity, string restCredentialName)
public async Task<int> ClearAllKeyAssociationsAsync()
{
return await _recordAssociationService.GetInvalidAssociationsAsync(destinationEntity, restCredentialName);
return await _keyAssociationService.ClearAllAssociationsAsync();
}
public async Task<int> CleanupInvalidRecordAssociationsAsync(string destinationEntity, string restCredentialName)
public async Task<List<KeyAssociation>> GetInvalidKeyAssociationsAsync(string destinationEntity, string restCredentialName)
{
return await _recordAssociationService.CleanupInvalidAssociationsAsync(destinationEntity, restCredentialName);
return await _keyAssociationService.GetInvalidAssociationsAsync(destinationEntity, restCredentialName);
}
public async Task<int> CleanupInvalidKeyAssociationsAsync(string destinationEntity, string restCredentialName)
{
return await _keyAssociationService.CleanupInvalidAssociationsAsync(destinationEntity, restCredentialName);
}
public async Task<bool> UpdateKeyAssociationLastVerifiedAsync(int id)
{
return await _keyAssociationService.UpdateLastVerifiedAsync(id);
}
public async Task<AssociationStatistics> GetKeyAssociationStatisticsAsync()
{
return await _keyAssociationService.GetStatisticsAsync();
}
#endregion
+4
View File
@@ -14,6 +14,10 @@
<ItemGroup>
<PackageReference Include="ExcelDataReader" Version="3.7.0" />
<PackageReference Include="ExcelDataReader.DataSet" Version="3.7.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.6" />
</ItemGroup>
+112 -68
View File
@@ -834,13 +834,24 @@
<input class="form-check-input" type="checkbox" id="useAssociations"
@bind="useRecordAssociations" />
<label class="form-check-label" for="useAssociations">
<strong>Utilizza sistema di associazioni per tracking automatico degli aggiornamenti</strong>
<br><small class="text-muted">Raccomandato: il sistema manterrà traccia delle associazioni tra record sorgente e destinazione</small>
<strong>Utilizza sistema di associazioni basato sui valori delle chiavi</strong>
<br><small class="text-muted">Raccomandato: il sistema manterrà traccia delle associazioni tra valori chiave e record di destinazione, permettendo aggiornamenti indipendentemente dalla sorgente</small>
</label>
</div>
@if (useRecordAssociations)
{
<div class="alert alert-info">
<i class="fas fa-lightbulb"></i>
<strong>Come funziona il nuovo sistema:</strong>
<ul class="mb-0 mt-2">
<li>Ogni valore di chiave univoco viene associato a un record di destinazione</li>
<li>Più sorgenti diverse possono gestire lo stesso oggetto business usando lo stesso valore chiave</li>
<li>Gli aggiornamenti avvengono automaticamente quando si trova un'associazione esistente</li>
<li>Il sistema individua automaticamente le chiavi dove possibile, ma puoi sempre scegliere manualmente</li>
</ul>
</div>
<div class="row">
<div class="col-md-6">
<label class="form-label">Campo Chiave Sorgente: <span class="text-danger">*</span></label>
@@ -2126,79 +2137,101 @@
// Genera la chiave sorgente per questo record
var sourceKey = GenerateSourceKey(record);
var currentSourceName = selectedSourceType == "database"
? (useCustomQuery ? "custom_query" : selectedTable)
: selectedSheet;
// NUOVA LOGICA: Cerca associazione esistente
// NUOVO SISTEMA: Cerca associazione esistente basata sul valore della chiave
if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey))
{
var existingAssociation = await CredentialService.FindRecordAssociationAsync(
currentSourceName, sourceKey, selectedRestEntity.Name);
Logger.LogInformation("ASSOCIATION DEBUG: Cerco associazione - KeyValue: '{KeyValue}', Entity: '{Entity}', Credential: '{Credential}'",
sourceKey, selectedRestEntity.Name, selectedRestCredential);
// Cerca se esiste già un'associazione per questo valore chiave
var existingAssociation = await CredentialService.FindKeyAssociationByValueAsync(
sourceKey, selectedRestEntity.Name, selectedRestCredential);
// FALLBACK: Se non troviamo l'associazione con tutti i parametri, proviamo solo con il KeyValue
if (existingAssociation == null)
{
Logger.LogWarning("ASSOCIATION DEBUG: Associazione non trovata con parametri specifici, provo solo con KeyValue: '{KeyValue}'", sourceKey);
existingAssociation = await CredentialService.FindKeyAssociationByValueAsync(sourceKey);
if (existingAssociation != null)
{
Logger.LogWarning("ASSOCIATION DEBUG: Trovata associazione con fallback - ID: {AssociationId}, Entity: '{Entity}', Credential: '{Credential}'",
existingAssociation.Id, existingAssociation.DestinationEntity, existingAssociation.RestCredentialName);
// Verifica se l'associazione trovata è compatibile
if (existingAssociation.DestinationEntity != selectedRestEntity.Name ||
existingAssociation.RestCredentialName != selectedRestCredential)
{
Logger.LogWarning("ASSOCIATION DEBUG: Associazione non compatibile - Entity: '{FoundEntity}' vs '{ExpectedEntity}', Credential: '{FoundCredential}' vs '{ExpectedCredential}'",
existingAssociation.DestinationEntity, selectedRestEntity.Name, existingAssociation.RestCredentialName, selectedRestCredential);
existingAssociation = null;
}
}
}
Logger.LogInformation("ASSOCIATION DEBUG: Associazione finale: {Found}. ID: {AssociationId}, DestinationId: '{DestinationId}', IsActive: {IsActive}",
existingAssociation != null, existingAssociation?.Id, existingAssociation?.DestinationId, existingAssociation?.IsActive);
if (existingAssociation != null && existingAssociation.IsActive)
{
// VALIDAZIONE: Verifica se l'ID di destinazione esiste ancora nel sistema target
bool destinationExists = false;
// Prova direttamente l'aggiornamento - più efficiente che verificare prima l'esistenza
Logger.LogInformation("ASSOCIATION DEBUG: Tentativo aggiornamento record esistente - DestinationId: '{DestinationId}'", existingAssociation.DestinationId);
try
{
// Usa il campo ID appropriato per cercare l'entità
var idField = GetEntityIdField(); // Potrebbe essere "DocEntry", "id", "Id", etc.
var searchKeys = new Dictionary<string, object> { { idField, existingAssociation.DestinationId } };
var foundEntities = await currentRestClient.FindEntitiesByKeysAsync(
selectedRestEntity.Name, searchKeys);
destinationExists = foundEntities != null && foundEntities.Any();
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Errore nella verifica dell'esistenza dell'entità {EntityId} - assumo che non esista", existingAssociation.DestinationId);
destinationExists = false;
}
if (!destinationExists)
{
// L'ID di destinazione non esiste più - elimina l'associazione non valida
Logger.LogWarning("ID destinazione {DestinationId} non più valido per associazione {AssociationId} - eliminazione associazione",
existingAssociation.DestinationId, existingAssociation.Id);
var updateResult = await currentRestClient.UpdateEntityAsync(
selectedRestEntity.Name, existingAssociation.DestinationId, restData);
await CredentialService.DeleteRecordAssociationAsync(existingAssociation.Id);
transferResult.Status = "error";
transferResult.Message = $"Associazione non valida eliminata (ID destinazione {existingAssociation.DestinationId} non esiste più) - creazione nuovo record";
// Procedi con la creazione di un nuovo record
goto CreateNewRecord;
if (updateResult != null)
{
updatedCount++;
transferResult.Status = "updated";
transferResult.Message = $"Record aggiornato con successo tramite associazione (ID: {existingAssociation.DestinationId})";
transferResult.EntityId = existingAssociation.DestinationId;
// Aggiorna l'associazione con la data di ultimo aggiornamento e verifica
existingAssociation.UpdatedAt = DateTime.UtcNow;
existingAssociation.LastVerifiedAt = DateTime.UtcNow;
await CredentialService.UpdateKeyAssociationAsync(existingAssociation);
Logger.LogInformation("ASSOCIATION DEBUG: Record aggiornato con successo tramite associazione: {EntityId} per valore chiave {KeyValue}",
existingAssociation.DestinationId, sourceKey);
transferResults.Add(transferResult);
recordNumber++;
continue;
}
else
{
// Update fallito ma senza eccezione - probabilmente l'entità non esiste più
Logger.LogWarning("ASSOCIATION DEBUG: Aggiornamento fallito (result null) per associazione {AssociationId} - elimino associazione e creo nuovo record", existingAssociation.Id);
goto HandleInvalidAssociation;
}
}
catch (Exception updateEx)
{
// Update fallito con eccezione - probabilmente l'entità non esiste più
Logger.LogWarning(updateEx, "ASSOCIATION DEBUG: Aggiornamento fallito per associazione {AssociationId} - elimino associazione e creo nuovo record", existingAssociation.Id);
goto HandleInvalidAssociation;
}
// L'ID di destinazione esiste - procedi con l'aggiornamento
var updateResult = await currentRestClient.UpdateEntityAsync(
selectedRestEntity.Name, existingAssociation.DestinationId, restData);
if (updateResult != null)
HandleInvalidAssociation:
// L'ID di destinazione non esiste più o l'update è fallito - elimina l'associazione non valida
try
{
updatedCount++;
transferResult.Status = "updated";
transferResult.Message = $"Record aggiornato con successo tramite associazione (ID: {existingAssociation.DestinationId})";
transferResult.EntityId = existingAssociation.DestinationId;
// Aggiorna l'associazione con la data di ultimo aggiornamento
existingAssociation.UpdatedAt = DateTime.UtcNow;
await CredentialService.UpdateRecordAssociationAsync(existingAssociation);
Logger.LogDebug("Record aggiornato tramite associazione: {EntityId} per chiave sorgente {SourceKey}",
existingAssociation.DestinationId, sourceKey);
await CredentialService.DeleteKeyAssociationAsync(existingAssociation.Id);
Logger.LogInformation("ASSOCIATION DEBUG: Associazione non valida eliminata: {AssociationId}", existingAssociation.Id);
}
else
catch (Exception delEx)
{
// Se l'aggiornamento fallisce, prova a creare un nuovo record
Logger.LogWarning("Aggiornamento fallito per associazione {AssociationId}, provo a creare nuovo record", existingAssociation.Id);
goto CreateNewRecord;
Logger.LogWarning(delEx, "Errore nell'eliminazione dell'associazione non valida {AssociationId}", existingAssociation.Id);
}
transferResults.Add(transferResult);
recordNumber++;
continue;
transferResult.Status = "info";
transferResult.Message = $"Associazione non valida eliminata (aggiornamento fallito) - creazione nuovo record";
// Procedi con la creazione di un nuovo record (non aggiungere il result qui, sarà aggiunto dopo CreateNewRecord)
}
}
@@ -2212,31 +2245,41 @@
transferResult.Status = "success";
transferResult.Message = "Record inserito con successo";
transferResult.EntityId = result.ContainsKey("id") ? result["id"]?.ToString() :
result.ContainsKey("Id") ? result["Id"]?.ToString() : null;
result.ContainsKey("Id") ? result["Id"]?.ToString() :
result.ContainsKey("DocEntry") ? result["DocEntry"]?.ToString() : null;
// Crea associazione solo se abbiamo una chiave sorgente e un ID destinazione
if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey) && !string.IsNullOrEmpty(transferResult.EntityId))
{
try
{
var association = new RecordAssociation
// Determina i campi chiave automaticamente
var destinationKeyField = GetEntityIdField(); // Campo chiave nella destinazione
var association = new KeyAssociation
{
SourceName = currentSourceName,
SourceType = selectedSourceType,
SourceKey = sourceKey,
KeyValue = sourceKey,
SourceKeyField = sourceKeyField,
DestinationKeyField = destinationKeyField,
DestinationEntity = selectedRestEntity.Name,
DestinationId = transferResult.EntityId,
RestCredentialName = selectedRestCredential,
CreatedAt = DateTime.UtcNow,
LastVerifiedAt = DateTime.UtcNow,
AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(new
{
TransferDate = DateTime.UtcNow,
RecordNumber = recordNumber,
MappingCount = fieldMappings.Count
MappingCount = fieldMappings.Count,
SourceType = selectedSourceType
})
};
await CredentialService.SaveRecordAssociationAsync(association);
Logger.LogDebug("Associazione creata: {SourceKey} -> {DestinationId}", sourceKey, transferResult.EntityId);
Logger.LogInformation("ASSOCIATION DEBUG: Creazione nuova associazione - KeyValue: '{KeyValue}', Entity: '{Entity}', DestinationId: '{DestinationId}', Credential: '{Credential}'",
sourceKey, selectedRestEntity.Name, transferResult.EntityId, selectedRestCredential);
var associationId = await CredentialService.SaveKeyAssociationAsync(association);
Logger.LogInformation("DEBUG: Associazione salvata con ID: {AssociationId}", associationId);
}
catch (Exception assocEx)
{
@@ -2599,7 +2642,8 @@
throw new InvalidOperationException($"Il valore del campo chiave '{sourceKeyField}' è vuoto o null per questo record.");
}
return keyValue;
// Normalizza il valore della chiave (trim e gestione case-sensitive)
return keyValue.Trim();
}
catch (Exception ex)
{
@@ -1,27 +1,129 @@
@page "/record-associations"
@page "/key-associations"
@using CredentialManager.Models
@using CredentialManager.Services
@using DataConnection.CredentialManagement.Interfaces
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.JSInterop
@inject IDataConnectionCredentialService CredentialService
@inject IJSRuntime JSRuntime
@inject ILogger<RecordAssociations> Logger
@inject ILogger<KeyAssociations> Logger
<PageTitle>Associazioni Record</PageTitle>
<PageTitle>Gestione Associazioni Chiavi</PageTitle>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h3><i class="fas fa-link"></i> Associazioni Record</h3>
<p class="text-muted">Visualizza e gestisci le associazioni tra record sorgente e destinazione</p>
<h3><i class="fas fa-key"></i> Gestione Associazioni Chiavi</h3>
<p class="text-muted">
Visualizza e gestisci le associazioni basate sui valori delle chiavi.
Ogni associazione lega un valore di chiave univoco a un record di destinazione,
indipendentemente dalla sorgente che ha generato quel valore.
</p>
</div>
</div>
<!-- Statistiche -->
@if (statistics != null)
{
<div class="row mb-4">
<div class="col-md-2">
<div class="card bg-primary text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">@statistics.TotalAssociations</h4>
<p class="card-text">Totali</p>
</div>
<div class="align-self-center">
<i class="fas fa-link fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card bg-success text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">@statistics.ActiveAssociations</h4>
<p class="card-text">Attive</p>
</div>
<div class="align-self-center">
<i class="fas fa-check-circle fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card bg-warning text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">@statistics.InactiveAssociations</h4>
<p class="card-text">Disattive</p>
</div>
<div class="align-self-center">
<i class="fas fa-pause-circle fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card bg-info text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">@statistics.UniqueKeyValues</h4>
<p class="card-text">Chiavi Uniche</p>
</div>
<div class="align-self-center">
<i class="fas fa-key fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card bg-secondary text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">@statistics.UniqueDestinationEntities</h4>
<p class="card-text">Entità</p>
</div>
<div class="align-self-center">
<i class="fas fa-database fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card bg-dark text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="card-title">@(statistics.OldestAssociation?.ToString("dd/MM/yy") ?? "N/A")</h6>
<p class="card-text">Più Vecchia</p>
</div>
<div class="align-self-center">
<i class="fas fa-calendar fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
</div>
}
<!-- Filtri -->
<div class="row mb-4">
<div class="col-md-3">
<label class="form-label">Filtra per Sorgente:</label>
<input class="form-control" @bind="sourceFilter" @bind:event="oninput" @onkeyup="ApplyFilters" placeholder="Nome sorgente..." />
<label class="form-label">Filtra per Valore Chiave:</label>
<input class="form-control" @bind="keyValueFilter" @bind:event="oninput" @onkeyup="ApplyFilters" placeholder="Valore chiave..." />
</div>
<div class="col-md-3">
<label class="form-label">Filtra per Entità:</label>
@@ -50,36 +152,25 @@
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<h6><i class="fas fa-broom"></i> Pulizia Associazioni</h6>
<div class="d-grid gap-2">
<button class="btn btn-warning" @onclick="() => ShowClearConfirmation(false)" disabled="@isProcessing">
<i class="fas fa-trash-alt"></i> Pulisci Associazioni Filtrate
<div class="col-md-6">
<h6>Operazioni di Pulizia</h6>
<div class="btn-group me-3">
<button class="btn btn-warning" @onclick="ValidateAssociations" disabled="@isProcessing">
<i class="fas fa-check-double"></i> Valida Associazioni
</button>
<button class="btn btn-danger" @onclick="() => ShowClearConfirmation(true)" disabled="@isProcessing">
<i class="fas fa-trash"></i> Elimina TUTTE le Associazioni
<button class="btn btn-danger" @onclick="CleanupInvalidAssociations" disabled="@isProcessing">
<i class="fas fa-broom"></i> Pulisci Non Valide
</button>
</div>
</div>
<div class="col-md-4">
<h6><i class="fas fa-check-circle"></i> Validazione Associazioni</h6>
<div class="d-grid gap-2">
<button class="btn btn-info" @onclick="ValidateAllAssociations" disabled="@isProcessing">
<i class="fas fa-search"></i> Verifica Associazioni Non Valide
<div class="col-md-6">
<h6>Operazioni Avanzate</h6>
<div class="btn-group">
<button class="btn btn-info" @onclick="ExportAssociations" disabled="@isProcessing">
<i class="fas fa-download"></i> Esporta CSV
</button>
<button class="btn btn-warning" @onclick="CleanupInvalidAssociations" disabled="@isProcessing">
<i class="fas fa-broom"></i> Pulisci Associazioni Non Valide
</button>
</div>
</div>
<div class="col-md-4">
<h6><i class="fas fa-download"></i> Esportazione</h6>
<div class="d-grid gap-2">
<button class="btn btn-success" @onclick="ExportToCsv" disabled="@isProcessing">
<i class="fas fa-file-csv"></i> Esporta in CSV
</button>
<button class="btn btn-outline-primary" @onclick="ShowImportDialog" disabled="@isProcessing">
<i class="fas fa-upload"></i> Importa da CSV
<button class="btn btn-danger" @onclick="ClearAllAssociations" disabled="@isProcessing">
<i class="fas fa-trash-alt"></i> Elimina Tutte
</button>
</div>
</div>
@@ -89,18 +180,17 @@
{
<div class="mt-3">
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 100%">
@processingMessage
</div>
<div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 100%"></div>
</div>
<small class="text-muted">@processingMessage</small>
</div>
}
@if (!string.IsNullOrEmpty(operationMessage))
{
<div class="alert @(operationMessageType == "success" ? "alert-success" : operationMessageType == "warning" ? "alert-warning" : "alert-danger") mt-3">
<i class="fas @(operationMessageType == "success" ? "fa-check-circle" : operationMessageType == "warning" ? "fa-exclamation-triangle" : "fa-exclamation-circle")"></i>
<div class="alert alert-@operationMessageType alert-dismissible fade show mt-3" role="alert">
@operationMessage
<button type="button" class="btn-close" @onclick="() => operationMessage = string.Empty"></button>
</div>
}
</div>
@@ -108,70 +198,6 @@
</div>
</div>
<!-- Statistiche -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">@filteredAssociations.Count</h4>
<p class="card-text">Associazioni Totali</p>
</div>
<div class="align-self-center">
<i class="fas fa-link fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">@filteredAssociations.Where(a => a.IsActive).Count()</h4>
<p class="card-text">Attive</p>
</div>
<div class="align-self-center">
<i class="fas fa-check-circle fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">@filteredAssociations.Where(a => !a.IsActive).Count()</h4>
<p class="card-text">Disattivate</p>
</div>
<div class="align-self-center">
<i class="fas fa-pause-circle fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">@filteredAssociations.Select(a => a.SourceName).Distinct().Count()</h4>
<p class="card-text">Sorgenti Diverse</p>
</div>
<div class="align-self-center">
<i class="fas fa-database fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tabella Associazioni -->
@if (isLoading)
{
@@ -188,7 +214,7 @@
<i class="fas fa-info-circle"></i>
@if (!allAssociations.Any())
{
<span>Nessuna associazione trovata. Le associazioni vengono create automaticamente durante il trasferimento dati.</span>
<span>Nessuna associazione trovata. Le associazioni vengono create automaticamente durante il trasferimento dati quando il sistema di associazioni è abilitato.</span>
}
else
{
@@ -201,7 +227,7 @@
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-table"></i> Associazioni Record
<i class="fas fa-table"></i> Associazioni Chiavi
<span class="badge bg-primary ms-2">@filteredAssociations.Count</span>
</h5>
</div>
@@ -210,15 +236,15 @@
<table class="table table-striped table-hover mb-0">
<thead class="table-dark">
<tr>
<th>Sorgente</th>
<th>Tipo</th>
<th>Chiave Sorgente</th>
<th>Valore Chiave</th>
<th>Campo Sorgente</th>
<th>Campo Destinazione</th>
<th>Entità Destinazione</th>
<th>ID Destinazione</th>
<th>Credenziale REST</th>
<th>Credenziale</th>
<th>Stato</th>
<th>Creata</th>
<th>Aggiornata</th>
<th>Verificata</th>
<th>Azioni</th>
</tr>
</thead>
@@ -227,15 +253,13 @@
{
<tr class="@(association.IsActive ? "" : "table-secondary")">
<td>
<strong>@association.SourceName</strong>
<code class="small">@association.KeyValue</code>
</td>
<td>
<span class="badge @(association.SourceType == "database" ? "bg-primary" : "bg-info")">
@association.SourceType
</span>
<span class="badge bg-info">@association.SourceKeyField</span>
</td>
<td>
<code class="small">@association.SourceKey</code>
<span class="badge bg-secondary">@association.DestinationKeyField</span>
</td>
<td>
<strong>@association.DestinationEntity</strong>
@@ -267,32 +291,29 @@
</td>
<td>
<small class="text-muted">
@(association.UpdatedAt?.ToString("dd/MM/yyyy HH:mm") ?? "-")
@(association.LastVerifiedAt?.ToString("dd/MM/yyyy HH:mm") ?? "Mai")
</small>
</td>
<td>
<div class="btn-group btn-group-sm">
@if (association.IsActive)
{
<button class="btn btn-warning" @onclick="() => DeactivateAssociation(association.Id)" title="Disattiva">
<button class="btn btn-outline-warning" @onclick="() => DeactivateAssociation(association.Id)" title="Disattiva">
<i class="fas fa-pause"></i>
</button>
}
else
{
<button class="btn btn-success" @onclick="() => ActivateAssociation(association.Id)" title="Riattiva">
<button class="btn btn-outline-success" @onclick="() => ActivateAssociation(association.Id)" title="Riattiva">
<i class="fas fa-play"></i>
</button>
}
<button class="btn btn-danger" @onclick="() => DeleteAssociation(association.Id)" title="Elimina definitivamente">
<button class="btn btn-outline-info" @onclick="() => ShowAssociationDetails(association)" title="Dettagli">
<i class="fas fa-info"></i>
</button>
<button class="btn btn-outline-danger" @onclick="() => DeleteAssociation(association.Id)" title="Elimina">
<i class="fas fa-trash"></i>
</button>
@if (!string.IsNullOrEmpty(association.AdditionalInfo))
{
<button class="btn btn-info" @onclick="() => ShowAdditionalInfo(association)" title="Mostra dettagli">
<i class="fas fa-info"></i>
</button>
}
</div>
</td>
</tr>
@@ -304,116 +325,54 @@
</div>
<!-- Paginazione -->
@if (filteredAssociations.Count > pageSize)
@if (totalPages > 1)
{
<nav class="mt-3">
<nav aria-label="Paginazione associazioni" class="mt-3">
<ul class="pagination justify-content-center">
<li class="page-item @(currentPage == 1 ? "disabled" : "")">
<button class="page-link" @onclick="() => ChangePage(currentPage - 1)">Precedente</button>
<a class="page-link" @onclick="() => ChangePage(currentPage - 1)">Precedente</a>
</li>
@for (int i = Math.Max(1, currentPage - 2); i <= Math.Min(totalPages, currentPage + 2); i++)
{
var pageNum = i;
<li class="page-item @(currentPage == pageNum ? "active" : "")">
<button class="page-link" @onclick="() => ChangePage(pageNum)">@pageNum</button>
<li class="page-item @(i == currentPage ? "active" : "")">
<a class="page-link" @onclick="() => ChangePage(i)">@i</a>
</li>
}
<li class="page-item @(currentPage == totalPages ? "disabled" : "")">
<button class="page-link" @onclick="() => ChangePage(currentPage + 1)">Successivo</button>
<a class="page-link" @onclick="() => ChangePage(currentPage + 1)">Successiva</a>
</li>
</ul>
</nav>
}
}
<!-- Azioni di massa -->
@if (filteredAssociations.Any())
{
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h6 class="mb-0">Azioni di Massa</h6>
</div>
<div class="card-body">
<div class="btn-group">
<button class="btn btn-warning" @onclick="DeactivateAllInactive">
<i class="fas fa-pause"></i> Disattiva Tutte Inattive
</button>
<button class="btn btn-danger" @onclick="DeleteAllInactive">
<i class="fas fa-trash"></i> Elimina Tutte Disattivate
</button>
<button class="btn btn-info" @onclick="ExportAssociations">
<i class="fas fa-download"></i> Esporta CSV
</button>
</div>
</div>
</div>
</div>
</div>
}
</div>
<!-- Modal di Conferma -->
@if (showConfirmModal)
{
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-exclamation-triangle text-warning"></i> Conferma Eliminazione
</h5>
</div>
<div class="modal-body">
<p>@confirmMessage</p>
<div class="alert alert-warning">
<i class="fas fa-info-circle"></i>
<strong>Attenzione:</strong> Questa operazione eliminerà definitivamente le associazioni dal database e non potrà essere annullata.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CancelClearConfirmation">
<i class="fas fa-times"></i> Annulla
</button>
<button type="button" class="btn btn-danger" @onclick="ConfirmClearAssociations">
<i class="fas fa-trash"></i> Conferma Eliminazione
</button>
</div>
</div>
</div>
</div>
}
@code {
private List<RecordAssociation> allAssociations = new();
private List<RecordAssociation> filteredAssociations = new();
private List<RecordAssociation> pagedAssociations = new();
private bool isLoading = true;
// Dati
private List<KeyAssociation> allAssociations = new();
private List<KeyAssociation> filteredAssociations = new();
private List<KeyAssociation> pagedAssociations = new();
private AssociationStatistics? statistics;
// Filtri
private string sourceFilter = "";
private string keyValueFilter = "";
private string entityFilter = "";
private string credentialFilter = "";
// Paginazione
private int currentPage = 1;
private int pageSize = 25;
private int totalPages => (int)Math.Ceiling((double)filteredAssociations.Count / pageSize);
private int totalPages = 1;
// Gestione operazioni
// Stato
private bool isLoading = true;
private bool isProcessing = false;
private string processingMessage = "";
private string operationMessage = "";
private string operationMessageType = "";
// Modal di conferma
private bool showConfirmModal = false;
private bool isDeleteAll = false;
private string confirmMessage = "";
protected override async Task OnInitializedAsync()
{
await RefreshAssociations();
@@ -421,27 +380,32 @@
private async Task RefreshAssociations()
{
isLoading = true;
operationMessage = "";
StateHasChanged();
try
{
isLoading = true;
allAssociations = await CredentialService.GetAllActiveRecordAssociationsAsync();
allAssociations = await CredentialService.GetAllKeyAssociationsAsync();
statistics = await CredentialService.GetKeyAssociationStatisticsAsync();
ApplyFilters();
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nel caricamento delle associazioni");
await JSRuntime.InvokeVoidAsync("alert", $"Errore nel caricamento delle associazioni: {ex.Message}");
SetOperationMessage($"Errore nel caricamento: {ex.Message}", "danger");
}
finally
{
isLoading = false;
StateHasChanged();
}
}
private void ApplyFilters()
{
filteredAssociations = allAssociations.Where(a =>
(string.IsNullOrEmpty(sourceFilter) || a.SourceName.Contains(sourceFilter, StringComparison.OrdinalIgnoreCase)) &&
(string.IsNullOrEmpty(keyValueFilter) || a.KeyValue.Contains(keyValueFilter, StringComparison.OrdinalIgnoreCase)) &&
(string.IsNullOrEmpty(entityFilter) || a.DestinationEntity.Contains(entityFilter, StringComparison.OrdinalIgnoreCase)) &&
(string.IsNullOrEmpty(credentialFilter) || a.RestCredentialName.Contains(credentialFilter, StringComparison.OrdinalIgnoreCase))
).OrderByDescending(a => a.CreatedAt).ToList();
@@ -453,7 +417,7 @@
private void ClearFilters()
{
sourceFilter = "";
keyValueFilter = "";
entityFilter = "";
credentialFilter = "";
ApplyFilters();
@@ -470,6 +434,7 @@
private void UpdatePagedAssociations()
{
totalPages = (int)Math.Ceiling((double)filteredAssociations.Count / pageSize);
var startIndex = (currentPage - 1) * pageSize;
pagedAssociations = filteredAssociations.Skip(startIndex).Take(pageSize).ToList();
}
@@ -480,21 +445,21 @@
{
try
{
var success = await CredentialService.DeactivateRecordAssociationAsync(id);
var success = await CredentialService.DeactivateKeyAssociationAsync(id);
if (success)
{
await JSRuntime.InvokeVoidAsync("alert", "Associazione disattivata con successo!");
SetOperationMessage("Associazione disattivata con successo!", "success");
await RefreshAssociations();
}
else
{
await JSRuntime.InvokeVoidAsync("alert", "Errore nella disattivazione dell'associazione.");
SetOperationMessage("Errore nella disattivazione dell'associazione.", "danger");
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nella disattivazione dell'associazione {Id}", id);
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
SetOperationMessage($"Errore: {ex.Message}", "danger");
}
}
}
@@ -507,214 +472,79 @@
if (association != null)
{
association.IsActive = true;
var success = await CredentialService.UpdateRecordAssociationAsync(association);
association.UpdatedAt = DateTime.UtcNow;
var success = await CredentialService.UpdateKeyAssociationAsync(association);
if (success)
{
await JSRuntime.InvokeVoidAsync("alert", "Associazione riattivata con successo!");
SetOperationMessage("Associazione riattivata con successo!", "success");
await RefreshAssociations();
}
else
{
await JSRuntime.InvokeVoidAsync("alert", "Errore nella riattivazione dell'associazione.");
SetOperationMessage("Errore nella riattivazione dell'associazione.", "danger");
}
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nella riattivazione dell'associazione {Id}", id);
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
SetOperationMessage($"Errore: {ex.Message}", "danger");
}
}
private async Task DeleteAssociation(int id)
{
if (await JSRuntime.InvokeAsync<bool>("confirm", "Sei sicuro di voler eliminare definitivamente questa associazione? Questa azione non può essere annullata."))
if (await JSRuntime.InvokeAsync<bool>("confirm", "Sei sicuro di voler eliminare questa associazione? Questa operazione non può essere annullata."))
{
try
{
var success = await CredentialService.DeleteRecordAssociationAsync(id);
var success = await CredentialService.DeleteKeyAssociationAsync(id);
if (success)
{
await JSRuntime.InvokeVoidAsync("alert", "Associazione eliminata con successo!");
SetOperationMessage("Associazione eliminata con successo!", "success");
await RefreshAssociations();
}
else
{
await JSRuntime.InvokeVoidAsync("alert", "Errore nell'eliminazione dell'associazione.");
SetOperationMessage("Errore nell'eliminazione dell'associazione.", "danger");
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nell'eliminazione dell'associazione {Id}", id);
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
SetOperationMessage($"Errore: {ex.Message}", "danger");
}
}
}
private async Task ShowAdditionalInfo(RecordAssociation association)
private async Task ShowAssociationDetails(KeyAssociation association)
{
var info = $"Informazioni aggiuntive per l'associazione:\n\n";
var info = $"Dettagli associazione:\n\n";
info += $"ID: {association.Id}\n";
info += $"Sorgente: {association.SourceName} ({association.SourceType})\n";
info += $"Chiave Sorgente: {association.SourceKey}\n";
info += $"Destinazione: {association.DestinationEntity}\n";
info += $"Valore Chiave: {association.KeyValue}\n";
info += $"Campo Sorgente: {association.SourceKeyField}\n";
info += $"Campo Destinazione: {association.DestinationKeyField}\n";
info += $"Entità: {association.DestinationEntity}\n";
info += $"ID Destinazione: {association.DestinationId}\n";
info += $"Credenziale REST: {association.RestCredentialName}\n";
info += $"Creata: {association.CreatedAt}\n";
info += $"Credenziale: {association.RestCredentialName}\n";
info += $"Creata: {association.CreatedAt:dd/MM/yyyy HH:mm}\n";
if (association.UpdatedAt.HasValue)
info += $"Aggiornata: {association.UpdatedAt}\n";
info += $"Aggiornata: {association.UpdatedAt:dd/MM/yyyy HH:mm}\n";
if (association.LastVerifiedAt.HasValue)
info += $"Verificata: {association.LastVerifiedAt:dd/MM/yyyy HH:mm}\n";
info += $"Stato: {(association.IsActive ? "Attiva" : "Disattivata")}\n";
if (!string.IsNullOrEmpty(association.SourcesInfo))
info += $"\nSorgenti:\n{association.SourcesInfo}\n";
if (!string.IsNullOrEmpty(association.AdditionalInfo))
info += $"\nInformazioni aggiuntive:\n{association.AdditionalInfo}";
await JSRuntime.InvokeVoidAsync("alert", info);
}
private async Task DeactivateAllInactive()
{
if (await JSRuntime.InvokeAsync<bool>("confirm", "Sei sicuro di voler disattivare tutte le associazioni che non sono attualmente in uso?"))
{
try
{
// Implementa logica per disattivare associazioni inattive
await JSRuntime.InvokeVoidAsync("alert", "Funzionalità in via di sviluppo.");
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nella disattivazione di massa");
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
}
}
}
private async Task DeleteAllInactive()
{
if (await JSRuntime.InvokeAsync<bool>("confirm", "Sei sicuro di voler eliminare definitivamente tutte le associazioni disattivate? Questa azione non può essere annullata."))
{
try
{
var inactiveAssociations = allAssociations.Where(a => !a.IsActive).ToList();
var deletedCount = 0;
foreach (var association in inactiveAssociations)
{
if (await CredentialService.DeleteRecordAssociationAsync(association.Id))
{
deletedCount++;
}
}
await JSRuntime.InvokeVoidAsync("alert", $"Eliminate {deletedCount} associazioni disattivate.");
await RefreshAssociations();
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nell'eliminazione di massa");
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
}
}
}
private async Task ExportAssociations()
{
try
{
var csv = "Sorgente,Tipo,Chiave Sorgente,Entità Destinazione,ID Destinazione,Credenziale REST,Stato,Creata,Aggiornata\n";
foreach (var association in filteredAssociations)
{
csv += $"\"{association.SourceName}\",\"{association.SourceType}\",\"{association.SourceKey}\",";
csv += $"\"{association.DestinationEntity}\",\"{association.DestinationId}\",\"{association.RestCredentialName}\",";
csv += $"\"{(association.IsActive ? "Attiva" : "Disattivata")}\",\"{association.CreatedAt:dd/MM/yyyy HH:mm}\",";
csv += $"\"{(association.UpdatedAt?.ToString("dd/MM/yyyy HH:mm") ?? "")}\"\n";
}
var fileName = $"associazioni_record_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
var bytes = System.Text.Encoding.UTF8.GetBytes(csv);
var base64 = Convert.ToBase64String(bytes);
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, base64);
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nell'esportazione delle associazioni");
await JSRuntime.InvokeVoidAsync("alert", $"Errore nell'esportazione: {ex.Message}");
}
}
private void ShowClearConfirmation(bool deleteAll)
{
isDeleteAll = deleteAll;
if (deleteAll)
{
confirmMessage = "Sei sicuro di voler eliminare TUTTE le associazioni dal sistema? Questa operazione non può essere annullata.";
}
else
{
var filteredCount = filteredAssociations.Count;
if (filteredCount == 0)
{
SetOperationMessage("Nessuna associazione da eliminare con i filtri attuali.", "warning");
return;
}
confirmMessage = $"Sei sicuro di voler eliminare {filteredCount} associazioni filtrate? Questa operazione non può essere annullata.";
}
showConfirmModal = true;
StateHasChanged();
}
private async Task ConfirmClearAssociations()
{
showConfirmModal = false;
isProcessing = true;
try
{
int deletedCount = 0;
if (isDeleteAll)
{
processingMessage = "Eliminazione di tutte le associazioni...";
StateHasChanged();
deletedCount = await CredentialService.ClearAllRecordAssociationsAsync();
SetOperationMessage($"Eliminate tutte le {deletedCount} associazioni dal sistema.", "success");
}
else
{
processingMessage = "Eliminazione associazioni filtrate...";
StateHasChanged();
// Elimina le associazioni filtrate una per una
foreach (var association in filteredAssociations.ToList())
{
await CredentialService.DeleteRecordAssociationAsync(association.Id);
deletedCount++;
}
SetOperationMessage($"Eliminate {deletedCount} associazioni filtrate.", "success");
}
await RefreshAssociations();
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nell'eliminazione delle associazioni");
SetOperationMessage($"Errore nell'eliminazione: {ex.Message}", "danger");
}
finally
{
isProcessing = false;
processingMessage = "";
StateHasChanged();
}
}
private void CancelClearConfirmation()
{
showConfirmModal = false;
StateHasChanged();
}
private async Task ValidateAllAssociations()
private async Task ValidateAssociations()
{
isProcessing = true;
processingMessage = "Validazione associazioni in corso...";
@@ -729,7 +559,7 @@
foreach (var group in uniqueDestinations)
{
var invalidAssociations = await CredentialService.GetInvalidRecordAssociationsAsync(
var invalidAssociations = await CredentialService.GetInvalidKeyAssociationsAsync(
group.Key.DestinationEntity,
group.Key.RestCredentialName);
invalidCount += invalidAssociations.Count;
@@ -741,7 +571,7 @@
}
else
{
SetOperationMessage($"Trovate {invalidCount} associazioni con ID di destinazione non più validi. Usa 'Pulisci Associazioni Non Valide' per rimuoverle.", "warning");
SetOperationMessage($"Trovate {invalidCount} associazioni con ID di destinazione non più validi. Usa 'Pulisci Non Valide' per rimuoverle.", "warning");
}
}
catch (Exception ex)
@@ -772,7 +602,7 @@
foreach (var group in uniqueDestinations)
{
var cleanedCount = await CredentialService.CleanupInvalidRecordAssociationsAsync(
var cleanedCount = await CredentialService.CleanupInvalidKeyAssociationsAsync(
group.Key.DestinationEntity,
group.Key.RestCredentialName);
totalCleaned += cleanedCount;
@@ -784,13 +614,13 @@
}
else
{
SetOperationMessage($"Pulite {totalCleaned} associazioni con ID di destinazione non più validi.", "success");
SetOperationMessage($"Pulite {totalCleaned} associazioni non valide!", "success");
await RefreshAssociations();
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nella pulizia delle associazioni non valide");
Logger.LogError(ex, "Errore nella pulizia delle associazioni");
SetOperationMessage($"Errore nella pulizia: {ex.Message}", "danger");
}
finally
@@ -801,50 +631,77 @@
}
}
private async Task ExportToCsv()
private async Task ExportAssociations()
{
isProcessing = true;
processingMessage = "Esportazione in corso...";
StateHasChanged();
try
{
await ExportAssociations();
SetOperationMessage($"Esportate {filteredAssociations.Count} associazioni in CSV.", "success");
var csv = "Valore Chiave,Campo Sorgente,Campo Destinazione,Entità Destinazione,ID Destinazione,Credenziale,Stato,Creata,Aggiornata,Verificata\n";
foreach (var association in filteredAssociations)
{
csv += $"\"{association.KeyValue}\",\"{association.SourceKeyField}\",\"{association.DestinationKeyField}\",";
csv += $"\"{association.DestinationEntity}\",\"{association.DestinationId}\",\"{association.RestCredentialName}\",";
csv += $"\"{(association.IsActive ? "Attiva" : "Disattivata")}\",\"{association.CreatedAt:dd/MM/yyyy HH:mm}\",";
csv += $"\"{(association.UpdatedAt?.ToString("dd/MM/yyyy HH:mm") ?? "")}\",";
csv += $"\"{(association.LastVerifiedAt?.ToString("dd/MM/yyyy HH:mm") ?? "")}\"\n";
}
var bytes = System.Text.Encoding.UTF8.GetBytes(csv);
var fileName = $"associazioni_chiavi_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, "text/csv", System.Convert.ToBase64String(bytes));
SetOperationMessage($"File {fileName} esportato con successo!", "success");
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nell'esportazione");
SetOperationMessage($"Errore nell'esportazione: {ex.Message}", "danger");
}
finally
{
isProcessing = false;
processingMessage = "";
StateHasChanged();
}
}
private async Task ShowImportDialog()
private async Task ClearAllAssociations()
{
// Placeholder per import dialog
await JSRuntime.InvokeVoidAsync("alert", "Funzionalità di importazione non ancora implementata.");
if (await JSRuntime.InvokeAsync<bool>("confirm", "ATTENZIONE: Questa operazione eliminerà TUTTE le associazioni dal sistema. Sei assolutamente sicuro di voler procedere?"))
{
try
{
isProcessing = true;
processingMessage = "Eliminazione di tutte le associazioni...";
StateHasChanged();
var deletedCount = await CredentialService.ClearAllKeyAssociationsAsync();
SetOperationMessage($"Eliminate {deletedCount} associazioni dal sistema!", "success");
await RefreshAssociations();
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nell'eliminazione di massa");
SetOperationMessage($"Errore: {ex.Message}", "danger");
}
finally
{
isProcessing = false;
processingMessage = "";
StateHasChanged();
}
}
}
private void SetOperationMessage(string message, string type)
{
operationMessage = message;
operationMessageType = type;
StateHasChanged();
// Auto-hide success messages after 5 seconds
if (type == "success")
{
_ = Task.Delay(5000).ContinueWith(_ =>
{
operationMessage = "";
StateHasChanged();
});
}
}
}
<script>
window.downloadFile = function (fileName, contentType, data) {
const link = document.createElement('a');
link.download = fileName;
link.href = 'data:' + contentType + ';base64,' + data;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
</script>
+316
View File
@@ -0,0 +1,316 @@
@page "/test-associations"
@using CredentialManager.Models
@using DataConnection.CredentialManagement.Interfaces
@inject IDataConnectionCredentialService CredentialService
@inject ILogger<TestAssociations> Logger
<PageTitle>Test Associazioni</PageTitle>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h3><i class="fas fa-vial"></i> Test Sistema Associazioni Chiave</h3>
<div class="row mt-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5><i class="fas fa-plus"></i> Crea Associazione Test</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Valore Chiave:</label>
<input type="text" class="form-control" @bind="testKeyValue" placeholder="es. 12345" />
</div>
<div class="mb-3">
<label class="form-label">Entità Destinazione:</label>
<input type="text" class="form-control" @bind="testDestinationEntity" placeholder="es. BusinessPartners" />
</div>
<div class="mb-3">
<label class="form-label">ID Destinazione:</label>
<input type="text" class="form-control" @bind="testDestinationId" placeholder="es. BP001" />
</div>
<div class="mb-3">
<label class="form-label">Credenziale REST:</label>
<input type="text" class="form-control" @bind="testRestCredential" placeholder="es. SAP_B1" />
</div>
<button class="btn btn-primary" @onclick="CreateTestAssociation">
<i class="fas fa-save"></i> Crea Associazione
</button>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5><i class="fas fa-search"></i> Cerca Associazione</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Valore Chiave da Cercare:</label>
<input type="text" class="form-control" @bind="searchKeyValue" placeholder="es. 12345" />
</div>
<div class="mb-3">
<label class="form-label">Entità Destinazione:</label>
<input type="text" class="form-control" @bind="searchDestinationEntity" placeholder="es. BusinessPartners" />
</div>
<div class="mb-3">
<label class="form-label">Credenziale REST:</label>
<input type="text" class="form-control" @bind="searchRestCredential" placeholder="es. SAP_B1" />
</div>
<button class="btn btn-info" @onclick="SearchAssociation">
<i class="fas fa-search"></i> Cerca
</button>
</div>
</div>
</div>
</div>
@if (!string.IsNullOrEmpty(resultMessage))
{
<div class="alert @(resultType == "success" ? "alert-success" : resultType == "error" ? "alert-danger" : "alert-info") mt-4">
<i class="fas @(resultType == "success" ? "fa-check-circle" : resultType == "error" ? "fa-exclamation-triangle" : "fa-info-circle")"></i>
@resultMessage
</div>
}
@if (foundAssociation != null)
{
<div class="card mt-4">
<div class="card-header">
<h5><i class="fas fa-link"></i> Associazione Trovata</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<strong>ID:</strong> @foundAssociation.Id<br>
<strong>Valore Chiave:</strong> @foundAssociation.KeyValue<br>
<strong>Campo Chiave Sorgente:</strong> @foundAssociation.SourceKeyField<br>
<strong>Campo Chiave Destinazione:</strong> @foundAssociation.DestinationKeyField<br>
</div>
<div class="col-md-6">
<strong>Entità Destinazione:</strong> @foundAssociation.DestinationEntity<br>
<strong>ID Destinazione:</strong> @foundAssociation.DestinationId<br>
<strong>Credenziale REST:</strong> @foundAssociation.RestCredentialName<br>
<strong>Attiva:</strong> @(foundAssociation.IsActive ? "Sì" : "No")<br>
</div>
</div>
<div class="row mt-2">
<div class="col-12">
<strong>Creata:</strong> @foundAssociation.CreatedAt.ToString("dd/MM/yyyy HH:mm:ss")<br>
@if (foundAssociation.UpdatedAt.HasValue)
{
<strong>Aggiornata:</strong> @foundAssociation.UpdatedAt.Value.ToString("dd/MM/yyyy HH:mm:ss")<br>
}
@if (foundAssociation.LastVerifiedAt.HasValue)
{
<strong>Ultima Verifica:</strong> @foundAssociation.LastVerifiedAt.Value.ToString("dd/MM/yyyy HH:mm:ss")<br>
}
</div>
</div>
</div>
</div>
}
<div class="card mt-4">
<div class="card-header">
<h5><i class="fas fa-list"></i> Tutte le Associazioni Attive</h5>
</div>
<div class="card-body">
<button class="btn btn-secondary mb-3" @onclick="LoadAllAssociations">
<i class="fas fa-refresh"></i> Carica Associazioni
</button>
@if (allAssociations != null && allAssociations.Any())
{
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Valore Chiave</th>
<th>Entità</th>
<th>ID Destinazione</th>
<th>Credenziale</th>
<th>Creata</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
@foreach (var assoc in allAssociations)
{
<tr>
<td>@assoc.Id</td>
<td>@assoc.KeyValue</td>
<td>@assoc.DestinationEntity</td>
<td>@assoc.DestinationId</td>
<td>@assoc.RestCredentialName</td>
<td>@assoc.CreatedAt.ToString("dd/MM/yyyy HH:mm")</td>
<td>
<button class="btn btn-sm btn-danger" @onclick="() => DeleteAssociation(assoc.Id)">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
}
else if (allAssociations != null)
{
<div class="alert alert-info">
<i class="fas fa-info-circle"></i> Nessuna associazione trovata.
</div>
}
</div>
</div>
</div>
</div>
</div>
@code {
private string testKeyValue = "";
private string testDestinationEntity = "";
private string testDestinationId = "";
private string testRestCredential = "";
private string searchKeyValue = "";
private string searchDestinationEntity = "";
private string searchRestCredential = "";
private string resultMessage = "";
private string resultType = "";
private KeyAssociation? foundAssociation;
private List<KeyAssociation>? allAssociations;
private async Task CreateTestAssociation()
{
try
{
if (string.IsNullOrEmpty(testKeyValue) || string.IsNullOrEmpty(testDestinationEntity) ||
string.IsNullOrEmpty(testDestinationId) || string.IsNullOrEmpty(testRestCredential))
{
resultMessage = "Tutti i campi sono obbligatori.";
resultType = "error";
return;
}
var association = new KeyAssociation
{
KeyValue = testKeyValue,
SourceKeyField = "test_field",
DestinationKeyField = "id",
DestinationEntity = testDestinationEntity,
DestinationId = testDestinationId,
RestCredentialName = testRestCredential,
CreatedAt = DateTime.UtcNow,
LastVerifiedAt = DateTime.UtcNow,
AdditionalInfo = "{\"test\": true}"
};
var id = await CredentialService.SaveKeyAssociationAsync(association);
resultMessage = $"Associazione creata con successo! ID: {id}";
resultType = "success";
Logger.LogInformation("Associazione test creata: ID={Id}, KeyValue={KeyValue}", id, testKeyValue);
}
catch (Exception ex)
{
resultMessage = $"Errore nella creazione: {ex.Message}";
resultType = "error";
Logger.LogError(ex, "Errore nella creazione dell'associazione test");
}
}
private async Task SearchAssociation()
{
try
{
if (string.IsNullOrEmpty(searchKeyValue))
{
resultMessage = "Valore chiave obbligatorio per la ricerca.";
resultType = "error";
return;
}
foundAssociation = null;
if (!string.IsNullOrEmpty(searchDestinationEntity) && !string.IsNullOrEmpty(searchRestCredential))
{
foundAssociation = await CredentialService.FindKeyAssociationByValueAsync(
searchKeyValue, searchDestinationEntity, searchRestCredential);
}
else
{
foundAssociation = await CredentialService.FindKeyAssociationByValueAsync(searchKeyValue);
}
if (foundAssociation != null)
{
resultMessage = "Associazione trovata!";
resultType = "success";
Logger.LogInformation("Associazione trovata: ID={Id} per KeyValue={KeyValue}", foundAssociation.Id, searchKeyValue);
}
else
{
resultMessage = "Nessuna associazione trovata con i criteri specificati.";
resultType = "info";
Logger.LogInformation("Nessuna associazione trovata per KeyValue={KeyValue}", searchKeyValue);
}
}
catch (Exception ex)
{
resultMessage = $"Errore nella ricerca: {ex.Message}";
resultType = "error";
Logger.LogError(ex, "Errore nella ricerca dell'associazione");
}
}
private async Task LoadAllAssociations()
{
try
{
allAssociations = await CredentialService.GetAllActiveKeyAssociationsAsync();
resultMessage = $"Caricate {allAssociations.Count} associazioni attive.";
resultType = "info";
}
catch (Exception ex)
{
resultMessage = $"Errore nel caricamento: {ex.Message}";
resultType = "error";
Logger.LogError(ex, "Errore nel caricamento delle associazioni");
}
}
private async Task DeleteAssociation(int id)
{
try
{
var result = await CredentialService.DeleteKeyAssociationAsync(id);
if (result)
{
resultMessage = $"Associazione {id} eliminata con successo.";
resultType = "success";
await LoadAllAssociations(); // Ricarica la lista
}
else
{
resultMessage = $"Errore nell'eliminazione dell'associazione {id}.";
resultType = "error";
}
}
catch (Exception ex)
{
resultMessage = $"Errore nell'eliminazione: {ex.Message}";
resultType = "error";
Logger.LogError(ex, "Errore nell'eliminazione dell'associazione {Id}", id);
}
}
protected override async Task OnInitializedAsync()
{
await LoadAllAssociations();
}
}
+6 -4
View File
@@ -18,11 +18,13 @@
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</NavLink>
</div> <div class="nav-item px-3">
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink>
</div> <div class="nav-item px-3">
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="credentials">
<span class="oi oi-key" aria-hidden="true"></span> Gestione Credenziali
</NavLink>
@@ -33,8 +35,8 @@
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="record-associations">
<span class="oi oi-link-intact" aria-hidden="true"></span> Associazioni Record
<NavLink class="nav-link" href="key-associations">
<span class="oi oi-link-intact" aria-hidden="true"></span> Gestione Associazioni Chiave
</NavLink>
</div>
</nav>
Binary file not shown.
Binary file not shown.
Binary file not shown.