Aggiornamento staging #11
@@ -25,6 +25,7 @@ public partial class ProfileSaver
|
||||
[Parameter] public string? DestinationTable { get; set; }
|
||||
[Parameter] public string? DestinationEndpoint { get; set; }
|
||||
[Parameter] public List<FieldMappingDto>? FieldMappings { get; set; }
|
||||
[Parameter] public List<ExternalIdRelationshipDto>? ExternalIdRelationships { get; set; }
|
||||
[Parameter] public string? SourceKeyField { get; set; }
|
||||
[Parameter] public bool UseRecordAssociations { get; set; }
|
||||
[Parameter] public EventCallback<DataCouplerProfileDto> OnProfileSaved { get; set; }
|
||||
@@ -78,6 +79,7 @@ public partial class ProfileSaver
|
||||
DestinationTable = DestinationTable,
|
||||
DestinationEndpoint = DestinationEndpoint,
|
||||
FieldMappings = FieldMappings,
|
||||
ExternalIdRelationships = ExternalIdRelationships,
|
||||
SourceKeyField = SourceKeyField,
|
||||
UseRecordAssociations = UseRecordAssociations
|
||||
};
|
||||
|
||||
+597
@@ -0,0 +1,597 @@
|
||||
// <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.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(CredentialDbContext))]
|
||||
[Migration("20260215151630_AddExternalIdRelationships")]
|
||||
partial class AddExternalIdRelationships
|
||||
{
|
||||
/// <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<string>("OdbcDsnName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OdbcMode")
|
||||
.HasMaxLength(20)
|
||||
.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.DataCouplerProfile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeletionAction")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeletionMarkField")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeletionMarkValue")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("DestinationCredentialId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DestinationEndpoint")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationSchema")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationTable")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ExternalIdRelationshipsJson")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FieldMappingJson")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<DateTime?>("LastUsedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("SourceCredentialId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SourceCustomQuery")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceDatabaseName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceKeyField")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceSchema")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceTable")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("SyncDeletions")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("UseRecordAssociations")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("DestinationCredentialId");
|
||||
|
||||
b.HasIndex("DestinationType");
|
||||
|
||||
b.HasIndex("IsActive");
|
||||
|
||||
b.HasIndex("LastUsedAt");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("SourceCredentialId");
|
||||
|
||||
b.HasIndex("SourceType");
|
||||
|
||||
b.ToTable("DataCouplerProfiles", (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>("Data_Hash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("DeletionSynced")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("DeletionSyncedAt")
|
||||
.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<bool>("IsSourceDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("KeyValue")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("LastVerifiedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MappedDestinationField")
|
||||
.HasMaxLength(200)
|
||||
.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);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CredentialManager.Models.ProfileSchedule", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DailyTime")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("DayOfMonth")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("DayOfWeek")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationDatabaseOverride")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EnableDeletionSync")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ExecutionCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("IntervalUnit")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("IntervalValue")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("LastExecutionMessage")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("LastExecutionRecordCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("LastExecutionStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("LastExecutionTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("NextExecutionTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ProfileId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ScheduleType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ScheduledDateTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceDatabaseOverride")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProfileId");
|
||||
|
||||
b.ToTable("ProfileSchedules");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CredentialManager.Models.ScheduleExecutionHistory", 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>("DestinationInfo")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("EndTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ErrorDetails")
|
||||
.HasMaxLength(5000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ProfileId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ProfileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("RecordsProcessed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("RecordsWithErrors")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ScheduleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SourceInfo")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("StartTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TriggerType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TriggeredBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProfileId");
|
||||
|
||||
b.HasIndex("ScheduleId");
|
||||
|
||||
b.HasIndex("StartTime");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("TriggerType");
|
||||
|
||||
b.ToTable("ScheduleExecutionHistories", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b =>
|
||||
{
|
||||
b.HasOne("CredentialManager.Models.CredentialEntity", "DestinationCredential")
|
||||
.WithMany()
|
||||
.HasForeignKey("DestinationCredentialId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("CredentialManager.Models.CredentialEntity", "SourceCredential")
|
||||
.WithMany()
|
||||
.HasForeignKey("SourceCredentialId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("DestinationCredential");
|
||||
|
||||
b.Navigation("SourceCredential");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CredentialManager.Models.ProfileSchedule", b =>
|
||||
{
|
||||
b.HasOne("CredentialManager.Models.DataCouplerProfile", "Profile")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProfileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Profile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CredentialManager.Models.ScheduleExecutionHistory", b =>
|
||||
{
|
||||
b.HasOne("CredentialManager.Models.ProfileSchedule", "Schedule")
|
||||
.WithMany()
|
||||
.HasForeignKey("ScheduleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Schedule");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CredentialManager.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddExternalIdRelationships : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ExternalIdRelationshipsJson",
|
||||
table: "DataCouplerProfiles",
|
||||
type: "TEXT",
|
||||
maxLength: 4000,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ExternalIdRelationshipsJson",
|
||||
table: "DataCouplerProfiles");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace CredentialManager.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
|
||||
|
||||
modelBuilder.Entity("CredentialManager.Models.CredentialEntity", b =>
|
||||
{
|
||||
@@ -182,6 +182,10 @@ namespace CredentialManager.Migrations
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ExternalIdRelationshipsJson")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FieldMappingJson")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -174,13 +174,51 @@ public static class ConnectionStringBuilder
|
||||
};
|
||||
} private static string BuildSqlServerConnectionString(DatabaseCredential credential)
|
||||
{
|
||||
var builder = new List<string>
|
||||
var builder = new List<string>();
|
||||
|
||||
// Gestione speciale per SQL Server locale e named instances
|
||||
// Se l'host contiene '\' (instance name) o '(localdb)', non aggiungere la porta
|
||||
bool hasInstanceName = credential.Host.Contains('\\') ||
|
||||
credential.Host.StartsWith("(localdb)", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (hasInstanceName)
|
||||
{
|
||||
$"Server={credential.Host},{credential.Port}",
|
||||
$"User Id={credential.Username}",
|
||||
$"Password={credential.Password}",
|
||||
$"Connection Timeout={credential.CommandTimeout}"
|
||||
};
|
||||
// Per named instances e LocalDB, non includere la porta
|
||||
builder.Add($"Server={credential.Host}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Per connessioni TCP/IP standard, include host e porta
|
||||
// Ma solo se la porta non è la default (1433) per localhost
|
||||
if ((credential.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase) ||
|
||||
credential.Host == "." ||
|
||||
credential.Host == "127.0.0.1") && credential.Port == 1433)
|
||||
{
|
||||
// Per localhost con porta default, ometti la porta per usare Named Pipes
|
||||
builder.Add($"Server={credential.Host}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Per altri casi, usa host,porta
|
||||
builder.Add($"Server={credential.Host},{credential.Port}");
|
||||
}
|
||||
}
|
||||
|
||||
// Se username è vuoto o è "Integrated", usa Windows Authentication
|
||||
if (string.IsNullOrWhiteSpace(credential.Username) ||
|
||||
credential.Username.Equals("Integrated", StringComparison.OrdinalIgnoreCase) ||
|
||||
credential.Username.Equals("Windows", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
builder.Add("Integrated Security=True");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Usa SQL Server Authentication
|
||||
builder.Add($"User Id={credential.Username}");
|
||||
builder.Add($"Password={credential.Password}");
|
||||
}
|
||||
|
||||
builder.Add($"Connection Timeout={credential.CommandTimeout}");
|
||||
|
||||
// Aggiungi Database solo se specificato
|
||||
if (!string.IsNullOrEmpty(credential.DatabaseName))
|
||||
|
||||
@@ -60,6 +60,10 @@ public class DataCouplerProfile
|
||||
[MaxLength(4000)]
|
||||
public string? FieldMappingJson { get; set; }
|
||||
|
||||
// External ID Relationships per Salesforce salvate come JSON
|
||||
[MaxLength(4000)]
|
||||
public string? ExternalIdRelationshipsJson { get; set; }
|
||||
|
||||
// Configurazione chiave sorgente e associazioni
|
||||
[MaxLength(200)]
|
||||
public string? SourceKeyField { get; set; }
|
||||
|
||||
@@ -30,6 +30,9 @@ public class DataCouplerProfileDto
|
||||
// Mapping dei campi
|
||||
public List<FieldMappingDto>? FieldMappings { get; set; }
|
||||
|
||||
// External ID Relationships per Salesforce
|
||||
public List<ExternalIdRelationshipDto>? ExternalIdRelationships { get; set; }
|
||||
|
||||
// Configurazione chiave sorgente e associazioni
|
||||
public string? SourceKeyField { get; set; }
|
||||
public bool UseRecordAssociations { get; set; }
|
||||
@@ -47,6 +50,37 @@ public class FieldMappingDto
|
||||
public bool IsRequired { get; set; }
|
||||
public string? DefaultValue { get; set; }
|
||||
public string? Transformation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Lista di relazioni External ID associate a questo campo (per Salesforce)
|
||||
/// </summary>
|
||||
public List<ExternalIdRelationshipDto>? ExternalIdRelationships { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO per External ID Relationship (Salesforce)
|
||||
/// </summary>
|
||||
public class ExternalIdRelationshipDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Nome della relazione (es. "Account__r")
|
||||
/// </summary>
|
||||
public string RelationshipName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Nome dell'oggetto correlato (es. "Account")
|
||||
/// </summary>
|
||||
public string RelatedObjectName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Campo External ID dell'oggetto correlato (es. "Country__c")
|
||||
/// </summary>
|
||||
public string ExternalIdField { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Campo sorgente da cui prendere il valore per l'External ID
|
||||
/// </summary>
|
||||
public string SourceField { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -109,6 +109,7 @@ public class DataCouplerProfileService : IDataCouplerProfileService
|
||||
existingProfile.DestinationTable = profile.DestinationTable;
|
||||
existingProfile.DestinationEndpoint = profile.DestinationEndpoint;
|
||||
existingProfile.FieldMappingJson = profile.FieldMappingJson;
|
||||
existingProfile.ExternalIdRelationshipsJson = profile.ExternalIdRelationshipsJson;
|
||||
existingProfile.SourceKeyField = profile.SourceKeyField;
|
||||
existingProfile.UseRecordAssociations = profile.UseRecordAssociations;
|
||||
existingProfile.IsActive = profile.IsActive;
|
||||
@@ -201,6 +202,41 @@ public class DataCouplerProfileService : IDataCouplerProfileService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializza la lista di External ID Relationships in JSON
|
||||
/// </summary>
|
||||
public string SerializeExternalIdRelationships(List<ExternalIdRelationshipDto>? relationships)
|
||||
{
|
||||
if (relationships == null || !relationships.Any())
|
||||
return string.Empty;
|
||||
|
||||
return JsonSerializer.Serialize(relationships, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializza il JSON delle External ID Relationships
|
||||
/// </summary>
|
||||
public List<ExternalIdRelationshipDto> DeserializeExternalIdRelationships(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return new List<ExternalIdRelationshipDto>();
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<ExternalIdRelationshipDto>>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
}) ?? new List<ExternalIdRelationshipDto>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<ExternalIdRelationshipDto>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converte un DataCouplerProfile in DTO
|
||||
/// </summary>
|
||||
@@ -226,6 +262,7 @@ public class DataCouplerProfileService : IDataCouplerProfileService
|
||||
DestinationTable = profile.DestinationTable,
|
||||
DestinationEndpoint = profile.DestinationEndpoint,
|
||||
FieldMappings = DeserializeFieldMappings(profile.FieldMappingJson),
|
||||
ExternalIdRelationships = DeserializeExternalIdRelationships(profile.ExternalIdRelationshipsJson),
|
||||
SourceKeyField = profile.SourceKeyField,
|
||||
UseRecordAssociations = profile.UseRecordAssociations
|
||||
};
|
||||
@@ -254,6 +291,7 @@ public class DataCouplerProfileService : IDataCouplerProfileService
|
||||
DestinationTable = dto.DestinationTable,
|
||||
DestinationEndpoint = dto.DestinationEndpoint,
|
||||
FieldMappingJson = SerializeFieldMappings(dto.FieldMappings),
|
||||
ExternalIdRelationshipsJson = SerializeExternalIdRelationships(dto.ExternalIdRelationships),
|
||||
SourceKeyField = dto.SourceKeyField,
|
||||
UseRecordAssociations = dto.UseRecordAssociations,
|
||||
CreatedBy = createdBy
|
||||
|
||||
@@ -146,6 +146,19 @@ public partial class DataCoupler : ComponentBase
|
||||
isRestConnected = true;
|
||||
|
||||
Logger.LogInformation("Discovery batch completato: trovate {EntityCount} entità REST", restEntities.Count);
|
||||
|
||||
// Carica anche i dettagli completi delle entità per External ID Relationships
|
||||
try
|
||||
{
|
||||
Logger.LogInformation("Caricamento dettagli entità per External ID Relationships...");
|
||||
availableRelationshipObjects = await currentRestDiscovery.DiscoverEntitiesAsync();
|
||||
Logger.LogInformation("Caricati {Count} oggetti disponibili per External ID Relationships", availableRelationshipObjects.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Impossibile caricare i dettagli delle entità per External ID Relationships");
|
||||
availableRelationshipObjects = new List<RestEntityInfo>();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -474,12 +474,27 @@ else
|
||||
<label class="form-label">Host/Server *</label>
|
||||
<InputText class="form-control" @bind-Value="currentDatabaseCredential.Host"
|
||||
placeholder="es. localhost o server.dominio.com" />
|
||||
@if (currentDatabaseCredential.DatabaseType == DatabaseType.SqlServer)
|
||||
{
|
||||
<div class="form-text">
|
||||
<strong>SQL Server locale:</strong><br/>
|
||||
• Named Instance: <code>localhost\SQLEXPRESS</code> o <code>.\SQLEXPRESS</code><br/>
|
||||
• LocalDB: <code>(localdb)\MSSQLLocalDB</code><br/>
|
||||
• Default: <code>localhost</code> o <code>.</code> (usa porta 1433)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Porta *</label>
|
||||
<InputNumber class="form-control" @bind-Value="currentDatabaseCredential.Port" />
|
||||
@if (currentDatabaseCredential.DatabaseType == DatabaseType.SqlServer)
|
||||
{
|
||||
<div class="form-text">
|
||||
<small>Ignorata per named instances e LocalDB</small>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -495,13 +510,26 @@ else
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Username *</label>
|
||||
<InputText class="form-control" @bind-Value="currentDatabaseCredential.Username" />
|
||||
<InputText class="form-control" @bind-Value="currentDatabaseCredential.Username"
|
||||
placeholder="o scrivi 'Integrated' per Windows Auth" />
|
||||
@if (currentDatabaseCredential.DatabaseType == DatabaseType.SqlServer)
|
||||
{
|
||||
<div class="form-text">
|
||||
<small>Per Windows Authentication, scrivi <strong>Integrated</strong> o lascia vuoto</small>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password *</label>
|
||||
<InputText type="password" class="form-control" @bind-Value="currentDatabaseCredential.Password" />
|
||||
@if (currentDatabaseCredential.DatabaseType == DatabaseType.SqlServer)
|
||||
{
|
||||
<div class="form-text">
|
||||
<small>Non richiesta per Windows Authentication</small>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -994,14 +1022,29 @@ else
|
||||
else
|
||||
{
|
||||
// Altri database: validazione standard (Host, Username, Password)
|
||||
if (string.IsNullOrEmpty(currentDatabaseCredential.Host) ||
|
||||
string.IsNullOrEmpty(currentDatabaseCredential.Username) ||
|
||||
// Per SQL Server, permetti Windows Authentication (username vuoto o "Integrated")
|
||||
bool isSqlServerWithWindowsAuth = currentDatabaseCredential.DatabaseType == DatabaseType.SqlServer &&
|
||||
(string.IsNullOrWhiteSpace(currentDatabaseCredential.Username) ||
|
||||
currentDatabaseCredential.Username.Equals("Integrated", StringComparison.OrdinalIgnoreCase) ||
|
||||
currentDatabaseCredential.Username.Equals("Windows", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (string.IsNullOrEmpty(currentDatabaseCredential.Host))
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("alert", "Il campo Host è obbligatorio.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSqlServerWithWindowsAuth)
|
||||
{
|
||||
// Per database che non usano Windows Authentication, richiedi username e password
|
||||
if (string.IsNullOrEmpty(currentDatabaseCredential.Username) ||
|
||||
string.IsNullOrEmpty(currentDatabaseCredential.Password))
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("alert", "Compila tutti i campi obbligatori (Host, Username, Password).");
|
||||
await JSRuntime.InvokeVoidAsync("alert", "Username e Password sono obbligatori. Per SQL Server con Windows Authentication, inserisci 'Integrated' come username.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (success, message) = await CredentialService.TestDatabaseConnectionAsync(currentDatabaseCredential);
|
||||
|
||||
|
||||
@@ -974,6 +974,119 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sezione External ID Relationships (Salesforce) -->
|
||||
@if (selectedRestEntity != null && currentRestDiscovery != null && IsSalesforceClient())
|
||||
{
|
||||
<div class="mt-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-link"></i> External ID Relationships (Salesforce)
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>Relating Records by External ID</strong><br>
|
||||
<small>
|
||||
Crea relazioni tra oggetti usando ID esterni invece degli ID interni di Salesforce.<br>
|
||||
Esempio: Collega Opportunity ad Account usando <code>Account.CardCode__c = "C60000"</code>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Form per aggiungere nuova relazione -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Oggetto Correlato:</label>
|
||||
<select class="form-select" @bind="selectedRelationshipObject" @bind:after="OnRelationshipObjectSelected">
|
||||
<option value="">-- Seleziona Oggetto --</option>
|
||||
@foreach (var entity in availableRelationshipObjects.OrderBy(e => e.Name))
|
||||
{
|
||||
<option value="@entity.Name">@entity.Name</option>
|
||||
}
|
||||
</select>
|
||||
<small class="text-muted">Es: Account, Contact</small>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">External ID Field:</label>
|
||||
<select class="form-select" @bind="selectedExternalIdField" disabled="@string.IsNullOrEmpty(selectedRelationshipObject)">
|
||||
<option value="">-- Seleziona Campo --</option>
|
||||
@foreach (var field in GetExternalIdFieldsForSelectedObject())
|
||||
{
|
||||
<option value="@field">@field</option>
|
||||
}
|
||||
</select>
|
||||
<small class="text-muted">Es: Country__c, CardCode__c</small>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Campo Sorgente:</label>
|
||||
<select class="form-select" @bind="selectedRelationshipSourceField">
|
||||
<option value="">-- Seleziona Campo --</option>
|
||||
@foreach (var field in GetSourceFieldsForRelationship())
|
||||
{
|
||||
<option value="@field">@field</option>
|
||||
}
|
||||
</select>
|
||||
<small class="text-muted">Valore da usare per la relazione</small>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<button class="btn btn-primary w-100" @onclick="AddExternalIdRelationship"
|
||||
disabled="@(string.IsNullOrEmpty(selectedRelationshipObject) || string.IsNullOrEmpty(selectedExternalIdField) || string.IsNullOrEmpty(selectedRelationshipSourceField))">
|
||||
<i class="fas fa-plus"></i> Aggiungi Relazione
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabella relazioni configurate -->
|
||||
@if (externalIdRelationships.Any())
|
||||
{
|
||||
<div class="mt-3">
|
||||
<h6>Relazioni Configurate (@externalIdRelationships.Count)</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Oggetto Correlato</th>
|
||||
<th>External ID Field</th>
|
||||
<th>Campo Sorgente</th>
|
||||
<th>Formato JSON Output</th>
|
||||
<th>Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var rel in externalIdRelationships)
|
||||
{
|
||||
<tr>
|
||||
<td><strong>@rel.RelatedObjectName</strong></td>
|
||||
<td><code>@rel.ExternalIdField</code></td>
|
||||
<td><span class="badge bg-info">@rel.SourceField</span></td>
|
||||
<td><small class="text-muted">@($"\"{rel.RelationshipName}\": {{ \"{rel.ExternalIdField}\": \"value\" }}")</small></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-danger" @onclick="@(() => RemoveExternalIdRelationship(rel))">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-secondary">
|
||||
<i class="fas fa-info-circle"></i> Nessuna relazione External ID configurata. Aggiungine una se necessario.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Sezione Mappature Correnti --> @if (fieldMappings.Any())
|
||||
{
|
||||
<div class="mt-4">
|
||||
@@ -1154,6 +1267,8 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
@@ -1199,6 +1314,7 @@
|
||||
DestinationCredentialName="@selectedRestCredential"
|
||||
DestinationEndpoint="@selectedRestEntity?.Name"
|
||||
FieldMappings="@GetCurrentFieldMappings()"
|
||||
ExternalIdRelationships="@externalIdRelationships"
|
||||
SourceKeyField="@sourceKeyField"
|
||||
UseRecordAssociations="@useRecordAssociations"
|
||||
OnProfileSaved="@OnProfileSaved" />
|
||||
|
||||
@@ -55,6 +55,13 @@ public partial class DataCoupler : ComponentBase
|
||||
private HashSet<string> keyFields = new(); // REST properties marked as keys
|
||||
private string selectedDbColumn = "";
|
||||
|
||||
// External ID Relationships (Salesforce)
|
||||
private List<ExternalIdRelationshipDto> externalIdRelationships = new();
|
||||
private string selectedRelationshipObject = "";
|
||||
private string selectedExternalIdField = "";
|
||||
private string selectedRelationshipSourceField = "";
|
||||
private List<RestEntityInfo> availableRelationshipObjects = new(); // Oggetti disponibili per relazioni
|
||||
|
||||
// Gestione chiavi sorgente e associazioni
|
||||
private string sourceKeyField = ""; // Campo che identifica univocamente il record sorgente
|
||||
private bool requiresManualKeySelection = false; // Flag per indicare se è richiesta selezione manuale
|
||||
@@ -375,6 +382,33 @@ public partial class DataCoupler : ComponentBase
|
||||
Logger.LogInformation("Nessuna chiave sorgente da applicare");
|
||||
}
|
||||
|
||||
// Step 5.5: Carica External ID Relationships (Salesforce)
|
||||
if (!string.IsNullOrEmpty(profile.ExternalIdRelationshipsJson))
|
||||
{
|
||||
Logger.LogInformation("Step 5.5 - Caricamento External ID Relationships...");
|
||||
try
|
||||
{
|
||||
var relationships = System.Text.Json.JsonSerializer.Deserialize<List<ExternalIdRelationshipDto>>(
|
||||
profile.ExternalIdRelationshipsJson,
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase });
|
||||
|
||||
if (relationships != null && relationships.Any())
|
||||
{
|
||||
externalIdRelationships.Clear();
|
||||
externalIdRelationships.AddRange(relationships);
|
||||
Logger.LogInformation("External ID Relationships caricate - Totale: {Count}", externalIdRelationships.Count);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Errore nel caricamento delle External ID Relationships dal profilo");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInformation("Nessuna External ID Relationship da applicare");
|
||||
}
|
||||
|
||||
// Step 6: Applica configurazione associazioni record
|
||||
useRecordAssociations = profile.UseRecordAssociations;
|
||||
Logger.LogInformation("Step 6 - Associazioni record configurate: {UseAssociations}", useRecordAssociations);
|
||||
@@ -688,6 +722,7 @@ public partial class DataCoupler : ComponentBase
|
||||
ResetDestinationState();
|
||||
fieldMappings.Clear();
|
||||
keyFields.Clear();
|
||||
externalIdRelationships.Clear(); // Reset relazioni
|
||||
transferResults.Clear();
|
||||
transferMessage = "";
|
||||
}
|
||||
@@ -1316,6 +1351,9 @@ public partial class DataCoupler : ComponentBase
|
||||
Logger.LogInformation("Rimosso mapping specifico per campo: {DbColumn}", dbColumn);
|
||||
}
|
||||
}
|
||||
Logger.LogInformation("Rimosso mapping specifico per campo: {DbColumn}", dbColumn);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearAllMappings()
|
||||
{
|
||||
@@ -1325,9 +1363,129 @@ public partial class DataCoupler : ComponentBase
|
||||
sourceKeyField = "";
|
||||
transferMessage = "";
|
||||
transferMessageType = "";
|
||||
externalIdRelationships.Clear(); // Pulisce anche le relazioni
|
||||
Logger.LogInformation("Tutti i mapping e le configurazioni sono stati cancellati");
|
||||
}
|
||||
|
||||
// External ID Relationships Methods
|
||||
|
||||
private void OnRelationshipObjectSelected()
|
||||
{
|
||||
// Il valore è già impostato tramite @bind, resettiamo solo i campi dipendenti
|
||||
selectedExternalIdField = ""; // Reset campo External ID quando cambia l'oggetto
|
||||
selectedRelationshipSourceField = ""; // Reset anche campo sorgente
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void AddExternalIdRelationship()
|
||||
{
|
||||
if (string.IsNullOrEmpty(selectedRelationshipObject) ||
|
||||
string.IsNullOrEmpty(selectedExternalIdField) ||
|
||||
string.IsNullOrEmpty(selectedRelationshipSourceField))
|
||||
{
|
||||
Logger.LogWarning("Impossibile aggiungere relazione: campi mancanti");
|
||||
return;
|
||||
}
|
||||
|
||||
// Trova il nome dell'oggetto correlato
|
||||
var relatedObject = availableRelationshipObjects.FirstOrDefault(o => o.Name == selectedRelationshipObject);
|
||||
if (relatedObject == null)
|
||||
{
|
||||
Logger.LogWarning("Oggetto correlato non trovato: {ObjectName}", selectedRelationshipObject);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determina il nome della relazione in base al tipo di oggetto
|
||||
// Salesforce: oggetti STANDARD usano solo il nome (es. "Account")
|
||||
// oggetti CUSTOM (finiscono con __c) usano __r (es. "CustomObject__r")
|
||||
string relationshipName;
|
||||
if (selectedRelationshipObject.EndsWith("__c"))
|
||||
{
|
||||
// Oggetto custom: rimuovi __c e aggiungi __r
|
||||
relationshipName = selectedRelationshipObject.Replace("__c", "__r");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Oggetto standard: usa solo il nome
|
||||
relationshipName = selectedRelationshipObject;
|
||||
}
|
||||
|
||||
// Crea la relazione
|
||||
var relationship = new ExternalIdRelationshipDto
|
||||
{
|
||||
RelationshipName = relationshipName,
|
||||
RelatedObjectName = selectedRelationshipObject,
|
||||
ExternalIdField = selectedExternalIdField,
|
||||
SourceField = selectedRelationshipSourceField
|
||||
};
|
||||
|
||||
// Verifica duplicati
|
||||
if (externalIdRelationships.Any(r =>
|
||||
r.RelatedObjectName == relationship.RelatedObjectName &&
|
||||
r.ExternalIdField == relationship.ExternalIdField))
|
||||
{
|
||||
Logger.LogWarning("Relazione già esistente per questo oggetto e campo External ID");
|
||||
return;
|
||||
}
|
||||
|
||||
externalIdRelationships.Add(relationship);
|
||||
|
||||
Logger.LogInformation("Aggiunta relazione External ID: {Relationship}.{Field} <- {SourceField}",
|
||||
relationship.RelationshipName, relationship.ExternalIdField, relationship.SourceField);
|
||||
|
||||
// Reset campi
|
||||
selectedRelationshipObject = "";
|
||||
selectedExternalIdField = "";
|
||||
selectedRelationshipSourceField = "";
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void RemoveExternalIdRelationship(ExternalIdRelationshipDto relationship)
|
||||
{
|
||||
if (externalIdRelationships.Remove(relationship))
|
||||
{
|
||||
Logger.LogInformation("Rimossa relazione External ID: {Relationship}.{Field}",
|
||||
relationship.RelationshipName, relationship.ExternalIdField);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private List<string> GetExternalIdFieldsForSelectedObject()
|
||||
{
|
||||
if (string.IsNullOrEmpty(selectedRelationshipObject))
|
||||
return new List<string>();
|
||||
|
||||
var entity = availableRelationshipObjects.FirstOrDefault(e => e.Name == selectedRelationshipObject);
|
||||
if (entity == null)
|
||||
return new List<string>();
|
||||
|
||||
// Filtra i campi che potrebbero essere External ID (tipicamente campo con __c o specifici tipi)
|
||||
return entity.Properties
|
||||
.Where(p => p.Name.EndsWith("__c") || p.Name == "Id" || p.Name.Contains("External"))
|
||||
.Select(p => p.Name)
|
||||
.OrderBy(p => p)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private List<string> GetSourceFieldsForRelationship()
|
||||
{
|
||||
// Restituisce i campi sorgente disponibili
|
||||
if (selectedSourceType == "database")
|
||||
{
|
||||
if (useCustomQuery && queryColumns.Any())
|
||||
return queryColumns.ToList();
|
||||
else if (!useCustomQuery && !string.IsNullOrEmpty(selectedTable) && databaseTables.ContainsKey(selectedTable))
|
||||
return databaseTables[selectedTable].Select(c => c.Name).ToList();
|
||||
}
|
||||
else if (selectedSourceType == "file" && fileSheets.ContainsKey(selectedSheet))
|
||||
{
|
||||
return fileSheets[selectedSheet].ToList();
|
||||
}
|
||||
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
private void AutoMapFields()
|
||||
{
|
||||
if (restEntityDetails == null)
|
||||
@@ -1943,11 +2101,25 @@ public partial class DataCoupler : ComponentBase
|
||||
{
|
||||
var restData = new Dictionary<string, object>();
|
||||
|
||||
// Crea un set con i campi sorgente usati in External ID Relationships
|
||||
// per escluderli dai mapping normali (verranno gestiti separatamente)
|
||||
var externalIdSourceFields = externalIdRelationships
|
||||
.Where(r => !string.IsNullOrWhiteSpace(r.SourceField))
|
||||
.Select(r => r.SourceField)
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var mapping in fieldMappings)
|
||||
{
|
||||
string dbColumn = mapping.Key;
|
||||
string restProperty = mapping.Value;
|
||||
|
||||
// Salta il mapping se il campo è usato in un External ID Relationship
|
||||
if (externalIdSourceFields.Contains(dbColumn))
|
||||
{
|
||||
Logger.LogDebug("Campo {DbColumn} usato in External ID Relationship, escluso da mapping normale", dbColumn);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dbRecord.ContainsKey(dbColumn))
|
||||
{
|
||||
var value = dbRecord[dbColumn];
|
||||
@@ -1962,6 +2134,35 @@ public partial class DataCoupler : ComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
// Aggiungi External ID Relationships (per Salesforce)
|
||||
if (externalIdRelationships.Any())
|
||||
{
|
||||
foreach (var relationship in externalIdRelationships)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(relationship.SourceField) &&
|
||||
dbRecord.ContainsKey(relationship.SourceField))
|
||||
{
|
||||
var sourceValue = dbRecord[relationship.SourceField];
|
||||
var transformedValue = TransformValue(sourceValue, relationship.SourceField, relationship.ExternalIdField);
|
||||
|
||||
if (transformedValue != null)
|
||||
{
|
||||
// Crea il dizionario annidato per l'External ID Relationship
|
||||
// Formato: { "Account": { "CardCode__c": "V50000" } }
|
||||
var externalIdObject = new Dictionary<string, object>
|
||||
{
|
||||
{ relationship.ExternalIdField, transformedValue }
|
||||
};
|
||||
|
||||
restData[relationship.RelationshipName] = externalIdObject;
|
||||
|
||||
Logger.LogDebug("Aggiunta External ID Relationship: {RelationshipName}.{ExternalIdField} = {Value} (from {SourceField})",
|
||||
relationship.RelationshipName, relationship.ExternalIdField, transformedValue, relationship.SourceField);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogDebug("Record trasformato: {DbColumns} → {RestProperties}",
|
||||
string.Join(", ", dbRecord.Keys),
|
||||
string.Join(", ", restData.Keys));
|
||||
|
||||
@@ -164,18 +164,25 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
||||
throw new InvalidOperationException("Nessun mapping dei campi configurato per il profilo");
|
||||
}
|
||||
|
||||
// 4.5. Parse External ID Relationships (Salesforce)
|
||||
var externalIdRelationships = ParseExternalIdRelationships(profile.ExternalIdRelationshipsJson);
|
||||
if (externalIdRelationships.Any())
|
||||
{
|
||||
_logger.LogInformation("Caricate {Count} External ID Relationships dal profilo", externalIdRelationships.Count);
|
||||
}
|
||||
|
||||
// 5. Determina se utilizzare Salesforce Composite API
|
||||
bool useSalesforceComposite = restClient is DataConnection.REST.Implementations.SalesforceServiceClient;
|
||||
|
||||
if (useSalesforceComposite)
|
||||
{
|
||||
_logger.LogInformation("Utilizzo Salesforce Composite API per il trasferimento");
|
||||
return await ExecuteDataTransferWithCompositeAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings, enableDeletionSync);
|
||||
return await ExecuteDataTransferWithCompositeAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings, externalIdRelationships, enableDeletionSync);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Utilizzo metodo trasferimento standard per il trasferimento");
|
||||
return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings, enableDeletionSync);
|
||||
return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings, externalIdRelationships, enableDeletionSync);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -363,6 +370,53 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
||||
return mappings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializza gli External ID Relationships dal JSON del profilo
|
||||
/// </summary>
|
||||
private List<ExternalIdRelationshipDto> ParseExternalIdRelationships(string? externalIdRelationshipsJson)
|
||||
{
|
||||
var relationships = new List<ExternalIdRelationshipDto>();
|
||||
|
||||
if (string.IsNullOrEmpty(externalIdRelationshipsJson))
|
||||
{
|
||||
_logger.LogDebug("ExternalIdRelationships JSON è vuoto o null");
|
||||
return relationships;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Parsing ExternalIdRelationships JSON: {Json}", externalIdRelationshipsJson);
|
||||
|
||||
try
|
||||
{
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
var relationshipsList = JsonSerializer.Deserialize<List<ExternalIdRelationshipDto>>(externalIdRelationshipsJson, options);
|
||||
if (relationshipsList != null)
|
||||
{
|
||||
relationships = relationshipsList;
|
||||
_logger.LogInformation("Trovati {Count} External ID Relationships nel JSON", relationships.Count);
|
||||
|
||||
foreach (var rel in relationships)
|
||||
{
|
||||
_logger.LogDebug("External ID Relationship: {RelationshipName} - {RelatedObject}.{ExternalIdField} <- {SourceField}",
|
||||
rel.RelationshipName, rel.RelatedObjectName, rel.ExternalIdField, rel.SourceField);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Deserializzazione ritornato null per ExternalIdRelationships JSON");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Errore nel parsing degli ExternalIdRelationships: {Json}", externalIdRelationshipsJson);
|
||||
}
|
||||
|
||||
return relationships;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene tutti i record dal database
|
||||
/// </summary>
|
||||
@@ -631,6 +685,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
||||
RestEntitySummary restEntity,
|
||||
RestApiCredential restCredential,
|
||||
Dictionary<string, string> fieldMappings,
|
||||
List<ExternalIdRelationshipDto> externalIdRelationships,
|
||||
bool enableDeletionSync = false)
|
||||
{
|
||||
_logger.LogInformation("Iniziando trasferimento dati standard per {RecordCount} record - DeletionSync: {DeletionSync}",
|
||||
@@ -644,8 +699,8 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. Trasforma il record utilizzando i field mappings
|
||||
var restData = TransformRecordForRest(record, fieldMappings);
|
||||
// 1. Trasforma il record utilizzando i field mappings e External ID Relationships
|
||||
var restData = TransformRecordForRest(record, fieldMappings, externalIdRelationships);
|
||||
|
||||
// 2. Gestione associazioni record se abilitata
|
||||
string? entityId = null;
|
||||
@@ -755,6 +810,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
||||
RestEntitySummary restEntity,
|
||||
RestApiCredential restCredential,
|
||||
Dictionary<string, string> fieldMappings,
|
||||
List<ExternalIdRelationshipDto> externalIdRelationships,
|
||||
bool enableDeletionSync = false)
|
||||
{
|
||||
_logger.LogInformation("Iniziando trasferimento dati COMPOSITE per {RecordCount} record - DeletionSync: {DeletionSync}",
|
||||
@@ -764,7 +820,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
||||
if (!(restClient is DataConnection.REST.Implementations.SalesforceServiceClient salesforceClient))
|
||||
{
|
||||
_logger.LogWarning("Client REST non è SalesforceServiceClient, fallback al metodo standard");
|
||||
return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential, fieldMappings, enableDeletionSync);
|
||||
return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential, fieldMappings, externalIdRelationships, enableDeletionSync);
|
||||
}
|
||||
|
||||
try
|
||||
@@ -794,8 +850,8 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
||||
var record = indexedRecord.Record;
|
||||
var recordNumber = indexedRecord.RecordNumber;
|
||||
|
||||
// Trasforma il record in base ai mapping (operazione locale, thread-safe)
|
||||
var restData = TransformRecordForRest(record, fieldMappings);
|
||||
// Trasforma il record in base ai mapping e External ID Relationships (operazione locale, thread-safe)
|
||||
var restData = TransformRecordForRest(record, fieldMappings, externalIdRelationships);
|
||||
|
||||
// Genera la chiave sorgente e l'hash dei dati per questo record (include MAPPING_SIGNATURE)
|
||||
var sourceKey = GenerateSourceKey(record, profile.SourceKeyField);
|
||||
@@ -1085,7 +1141,10 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
||||
/// <summary>
|
||||
/// Trasforma un record sorgente in formato REST utilizzando i field mappings
|
||||
/// </summary>
|
||||
private Dictionary<string, object> TransformRecordForRest(Dictionary<string, object> sourceRecord, Dictionary<string, string> fieldMappings)
|
||||
private Dictionary<string, object> TransformRecordForRest(
|
||||
Dictionary<string, object> sourceRecord,
|
||||
Dictionary<string, string> fieldMappings,
|
||||
List<ExternalIdRelationshipDto>? externalIdRelationships = null)
|
||||
{
|
||||
var restData = new Dictionary<string, object>();
|
||||
|
||||
@@ -1105,6 +1164,35 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
||||
}
|
||||
}
|
||||
|
||||
// Aggiungi External ID Relationships (per Salesforce)
|
||||
if (externalIdRelationships != null && externalIdRelationships.Any())
|
||||
{
|
||||
foreach (var relationship in externalIdRelationships)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(relationship.SourceField) &&
|
||||
sourceRecord.ContainsKey(relationship.SourceField))
|
||||
{
|
||||
var sourceValue = sourceRecord[relationship.SourceField];
|
||||
var transformedValue = TransformValueForRest(sourceValue);
|
||||
|
||||
if (transformedValue != null)
|
||||
{
|
||||
// Crea il dizionario annidato per l'External ID Relationship
|
||||
// Formato: { "Account__r": { "Country__c": "US" } }
|
||||
var externalIdObject = new Dictionary<string, object>
|
||||
{
|
||||
{ relationship.ExternalIdField, transformedValue }
|
||||
};
|
||||
|
||||
restData[relationship.RelationshipName] = externalIdObject;
|
||||
|
||||
_logger.LogDebug("Aggiunta External ID Relationship: {RelationshipName} → {ExternalIdField} = {Value}",
|
||||
relationship.RelationshipName, relationship.ExternalIdField, transformedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return restData;
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,350 @@
|
||||
# Implementazione External ID Relationships per Salesforce
|
||||
|
||||
## 📋 Panoramica
|
||||
|
||||
Implementata la funzionalità completa per gestire **External ID Relationships** nell'interfaccia di mapping dei campi di Data-Coupler. Questa feature permette di creare relazioni tra oggetti Salesforce utilizzando External ID durante il trasferimento dati, evitando la necessità di conoscere gli ID Salesforce interni.
|
||||
|
||||
## 🎯 Obiettivi Raggiunti
|
||||
|
||||
- ✅ Estensione modelli dati (DTO ed Entity) per supportare External ID Relationships
|
||||
- ✅ UI completa per configurazione relazioni con autocomplete
|
||||
- ✅ Logica di trasformazione dati integrata in DataCoupler e ScheduledProfileExecutionService
|
||||
- ✅ Supporto per salvataggio e caricamento relazioni in profili Data Coupler
|
||||
- ✅ Migrazione database per persistenza configurazioni
|
||||
- ✅ Supporto per esecuzioni schedulate
|
||||
|
||||
## 🏗️ Architettura Implementata
|
||||
|
||||
### 1. Modelli Dati
|
||||
|
||||
#### **ExternalIdRelationshipDto** (CredentialManager/Models/DataCouplerProfileDto.cs)
|
||||
```csharp
|
||||
public class ExternalIdRelationshipDto
|
||||
{
|
||||
public string RelationshipName { get; set; } = string.Empty; // Es: "Account__r"
|
||||
public string RelatedObjectName { get; set; } = string.Empty; // Es: "Account"
|
||||
public string ExternalIdField { get; set; } = string.Empty; // Es: "Country__c"
|
||||
public string SourceField { get; set; } = string.Empty; // Campo sorgente con valore
|
||||
}
|
||||
```
|
||||
|
||||
#### **DataCouplerProfile Entity** (CredentialManager/Models/DataCouplerProfile.cs)
|
||||
```csharp
|
||||
[MaxLength(4000)]
|
||||
public string? ExternalIdRelationshipsJson { get; set; }
|
||||
```
|
||||
|
||||
### 2. Serializzazione/Deserializzazione
|
||||
|
||||
#### **DataCouplerProfileService** (CredentialManager/Services/DataCouplerProfileService.cs)
|
||||
|
||||
**Metodi Aggiunti:**
|
||||
- `SerializeExternalIdRelationships()` - Serializza lista DTO → JSON
|
||||
- `DeserializeExternalIdRelationships()` - Deserializza JSON → lista DTO
|
||||
- Aggiornato `ToDto()` per includere External ID Relationships
|
||||
- Aggiornato `FromDto()` per serializzare relazioni
|
||||
- Aggiornato `UpdateProfileAsync()` per persistere ExternalIdRelationshipsJson
|
||||
|
||||
### 3. Interfaccia Utente
|
||||
|
||||
#### **DataCoupler.razor** - Sezione External ID Relationships
|
||||
|
||||
**Componenti UI:**
|
||||
1. **Selezione Oggetto Correlato**: Dropdown con tutti gli oggetti REST disponibili
|
||||
2. **Selezione External ID Field**: Dropdown con campi filtrati (terminanti con `__c`, `Id`, contengono "External")
|
||||
3. **Selezione Campo Sorgente**: Dropdown con campi disponibili dalla sorgente dati
|
||||
4. **Pulsante Aggiungi**: Conferma e aggiunge relazione alla lista
|
||||
5. **Tabella Relazioni**: Visualizza tutte le relazioni configurate con formato di esempio
|
||||
|
||||
**Visibilità Condizionale:**
|
||||
```csharp
|
||||
@if (fieldMappings.Any() && currentRestDiscovery != null && IsSalesforceClient())
|
||||
```
|
||||
- Mostrata solo per connessioni Salesforce
|
||||
- Solo dopo aver configurato i field mappings principali
|
||||
|
||||
#### **DataCoupler.razor.cs** - Gestione Relazioni
|
||||
|
||||
**Campi Aggiunti:**
|
||||
```csharp
|
||||
private List<ExternalIdRelationshipDto> externalIdRelationships = new();
|
||||
private string selectedRelationshipObject = string.Empty;
|
||||
private string selectedExternalIdField = string.Empty;
|
||||
private string selectedRelationshipSourceField = string.Empty;
|
||||
private List<RestEntityInfo> availableRelationshipObjects = new();
|
||||
```
|
||||
|
||||
**Metodi Implementati:**
|
||||
- `OnRelationshipObjectSelected()` - Gestisce selezione oggetto
|
||||
- `AddExternalIdRelationship()` - Aggiunge nuova relazione con validazione
|
||||
- `RemoveExternalIdRelationship()` - Rimuove relazione esistente
|
||||
- `GetExternalIdFieldsForSelectedObject()` - Ottiene campi External ID disponibili
|
||||
- `GetSourceFieldsForRelationship()` - Ottiene campi sorgente per mapping
|
||||
|
||||
**Integrazione Reset/Clear:**
|
||||
- Aggiornato `ClearAllMappings()` per pulire relazioni
|
||||
- Aggiornato `ResetAllState()` per reset completo
|
||||
- Aggiornato `ApplyProfileConfiguration()` per caricare relazioni da profilo
|
||||
|
||||
### 4. Trasformazione Dati
|
||||
|
||||
#### **DataCoupler.razor.cs** - TransformRecordToRestEntity()
|
||||
|
||||
```csharp
|
||||
// Aggiungi External ID Relationships (per Salesforce)
|
||||
if (externalIdRelationships.Any())
|
||||
{
|
||||
foreach (var relationship in externalIdRelationships)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(relationship.SourceField) &&
|
||||
dbRecord.ContainsKey(relationship.SourceField))
|
||||
{
|
||||
var sourceValue = dbRecord[relationship.SourceField];
|
||||
var transformedValue = TransformValue(sourceValue, relationship.SourceField, relationship.ExternalIdField);
|
||||
|
||||
if (transformedValue != null)
|
||||
{
|
||||
// Formato: { "Account__r": { "Country__c": "US" } }
|
||||
var externalIdObject = new Dictionary<string, object>
|
||||
{
|
||||
{ relationship.ExternalIdField, transformedValue }
|
||||
};
|
||||
|
||||
restData[relationship.RelationshipName] = externalIdObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **ScheduledProfileExecutionService** - TransformRecordForRest()
|
||||
|
||||
**Modifiche:**
|
||||
- Aggiunto parametro opzionale `List<ExternalIdRelationshipDto>? externalIdRelationships`
|
||||
- Implementata stessa logica di trasformazione per esecuzioni schedulate
|
||||
- Aggiornato `ExecuteDataTransferAsync()` per deserializzare e passare relazioni
|
||||
- Aggiornato `ExecuteDataTransferStandardAsync()` per accettare e usare relazioni
|
||||
- Aggiornato `ExecuteDataTransferWithCompositeAsync()` per supporto Salesforce Composite API
|
||||
|
||||
**Nuovo Metodo:**
|
||||
```csharp
|
||||
private List<ExternalIdRelationshipDto> ParseExternalIdRelationships(string? externalIdRelationshipsJson)
|
||||
{
|
||||
// Deserializza JSON con stesse opzioni di DataCouplerProfileService
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
return JsonSerializer.Deserialize<List<ExternalIdRelationshipDto>>(externalIdRelationshipsJson, options);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Salvataggio Profili
|
||||
|
||||
#### **Components/ProfileSaver.razor.cs**
|
||||
|
||||
**Modifiche:**
|
||||
- Aggiunto parametro `ExternalIdRelationships`
|
||||
- Incluso nella creazione del DTO per salvataggio profili
|
||||
|
||||
```csharp
|
||||
[Parameter]
|
||||
public List<ExternalIdRelationshipDto> ExternalIdRelationships { get; set; } = new();
|
||||
|
||||
// In SaveProfile()
|
||||
ExternalIdRelationships = this.ExternalIdRelationships,
|
||||
```
|
||||
|
||||
### 6. Discovery REST API
|
||||
|
||||
#### **Data_Coupler/Extensions/DataCoupler/RESTMethod.cs**
|
||||
|
||||
**Modifiche:**
|
||||
- Aggiornato `ConnectToRestApi()` per popolare `availableRelationshipObjects`
|
||||
- Chiamata a `DiscoverEntitiesAsync()` per ottenere dettagli completi oggetti REST
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
availableRelationshipObjects = (await currentRestDiscovery.DiscoverEntitiesAsync()).ToList();
|
||||
Logger.LogInformation("Caricati {Count} oggetti REST per External ID Relationships", availableRelationshipObjects.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Impossibile caricare oggetti REST per External ID Relationships");
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Migrazione Database
|
||||
|
||||
#### **File Creati:**
|
||||
|
||||
1. **20260203000000_AddExternalIdRelationships.cs**
|
||||
- Migrazione Entity Framework per aggiungere campo `ExternalIdRelationshipsJson`
|
||||
- Tipo: TEXT, MaxLength: 4000, Nullable
|
||||
|
||||
2. **20260203000000_AddExternalIdRelationships.sql**
|
||||
- Script SQL manuale per applicazione diretta se necessario
|
||||
- Include update di `__EFMigrationsHistory`
|
||||
|
||||
```sql
|
||||
ALTER TABLE DataCouplerProfiles ADD COLUMN ExternalIdRelationshipsJson TEXT;
|
||||
INSERT INTO __EFMigrationsHistory (MigrationId, ProductVersion)
|
||||
VALUES ('20260203000000_AddExternalIdRelationships', '9.0.0');
|
||||
```
|
||||
|
||||
## 📊 Formato Dati Salesforce
|
||||
|
||||
### Esempio di Trasformazione
|
||||
|
||||
**Configurazione:**
|
||||
- **Relationship Name**: `Account__r`
|
||||
- **Related Object**: `Account`
|
||||
- **External ID Field**: `Country__c`
|
||||
- **Source Field**: `CountryCode` (dalla tabella sorgente)
|
||||
|
||||
**Record Sorgente:**
|
||||
```json
|
||||
{
|
||||
"ProductName": "Widget A",
|
||||
"Price": 99.99,
|
||||
"CountryCode": "US"
|
||||
}
|
||||
```
|
||||
|
||||
**Record Trasformato per Salesforce:**
|
||||
```json
|
||||
{
|
||||
"Name": "Widget A",
|
||||
"Price__c": 99.99,
|
||||
"Account__r": {
|
||||
"Country__c": "US"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Vantaggi External ID
|
||||
|
||||
1. **Nessun ID Salesforce Richiesto**: Non serve conoscere l'ID Salesforce dell'Account
|
||||
2. **Lookup Automatico**: Salesforce cerca automaticamente l'Account con `Country__c = "US"`
|
||||
3. **Upsert Intelligente**: Se non trova l'Account, può crearlo automaticamente (se configurato)
|
||||
4. **Manutenzione Semplificata**: I codici esterni sono più stabili degli ID interni
|
||||
|
||||
## 🔄 Flusso Operativo
|
||||
|
||||
### Configurazione Manuale (DataCoupler.razor)
|
||||
|
||||
1. Utente configura connessione sorgente (database/file) e destinazione (Salesforce)
|
||||
2. Sistema scopre automaticamente oggetti REST disponibili
|
||||
3. Utente configura field mappings principali
|
||||
4. Sezione External ID Relationships diventa visibile
|
||||
5. Utente seleziona:
|
||||
- Oggetto correlato (es: Account)
|
||||
- Campo External ID (es: Country__c)
|
||||
- Campo sorgente (es: CountryCode)
|
||||
6. Click su "Aggiungi Relazione" → validazione e aggiunta alla lista
|
||||
7. (Opzionale) Salvataggio come profilo per riutilizzo futuro
|
||||
8. Esecuzione trasferimento → relazioni applicate automaticamente
|
||||
|
||||
### Esecuzione Schedulata (ScheduledProfileExecutionService)
|
||||
|
||||
1. Background service carica profilo dal database
|
||||
2. Deserializza External ID Relationships da JSON
|
||||
3. Estrae dati dalla sorgente
|
||||
4. Trasforma ogni record applicando field mappings + External ID Relationships
|
||||
5. Invia a Salesforce (Standard API o Composite API)
|
||||
6. Gestisce associazioni record e hash per evitare duplicati
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Scenari di Test Consigliati
|
||||
|
||||
1. **Configurazione UI**
|
||||
- ✅ Selezione oggetti e campi funziona correttamente
|
||||
- ✅ Validazione impedisce relazioni incomplete
|
||||
- ✅ Aggiunta e rimozione relazioni aggiorna UI
|
||||
|
||||
2. **Salvataggio/Caricamento Profili**
|
||||
- ✅ Relazioni salvate correttamente in JSON
|
||||
- ✅ Profilo ricaricato ripristina tutte le relazioni
|
||||
- ✅ Database persiste ExternalIdRelationshipsJson
|
||||
|
||||
3. **Trasformazione Dati**
|
||||
- ✅ Record trasformato include dizionario annidato per relazioni
|
||||
- ✅ Valori null/vuoti gestiti correttamente
|
||||
- ✅ Logging dettagliato per ogni relazione aggiunta
|
||||
|
||||
4. **Esecuzione Schedulata**
|
||||
- ✅ Schedulazione carica e applica relazioni
|
||||
- ✅ Funziona sia con Standard API che Composite API
|
||||
- ✅ Errori gestiti e loggati senza bloccare il flusso
|
||||
|
||||
5. **Integrazione Salesforce**
|
||||
- ✅ Salesforce accetta formato External ID Relationship
|
||||
- ✅ Lookup automatico funziona correttamente
|
||||
- ✅ Record creati con relazioni corrette
|
||||
|
||||
## 📝 Note Implementative
|
||||
|
||||
### Decisioni di Design
|
||||
|
||||
1. **MaxLength JSON: 4000 caratteri**
|
||||
- Ragionamento: Supporta configurazioni complesse senza eccedere limiti SQLite
|
||||
- Alternativa: Se necessario più spazio, può essere aumentato a TEXT illimitato
|
||||
|
||||
2. **Parametro Opzionale in TransformRecordForRest**
|
||||
- Backward compatibility garantita
|
||||
- Chiamate esistenti senza External ID continuano a funzionare
|
||||
|
||||
3. **Filtro Campi External ID**
|
||||
- Logica: `EndsWith("__c") || Name == "Id" || Contains("External")`
|
||||
- Copre la maggior parte dei casi comuni in Salesforce
|
||||
- Personalizzabile se necessario
|
||||
|
||||
4. **Visibilità Condizionale UI**
|
||||
- Solo per Salesforce (verifica `IsSalesforceClient()`)
|
||||
- Solo dopo field mappings configurati (`fieldMappings.Any()`)
|
||||
- Migliora UX evitando confusione per altre API
|
||||
|
||||
### Potenziali Estensioni Future
|
||||
|
||||
1. **Validazione Avanzata**: Verifica esistenza oggetto/campo su Salesforce prima di salvare
|
||||
2. **Multi-Level Relationships**: Supporto per relazioni annidate (es: `Account__r.Owner__r.Name__c`)
|
||||
3. **Relazioni Composite**: Più External ID per stesso oggetto (es: FirstName + LastName)
|
||||
4. **Import/Export Relazioni**: Backup e restore separato delle configurazioni relazioni
|
||||
5. **Template Relazioni**: Libreria di relazioni predefinite per oggetti Salesforce comuni
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Errori Comuni
|
||||
|
||||
**Errore: "External ID field not found"**
|
||||
- Causa: Campo External ID non esiste sull'oggetto Salesforce
|
||||
- Soluzione: Verificare che il campo sia configurato come External ID in Salesforce
|
||||
|
||||
**Errore: "Multiple records found with external ID"**
|
||||
- Causa: External ID non è univoco in Salesforce
|
||||
- Soluzione: Verificare unicità del campo External ID
|
||||
|
||||
**Relazioni Non Applicate**
|
||||
- Causa: `externalIdRelationships` è vuoto
|
||||
- Soluzione: Verificare deserializzazione JSON in profilo
|
||||
|
||||
**UI Non Mostra Sezione Relazioni**
|
||||
- Causa: Condizione visibilità non soddisfatta
|
||||
- Soluzione: Verificare che sia Salesforce e field mappings configurati
|
||||
|
||||
## 📚 Riferimenti
|
||||
|
||||
- [Salesforce External ID Documentation](https://help.salesforce.com/s/articleView?id=sf.fields_about_custom_external_id.htm)
|
||||
- [Salesforce REST API - Insert or Update](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_upsert.htm)
|
||||
- [Salesforce Relationship Fields](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_relationship_fields.htm)
|
||||
|
||||
---
|
||||
|
||||
**Implementazione Completata**: 3 Febbraio 2026
|
||||
**Framework**: .NET 9.0
|
||||
**Pattern**: Repository + DTO + Service Layer
|
||||
**Database**: SQLite con Entity Framework Core
|
||||
**UI**: Blazor Server con Bootstrap 5
|
||||
@@ -0,0 +1,345 @@
|
||||
# Fix Connessione SQL Server con Localhost
|
||||
|
||||
**Data**: 15 Febbraio 2026
|
||||
**Versione**: 2.1+
|
||||
|
||||
## 📋 Problema Risolto
|
||||
|
||||
Il sistema non riusciva a connettersi correttamente a SQL Server quando si utilizzava "localhost" come host, specialmente per:
|
||||
- Named Instances (es. `localhost\SQLEXPRESS`)
|
||||
- LocalDB (es. `(localdb)\MSSQLLocalDB`)
|
||||
- Windows Authentication
|
||||
|
||||
## 🔧 Modifiche Implementate
|
||||
|
||||
### 1. ConnectionStringBuilder - Gestione Intelligente del Server
|
||||
|
||||
**File**: `CredentialManager/Models/CredentialModels.cs`
|
||||
|
||||
#### Miglioramenti:
|
||||
|
||||
**a) Named Instances**
|
||||
- Se l'host contiene `\` (backslash), la porta viene omessa automaticamente
|
||||
- Esempi supportati:
|
||||
- `localhost\SQLEXPRESS`
|
||||
- `.\SQLEXPRESS`
|
||||
- `SERVERNAME\INSTANCE`
|
||||
|
||||
**b) LocalDB**
|
||||
- Se l'host inizia con `(localdb)`, la porta viene omessa
|
||||
- Esempi supportati:
|
||||
- `(localdb)\MSSQLLocalDB`
|
||||
- `(localdb)\v11.0`
|
||||
- `(localdb)\ProjectsV13`
|
||||
|
||||
**c) Localhost con Named Pipes**
|
||||
- Per `localhost`, `.` o `127.0.0.1` con porta 1433 (default), la porta viene omessa
|
||||
- Questo permette a SQL Server di usare Named Pipes invece di TCP/IP per connessioni locali più veloci
|
||||
|
||||
**d) Windows Authentication**
|
||||
- Se username è vuoto, `Integrated` o `Windows`, usa Windows Authentication
|
||||
- Non richiede password quando si usa Windows Authentication
|
||||
- Connection string include `Integrated Security=True`
|
||||
|
||||
#### Codice Modificato:
|
||||
|
||||
```csharp
|
||||
private static string BuildSqlServerConnectionString(DatabaseCredential credential)
|
||||
{
|
||||
var builder = new List<string>();
|
||||
|
||||
// Gestione speciale per SQL Server locale e named instances
|
||||
bool hasInstanceName = credential.Host.Contains('\\') ||
|
||||
credential.Host.StartsWith("(localdb)", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (hasInstanceName)
|
||||
{
|
||||
// Per named instances e LocalDB, non includere la porta
|
||||
builder.Add($"Server={credential.Host}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Per localhost con porta default, ometti la porta per usare Named Pipes
|
||||
if ((credential.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase) ||
|
||||
credential.Host == "." ||
|
||||
credential.Host == "127.0.0.1") && credential.Port == 1433)
|
||||
{
|
||||
builder.Add($"Server={credential.Host}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Per altri casi, usa host,porta
|
||||
builder.Add($"Server={credential.Host},{credential.Port}");
|
||||
}
|
||||
}
|
||||
|
||||
// Windows Authentication vs SQL Authentication
|
||||
if (string.IsNullOrWhiteSpace(credential.Username) ||
|
||||
credential.Username.Equals("Integrated", StringComparison.OrdinalIgnoreCase) ||
|
||||
credential.Username.Equals("Windows", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
builder.Add("Integrated Security=True");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Add($"User Id={credential.Username}");
|
||||
builder.Add($"Password={credential.Password}");
|
||||
}
|
||||
|
||||
builder.Add($"Connection Timeout={credential.CommandTimeout}");
|
||||
|
||||
if (!string.IsNullOrEmpty(credential.DatabaseName))
|
||||
builder.Add($"Database={credential.DatabaseName}");
|
||||
|
||||
if (credential.IgnoreSslErrors)
|
||||
builder.Add("TrustServerCertificate=True");
|
||||
|
||||
return string.Join(";", builder);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. UI - Guida Contestuale per SQL Server
|
||||
|
||||
**File**: `Data_Coupler/Pages/CredentialManagement.razor`
|
||||
|
||||
#### Aggiunte:
|
||||
|
||||
**a) Help Text per Host/Server**
|
||||
- Mostra esempi specifici per SQL Server locale:
|
||||
- Named Instance: `localhost\SQLEXPRESS` o `.\SQLEXPRESS`
|
||||
- LocalDB: `(localdb)\MSSQLLocalDB`
|
||||
- Default: `localhost` o `.` (usa porta 1433)
|
||||
|
||||
**b) Nota sulla Porta**
|
||||
- Indica che la porta viene ignorata per named instances e LocalDB
|
||||
|
||||
**c) Guida Windows Authentication**
|
||||
- Nel campo Username: placeholder "o scrivi 'Integrated' per Windows Auth"
|
||||
- Help text: "Per Windows Authentication, scrivi **Integrated** o lascia vuoto"
|
||||
- Nel campo Password: "Non richiesta per Windows Authentication"
|
||||
|
||||
#### Codice Aggiunto:
|
||||
|
||||
```razor
|
||||
@if (currentDatabaseCredential.DatabaseType == DatabaseType.SqlServer)
|
||||
{
|
||||
<div class="form-text">
|
||||
<strong>SQL Server locale:</strong><br/>
|
||||
• Named Instance: <code>localhost\SQLEXPRESS</code> o <code>.\SQLEXPRESS</code><br/>
|
||||
• LocalDB: <code>(localdb)\MSSQLLocalDB</code><br/>
|
||||
• Default: <code>localhost</code> o <code>.</code> (usa porta 1433)
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Validazione Aggiornata
|
||||
|
||||
**File**: `Data_Coupler/Pages/CredentialManagement.razor`
|
||||
|
||||
#### Miglioramenti:
|
||||
|
||||
**a) Validazione Credenziali**
|
||||
- Permette username/password vuoti per SQL Server con Windows Authentication
|
||||
- Riconosce "Integrated" e "Windows" come segnali per Windows Authentication
|
||||
- Validazione più specifica con messaggi di errore appropriati
|
||||
|
||||
#### Codice Modificato:
|
||||
|
||||
```csharp
|
||||
// Per SQL Server, permetti Windows Authentication
|
||||
bool isSqlServerWithWindowsAuth = currentDatabaseCredential.DatabaseType == DatabaseType.SqlServer &&
|
||||
(string.IsNullOrWhiteSpace(currentDatabaseCredential.Username) ||
|
||||
currentDatabaseCredential.Username.Equals("Integrated", StringComparison.OrdinalIgnoreCase) ||
|
||||
currentDatabaseCredential.Username.Equals("Windows", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!isSqlServerWithWindowsAuth)
|
||||
{
|
||||
// Per database che non usano Windows Authentication, richiedi username e password
|
||||
if (string.IsNullOrEmpty(currentDatabaseCredential.Username) ||
|
||||
string.IsNullOrEmpty(currentDatabaseCredential.Password))
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("alert",
|
||||
"Username e Password sono obbligatori. Per SQL Server con Windows Authentication, inserisci 'Integrated' come username.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📚 Guida Utilizzo
|
||||
|
||||
### Scenario 1: SQL Server Express Locale
|
||||
|
||||
**Configurazione Credenziale:**
|
||||
- **Host**: `localhost\SQLEXPRESS` o `.\SQLEXPRESS`
|
||||
- **Porta**: 1433 (ignorata)
|
||||
- **Database**: Nome del database (es. `MyDatabase`)
|
||||
- **Username**: `Integrated` o lascia vuoto
|
||||
- **Password**: Lascia vuoto
|
||||
|
||||
**Connection String Generata:**
|
||||
```
|
||||
Server=localhost\SQLEXPRESS;Integrated Security=True;Connection Timeout=30;Database=MyDatabase;TrustServerCertificate=True
|
||||
```
|
||||
|
||||
### Scenario 2: SQL Server LocalDB
|
||||
|
||||
**Configurazione Credenziale:**
|
||||
- **Host**: `(localdb)\MSSQLLocalDB`
|
||||
- **Porta**: 1433 (ignorata)
|
||||
- **Database**: Nome del database (es. `TestDB`)
|
||||
- **Username**: `Integrated` o lascia vuoto
|
||||
- **Password**: Lascia vuoto
|
||||
|
||||
**Connection String Generata:**
|
||||
```
|
||||
Server=(localdb)\MSSQLLocalDB;Integrated Security=True;Connection Timeout=30;Database=TestDB
|
||||
```
|
||||
|
||||
### Scenario 3: SQL Server Locale con SQL Authentication
|
||||
|
||||
**Configurazione Credenziale:**
|
||||
- **Host**: `localhost`
|
||||
- **Porta**: 1433
|
||||
- **Database**: Nome del database (es. `Production`)
|
||||
- **Username**: `sa` (o un altro utente SQL)
|
||||
- **Password**: Password dell'utente
|
||||
|
||||
**Connection String Generata:**
|
||||
```
|
||||
Server=localhost;User Id=sa;Password=***;Connection Timeout=30;Database=Production;TrustServerCertificate=True
|
||||
```
|
||||
|
||||
### Scenario 4: SQL Server Remoto
|
||||
|
||||
**Configurazione Credenziale:**
|
||||
- **Host**: `sql.example.com`
|
||||
- **Porta**: 1433 (o porta custom, es. 14330)
|
||||
- **Database**: Nome del database
|
||||
- **Username**: Utente SQL
|
||||
- **Password**: Password
|
||||
|
||||
**Connection String Generata:**
|
||||
```
|
||||
Server=sql.example.com,1433;User Id=username;Password=***;Connection Timeout=30;Database=DBName;TrustServerCertificate=True
|
||||
```
|
||||
|
||||
### Scenario 5: SQL Server con Instance Name Remoto
|
||||
|
||||
**Configurazione Credenziale:**
|
||||
- **Host**: `server.domain.com\PRODUCTION`
|
||||
- **Porta**: 1433 (ignorata)
|
||||
- **Database**: Nome del database
|
||||
- **Username**: Utente SQL
|
||||
- **Password**: Password
|
||||
|
||||
**Connection String Generata:**
|
||||
```
|
||||
Server=server.domain.com\PRODUCTION;User Id=username;Password=***;Connection Timeout=30;Database=DBName;TrustServerCertificate=True
|
||||
```
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Problema: "A network-related or instance-specific error"
|
||||
|
||||
**Possibili Cause:**
|
||||
1. **SQL Server Browser non in esecuzione** (per named instances)
|
||||
- Soluzione: Avvia il servizio "SQL Server Browser" da services.msc
|
||||
|
||||
2. **TCP/IP non abilitato**
|
||||
- Soluzione: SQL Server Configuration Manager → Protocols → Enable TCP/IP
|
||||
|
||||
3. **Named Instance non specificata**
|
||||
- Soluzione: Usa `localhost\SQLEXPRESS` invece di solo `localhost`
|
||||
|
||||
4. **Firewall blocca la porta**
|
||||
- Soluzione: Aggiungi eccezione firewall per SQL Server
|
||||
|
||||
### Problema: "Login failed for user"
|
||||
|
||||
**Possibili Cause:**
|
||||
1. **Windows Authentication richiesta ma SQL Auth specificata**
|
||||
- Soluzione: Usa username `Integrated` o lascialo vuoto
|
||||
|
||||
2. **SQL Authentication non abilitata**
|
||||
- Soluzione: SQL Server Management Studio → Proprietà Server → Security → SQL Server and Windows Authentication mode
|
||||
|
||||
3. **Password errata**
|
||||
- Soluzione: Verifica la password
|
||||
|
||||
### Problema: "Cannot open database"
|
||||
|
||||
**Possibili Cause:**
|
||||
1. **Database non esiste**
|
||||
- Soluzione: Verifica il nome del database o lascia il campo vuoto per connetterti solo al server
|
||||
|
||||
2. **Permessi insufficienti**
|
||||
- Soluzione: Verifica che l'utente abbia accesso al database
|
||||
|
||||
## ✅ Test di Connessione
|
||||
|
||||
Dopo aver configurato la credenziale, usa il pulsante **"Testa Connessione"** per verificare:
|
||||
- ✅ Connection string corretta
|
||||
- ✅ SQL Server raggiungibile
|
||||
- ✅ Autenticazione riuscita
|
||||
- ✅ Database accessibile (se specificato)
|
||||
|
||||
Il test mostra:
|
||||
- Versione SQL Server
|
||||
- Host e porta usati
|
||||
- Database connesso
|
||||
- Timeout configurato
|
||||
|
||||
## 📝 Note Tecniche
|
||||
|
||||
### Differenze TCP/IP vs Named Pipes
|
||||
|
||||
**Named Pipes** (preferito per localhost):
|
||||
- Più veloce per connessioni locali
|
||||
- Non richiede SQL Server Browser
|
||||
- Usa IPC invece di network stack
|
||||
- Sintassi: `Server=localhost` o `Server=.`
|
||||
|
||||
**TCP/IP** (richiesto per remote):
|
||||
- Richiesto per connessioni remote
|
||||
- Richiede porta specifica
|
||||
- Richiede SQL Server Browser per named instances
|
||||
- Sintassi: `Server=hostname,port`
|
||||
|
||||
### Windows Authentication vs SQL Authentication
|
||||
|
||||
**Windows Authentication**:
|
||||
- ✅ Più sicuro (usa credenziali Windows)
|
||||
- ✅ No password nel codice
|
||||
- ✅ Single Sign-On
|
||||
- ❌ Richiede domain trust per remote
|
||||
|
||||
**SQL Authentication**:
|
||||
- ✅ Funziona sempre (anche cross-domain)
|
||||
- ✅ Credenziali specifiche per SQL Server
|
||||
- ❌ Password nel connection string
|
||||
- ❌ Deve essere abilitato in SQL Server
|
||||
|
||||
## 🔄 Retrocompatibilità
|
||||
|
||||
Le modifiche sono completamente retrocompatibili:
|
||||
- ✅ Connection string esistenti continuano a funzionare
|
||||
- ✅ Credenziali già salvate non richiedono modifiche
|
||||
- ✅ Comportamento default invariato per server remoti
|
||||
- ✅ Nessuna migrazione database richiesta
|
||||
|
||||
## 📊 Impatto Performance
|
||||
|
||||
**Miglioramenti**:
|
||||
- 🚀 Named Pipes più veloce di TCP/IP per localhost
|
||||
- 🚀 Riduzione overhead network stack
|
||||
- 🚀 Connection pooling più efficiente
|
||||
|
||||
**Nessun Impatto Negativo**:
|
||||
- ✅ Server remoti usano sempre TCP/IP (comportamento corretto)
|
||||
- ✅ Connection string ottimizzate per scenario specifico
|
||||
|
||||
---
|
||||
|
||||
**Sviluppatore**: Alessio Dalsanto
|
||||
**Issue**: Connessione localhost SQL Server
|
||||
**Status**: ✅ Risolto
|
||||
Reference in New Issue
Block a user