Files
Data-Coupler/AGENTS.md
T
Alessio b75e57fe31 [Feature] Aggiunto supporto OAuth2 client_credentials per Salesforce
Implementato il flusso OAuth2 grant_type=client_credentials come alternativa
al flusso password gia' esistente per l'autenticazione Salesforce server-to-server.
La modifica e' completamente retrocompatibile (default rimane Password).

## Dettaglio modifiche

### CredentialManager/Models/CredentialModels.cs
- Aggiunto enum SalesforceGrantType con valori Password e ClientCredentials
- Aggiunta proprieta' GrantType (default: Password) su RestApiCredential
- Aggiunta proprieta' GrantType (default: Password) su SalesforceCredential

### DataConnection/REST/Configuration/RestServiceOptions.cs
- Aggiunta proprieta' SalesforceGrantType per passare il tipo di flusso al client

### DataConnection/REST/Implementations/SalesforceServiceClient.cs
- Iniettato ILogger<SalesforceServiceClient> con NullLogger come fallback
- Sostituiti ~165 Console.WriteLine con chiamate ILogger appropriate
  (LogDebug per dettagli, LogInformation per eventi, LogWarning/LogError per problemi)
- Aggiunto AuthenticateWithPasswordAsync: incapsula il flusso grant_type=password
- Aggiunto AuthenticateWithClientCredentialsAsync: implementa grant_type=client_credentials
  (richiede solo ClientId e ClientSecret, nessun utente, URL My Domain obbligatorio)
- Aggiunto SendTokenRequestAsync: helper condiviso per la POST al token endpoint
- Aggiornato AuthenticateAsync() override: instrada al flusso corretto in base a GrantType
- Rimosso modificatore static da NormalizeNumericValues (usava _logger, causava CS0120)

### Data_Coupler/Services/DataConnectionFactory.cs
- Mappatura del campo GrantType dalle opzioni Salesforce a RestServiceOptions
- Passaggio dell'ILogger al costruttore di SalesforceServiceClient

### CredentialManager/Services/CredentialService.cs
- SaveRestApiCredentialAsync (blocco Salesforce): serializza GrantType in AdditionalParameters
- SaveSalesforceCredentialAsync: aggiunto GrantType nel dizionario iniziale
- MapToRestApiCredential: deserializza GrantType da AdditionalParameters con Enum.TryParse
- MapToSalesforceCredential: idem per il tipo SalesforceCredential

### DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs
- TestSalesforceOAuthLogin aggiornato: per ClientCredentials invia solo client_id e
  client_secret (senza username/password/security_token); per Password comportamento invariato

### Data_Coupler/Pages/CredentialManagement.razor
- Aggiunto dropdown 'Tipo di Autenticazione OAuth2' nella sezione Salesforce
- I campi Username, Password e Security Token vengono nascosti quando si seleziona
  il flusso ClientCredentials
- Alert contestuale: warning My Domain URL per ClientCredentials, info per Password
- GrantType propagato correttamente in EditRestApiCredential e TestRestApiConnectionFromModal

### AGENTS.md
- Aggiunta sezione di documentazione per la nuova funzionalita' OAuth2 client_credentials
2026-05-24 23:11:22 +02:00

36 KiB

Data-Coupler: Sistema di Integrazione Dati Multi-Platform

📋 Project Overview

Data-Coupler è una soluzione enterprise di integrazione dati sviluppata in .NET 9.0 con architettura Blazor Server. Il sistema facilita il trasferimento e la sincronizzazione di dati tra diverse tipologie di sorgenti: database relazionali, REST API, file Excel/CSV.

🎯 Obiettivi del Progetto

  • Unificazione Data Sources: Connessione trasparente a database eterogenei e API REST
  • Trasferimenti Sicuri: Gestione crittografata delle credenziali cross-platform
  • Automazione: Sistema di scheduling per operazioni periodiche
  • Mappatura Intelligente: Sistema avanzato di mapping campi tra sorgenti diverse
  • Gestione Associazioni: Tracking delle chiavi per evitare duplicati e gestire relazioni
  • Backup e Ripristino: Sistema completo di backup/restore per configurazioni e dati
  • Amministrazione Avanzata: Interfaccia unificata per gestione sistema e sicurezza

🚀 NUOVE FUNZIONALITÀ - Salesforce OAuth2 Client Credentials Flow (2026)

Supporto grant_type=client_credentials per autenticazione server-to-server

Data Aggiornamento: 2026

Panoramica

Aggiunto supporto per il flusso OAuth2 client_credentials come alternativa al flusso password già esistente. Completamente retrocompatibile: il default rimane Password.

Enum SalesforceGrantType (in CredentialManager/Models/CredentialModels.cs)

public enum SalesforceGrantType
{
    Password,           // grant_type=password — richiede Username, Password, SecurityToken (+ClientId/ClientSecret)
    ClientCredentials   // grant_type=client_credentials — server-to-server, nessun utente
}

Differenze tra i flussi

Aspetto password client_credentials
Richiede Username/Password No
Richiede SecurityToken Sì (se non Connected App) No
ClientId / ClientSecret Opzionale Obbligatorio
Base URL login/test.salesforce.com My Domain URL (es. https://myorg.my.salesforce.com)
Utente Salesforce Necessario Integration User (assegnato nella Connected App)

File modificati

  • CredentialManager/Models/CredentialModels.cs — enum SalesforceGrantType, proprietà GrantType su RestApiCredential e SalesforceCredential
  • DataConnection/REST/Configuration/RestServiceOptions.cs — proprietà SalesforceGrantType
  • DataConnection/REST/Implementations/SalesforceServiceClient.csAuthenticateWithPasswordAsync, AuthenticateWithClientCredentialsAsync, SendTokenRequestAsync; ILogger iniettato; NormalizeNumericValues reso non-static
  • Data_Coupler/Services/DataConnectionFactory.cs — mapping GrantType, logger passato al client
  • CredentialManager/Services/CredentialService.csGrantType serializzato/deserializzato in AdditionalParameters JSON
  • DataConnection/CredentialManagement/Services/DataConnectionCredentialService.csTestSalesforceOAuthLogin instrada per GrantType
  • Data_Coupler/Pages/CredentialManagement.razor — dropdown "Tipo di Autenticazione OAuth2"; Username/Password/SecurityToken nascosti per ClientCredentials; warning My Domain URL

🚀 NUOVE FUNZIONALITÀ - Salesforce Optimizations (Febbraio 2026)

Salesforce Batch Describe via Composite API

Data Aggiornamento: Febbraio 2026

La discovery dei metadati Salesforce è stata ottimizzata tramite la Composite Batch API:

BatchDescribeSObjectsAsync (nuovo metodo privato in SalesforceServiceClient)

  • Raggruppa i nomi degli SObject in chunk da 25
  • Ogni chunk viene inviato come singola POST /services/data/vXX.0/composite/batch
  • I risultati vengono processati in parallelo via Task.WhenAll
  • Risparmio concreto: per 200 SObject, da 201 chiamate API a sole 9

Discovery Parallela in RESTMethod.cs

  • DiscoverEntitySummariesAsync (rapida, 1 chiamata) e DiscoverEntitiesAsync (batch) partono in parallelo
  • La lista entità diventa interattiva dopo ~0.3 s; i dettagli completano in background
  • StateHasChanged() chiamato dopo le summaries per aggiornare subito la UI

Fix Scheduler: External ID Relationships e Default Values

  • Bug 1 (DataCoupler.razor.cs): in entrambi i blocchi di update profilo esistente (riattivazione profilo inattivo + sovrascrittura profilo attivo), i campi ExternalIdRelationshipsJson e DefaultValuesJson venivano omessi nella copia → cancellati silenziosamente ad ogni re-salvataggio
  • Bug 2 (ScheduledProfileExecutionService.cs): TransformRecordForRest non escludeva i campi sorgente usati nelle External ID Relationships dal loop di mapping normale, causando dati duplicati nell'entità destinazione (stessa logica già presente nella UI manuale, ora allineata allo scheduler)

🚀 NUOVE FUNZIONALITÀ - Salesforce Batch Extraction

Miglioramenti Significativi alle Performance REST

Data Aggiornamento: Settembre 2024

Il sistema SalesforceServiceClient è stato completamente potenziato con funzionalità avanzate per l'estrazione batch di oggetti REST:

Nuovi Metodi Implementati:

  1. BatchExecuteQueriesAsync - Esecuzione parallela di multiple query SOQL

    • Utilizza Salesforce Composite API (max 25 query per batch)
    • Processing parallelo automatico dei batch
    • Performance: 10-25x più veloce per grandi dataset
  2. BatchFindEntitiesByKeysAsync - Ricerca batch di entità multiple

    • Ricerca simultanea con diverse combinazioni di chiavi
    • Riduzione drastica delle chiamate API (60-90% in meno)
  3. BatchGetEntitiesByIdsAsync - Recupero batch tramite ID

    • Query ottimizzate con clausole IN (max 200 ID per query)
    • Gestione automatica di migliaia di ID
  4. ExtractAllEntitiesAsync - Estrazione completa con paginazione

    • Paginazione automatica usando nextRecordsUrl
    • Auto-discovery campi disponibili
    • Gestione intelligente della memoria
  5. ExtractEntitiesParallelAsync - Estrazione parallela avanzata

    • Split automatico per criteri (date, tipo, ecc.)
    • Deduplicazione automatica basata su ID
    • Processing parallelo ottimizzato
  6. ExtractLargeDatasetAsync - Estrattore intelligente

    • Auto-detect dimensione dataset (>10K = parallelo)
    • Strategia adattiva (sequenziale vs parallelo)
    • Chunking automatico basato su date
  7. ExtractRecentlyModifiedAsync - Sincronizzazione incrementale

    • Estrazione ottimizzata per record modificati di recente
    • Configurabile (ore/giorni indietro)

Utilità e Helper Methods:

  • CreateDateBasedWhereClauses: Genera automaticamente chunk temporali
  • Error Handling Avanzato: Isolamento errori batch, fallback graceful
  • Memory Management: Streaming processing per evitare OutOfMemory

Risultati Performance:

  • 🚀 Velocità: 10-25x miglioramento per grandi dataset
  • 📉 API Calls: Riduzione 60-90% chiamate API
  • 💾 Memoria: Gestione ottimizzata con chunking
  • 🔄 Affidabilità: Retry automatico e gestione errori robusta

Esempi di Utilizzo:

// Estrazione completa con paginazione automatica
var allAccounts = await salesforceClient.ExtractAllEntitiesAsync("Account");

// Estrazione parallela per grandi dataset
var largeData = await salesforceClient.ExtractLargeDatasetAsync("Case", maxRecords: 100000);

// Sincronizzazione incrementale (ultime 24 ore)
var recent = await salesforceClient.ExtractRecentlyModifiedAsync("Contact", hoursBack: 24);

// Ricerca batch multiple email
var searchResults = await salesforceClient.BatchFindEntitiesByKeysAsync("Contact", emailList);

Documentazione Completa: SALESFORCE_BATCH_EXTRACTION_IMPROVEMENTS.md
Esempi Pratici: DataConnection\REST\Examples\SalesforceBatchExtractionExamples.cs

🏗️ Architettura del Sistema

Struttura Modulare

Data-Coupler/
├── Data_Coupler/          # 🎯 Applicazione principale Blazor Server
├── DataConnection/        # 🔌 Libreria core per connessioni dati
├── CredentialManager/     # 🔐 Gestione sicura credenziali
├── Components/           # 🧩 Componenti UI riutilizzabili
└── DatabaseUpdater/      # 🔧 Utility per manutenzione database

Stack Tecnologico

  • .NET 9.0: Framework principale
  • Blazor Server: Framework UI reattivo
  • Entity Framework Core: ORM per accesso dati
  • SQLite: Database embedded per configurazioni
  • Bootstrap 5: Framework CSS responsive

Database Supportati

  • SQL Server, MySQL, PostgreSQL, Oracle, SQLite, IBM DB2, SAP HANA

REST API Supportate

  • Generic REST APIs, Salesforce, SAP Business One Service Layer

🔨 Build and Test Commands

Prerequisiti

  • .NET 9.0 SDK
  • Git per clone del repository
  • Editor: Visual Studio 2022, VS Code, o Rider

Setup Ambiente di Sviluppo

# Clone del repository
git clone https://github.com/AlessioDalsi/Data-Coupler.git
cd Data-Coupler

# Restore dei pacchetti NuGet
dotnet restore Data_Coupler.sln

Build Commands

Build Development

# Build completo della soluzione
dotnet build Data_Coupler.sln

# Build singolo progetto
dotnet build Data_Coupler/Data_Coupler.csproj
dotnet build DataConnection/DataConnection.csproj
dotnet build CredentialManager/CredentialManager.csproj

Build Production

# Build release
dotnet build Data_Coupler.sln --configuration Release

# Publish per deployment
dotnet publish Data_Coupler/Data_Coupler.csproj \
  --configuration Release \
  --output ./publish \
  --self-contained true \
  --runtime win-x64

Build per Diverse Piattaforme

# Windows x64
dotnet publish --runtime win-x64 --configuration Release

# Linux x64
dotnet publish --runtime linux-x64 --configuration Release

# macOS x64
dotnet publish --runtime osx-x64 --configuration Release

# macOS ARM64 (Apple Silicon)
dotnet publish --runtime osx-arm64 --configuration Release

Run Commands

Sviluppo Locale

# Avvio applicazione principale
dotnet run --project Data_Coupler/Data_Coupler.csproj

# Avvio con hot reload
dotnet watch run --project Data_Coupler/Data_Coupler.csproj

# Avvio su porta specifica
dotnet run --project Data_Coupler/Data_Coupler.csproj --urls "http://localhost:8080"

Utility e Manutenzione

# Aggiornamento database
dotnet run --project DatabaseUpdater/DatabaseUpdater.csproj

# Creazione migrazione Entity Framework
cd CredentialManager
dotnet ef migrations add [MigrationName]
dotnet ef database update

Test Commands

Esecuzione Test Unitari

# Esecuzione tutti i test
dotnet test Data_Coupler.sln

# Test con verbosità dettagliata
dotnet test Data_Coupler.sln --verbosity detailed

# Test con coverage
dotnet test Data_Coupler.sln --collect:"XPlat Code Coverage"

# Test specifici per progetto
dotnet test DataConnection.Tests/DataConnection.Tests.csproj
dotnet test CredentialManager.Tests/CredentialManager.Tests.csproj

Test di Integrazione

# Test di integrazione database
dotnet test --filter "Category=Integration"

# Test connessioni REST
dotnet test --filter "Category=RestIntegration"

# Test end-to-end
dotnet test --filter "Category=E2E"

Performance Testing

# Benchmark performance
dotnet run --project PerformanceTests/PerformanceTests.csproj --configuration Release

# Memory profiling
dotnet run --project Data_Coupler/Data_Coupler.csproj --configuration Release
# Utilizzare strumenti come dotMemory o PerfView

📝 Code Style Guidelines

Convenzioni di Naming

C# Naming Conventions

// Classes e Interfaces: PascalCase
public class DataConnectionFactory { }
public interface IDatabaseManager { }

// Methods: PascalCase
public async Task<List<Data>> GetDataAsync() { }

// Properties: PascalCase
public string ConnectionString { get; set; }

// Private fields: camelCase con underscore
private readonly ILogger _logger;

// Local variables e parameters: camelCase
public void ProcessData(string connectionName, int batchSize) { }

// Constants: PascalCase
public const string DefaultTimeout = "30";

File e Directory Naming

// Cartelle: PascalCase
Services/
Models/
Extensions/

// File C#: PascalCase con classe principale
DatabaseConnectionService.cs
CredentialEntity.cs

// File Razor: PascalCase
DataCoupler.razor
ProfileManagement.razor

Organizzazione Codice

Structure dei File

using System;                    // System namespaces
using System.Collections.Generic;
using Microsoft.Extensions;      // Microsoft namespaces
using DataConnection.Interfaces; // Project namespaces
using ThirdParty.Library;       // Third-party namespaces

namespace Data_Coupler.Services
{
    /// <summary>
    /// Descrizione chiara della classe
    /// </summary>
    public class ExampleService : IExampleService
    {
        #region Private Fields
        private readonly ILogger<ExampleService> _logger;
        #endregion

        #region Constructor
        public ExampleService(ILogger<ExampleService> logger)
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }
        #endregion

        #region Public Methods
        public async Task<Result> ProcessAsync(Request request)
        {
            // Implementation
        }
        #endregion

        #region Private Methods
        private void ValidateRequest(Request request)
        {
            // Validation logic
        }
        #endregion
    }
}

Async/Await Pattern

// ✅ Corretto
public async Task<List<Data>> GetDataAsync(CancellationToken cancellationToken = default)
{
    var data = await _repository.GetDataAsync(cancellationToken);
    return data.ToList();
}

// ❌ Evitare
public async Task<List<Data>> GetData()
{
    var data = await _repository.GetDataAsync().Result; // Blocking call
    return data.ToList();
}

Error Handling

// ✅ Gestione errori strutturata
public async Task<Result<Data>> ProcessDataAsync(string input)
{
    try
    {
        if (string.IsNullOrWhiteSpace(input))
            return Result<Data>.Failure("Input cannot be empty");

        var data = await _service.ProcessAsync(input);
        return Result<Data>.Success(data);
    }
    catch (ValidationException ex)
    {
        _logger.LogWarning(ex, "Validation failed for input: {Input}", input);
        return Result<Data>.Failure(ex.Message);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Unexpected error processing input: {Input}", input);
        return Result<Data>.Failure("An unexpected error occurred");
    }
}

Dependency Injection Guidelines

Service Registration

// Program.cs - Registrazione servizi
// Singleton per servizi stateless
builder.Services.AddSingleton<IEncryptionService, EncryptionService>();

// Scoped per servizi con stato per request
builder.Services.AddScoped<IDataConnectionFactory, DataConnectionFactory>();

// Transient per servizi leggeri
builder.Services.AddTransient<IValidator<Model>, ModelValidator>();

Constructor Injection

public class ProfileService
{
    private readonly IDbContext _context;
    private readonly ILogger<ProfileService> _logger;
    private readonly IOptions<AppSettings> _options;

    public ProfileService(
        IDbContext context,
        ILogger<ProfileService> logger,
        IOptions<AppSettings> options)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _options = options ?? throw new ArgumentNullException(nameof(options));
    }
}

Commenti e Documentazione

XML Documentation

/// <summary>
/// Trasferisce dati da una sorgente a una destinazione utilizzando il mapping specificato
/// </summary>
/// <param name="sourceConnection">Connessione alla sorgente dati</param>
/// <param name="destinationConnection">Connessione alla destinazione</param>
/// <param name="mapping">Configurazione mapping campi</param>
/// <param name="cancellationToken">Token per cancellazione operazione</param>
/// <returns>Risultato del trasferimento con statistiche</returns>
/// <exception cref="ArgumentNullException">Lanciata quando un parametro richiesto è null</exception>
/// <exception cref="InvalidOperationException">Lanciata quando le connessioni non sono valide</exception>
public async Task<TransferResult> TransferDataAsync(
    IDataConnection sourceConnection,
    IDataConnection destinationConnection,
    FieldMapping mapping,
    CancellationToken cancellationToken = default)
{
    // Implementation
}

🧪 Testing Instructions

Struttura Test

Test Project Organization

Tests/
├── Unit/                        # Test unitari
│   ├── Data_Coupler.Tests/
│   ├── DataConnection.Tests/
│   └── CredentialManager.Tests/
├── Integration/                 # Test di integrazione
│   ├── Database.Integration.Tests/
│   └── API.Integration.Tests/
└── E2E/                        # Test end-to-end
    └── UI.Tests/

Test Unitari

Setup Test Class

[TestClass]
public class CredentialServiceTests
{
    private Mock<IDbContext> _mockContext;
    private Mock<IEncryptionService> _mockEncryption;
    private Mock<ILogger<CredentialService>> _mockLogger;
    private CredentialService _service;

    [TestInitialize]
    public void Setup()
    {
        _mockContext = new Mock<IDbContext>();
        _mockEncryption = new Mock<IEncryptionService>();
        _mockLogger = new Mock<ILogger<CredentialService>>();
        
        _service = new CredentialService(
            _mockContext.Object,
            _mockEncryption.Object,
            _mockLogger.Object);
    }

    [TestCleanup]
    public void Cleanup()
    {
        _service?.Dispose();
    }
}

Test Methods Pattern

[TestMethod]
public async Task GetCredentialAsync_WithValidName_ReturnsCredential()
{
    // Arrange
    var credentialName = "TestCredential";
    var expectedCredential = new CredentialEntity { Name = credentialName };
    
    _mockContext.Setup(x => x.Credentials.FirstOrDefaultAsync(
        It.IsAny<Expression<Func<CredentialEntity, bool>>>(),
        It.IsAny<CancellationToken>()))
        .ReturnsAsync(expectedCredential);

    // Act
    var result = await _service.GetCredentialAsync(credentialName);

    // Assert
    Assert.IsNotNull(result);
    Assert.AreEqual(credentialName, result.Name);
    _mockContext.Verify(x => x.Credentials.FirstOrDefaultAsync(
        It.IsAny<Expression<Func<CredentialEntity, bool>>>(),
        It.IsAny<CancellationToken>()), Times.Once);
}

[TestMethod]
public async Task GetCredentialAsync_WithNullName_ThrowsArgumentNullException()
{
    // Act & Assert
    await Assert.ThrowsExceptionAsync<ArgumentNullException>(
        () => _service.GetCredentialAsync(null));
}

Test di Integrazione

Database Integration Tests

[TestClass]
public class DatabaseIntegrationTests
{
    private TestDatabase _testDb;
    private IDatabaseManager _databaseManager;

    [TestInitialize]
    public async Task Setup()
    {
        _testDb = await TestDatabase.CreateAsync();
        _databaseManager = new EFCoreDatabaseManager(_testDb.ConnectionString);
    }

    [TestCleanup]
    public async Task Cleanup()
    {
        await _testDb.DisposeAsync();
    }

    [TestMethod]
    public async Task ExecuteQueryAsync_WithValidQuery_ReturnsData()
    {
        // Arrange
        await _testDb.SeedDataAsync();
        var query = "SELECT * FROM TestTable WHERE Id = 1";

        // Act
        var result = await _databaseManager.ExecuteQueryAsync<TestEntity>(query);

        // Assert
        Assert.IsNotNull(result);
        Assert.IsTrue(result.Any());
    }
}

REST API Integration Tests

[TestClass]
public class RestApiIntegrationTests
{
    private TestServer _testServer;
    private HttpClient _client;

    [TestInitialize]
    public void Setup()
    {
        var builder = WebApplication.CreateBuilder();
        // Configure test server
        _testServer = new TestServer(builder);
        _client = _testServer.CreateClient();
    }

    [TestMethod]
    public async Task GetData_WithValidCredentials_ReturnsSuccess()
    {
        // Arrange
        var apiClient = new GenericRestServiceClient(_client);
        
        // Act
        var result = await apiClient.GetAsync<ApiResponse>("/api/data");
        
        // Assert
        Assert.IsNotNull(result);
        Assert.IsTrue(result.Success);
    }
}

Test End-to-End

Playwright UI Tests

[TestClass]
public class UITests
{
    private IPlaywright _playwright;
    private IBrowser _browser;
    private IPage _page;

    [TestInitialize]
    public async Task Setup()
    {
        _playwright = await Playwright.CreateAsync();
        _browser = await _playwright.Chromium.LaunchAsync();
        _page = await _browser.NewPageAsync();
    }

    [TestMethod]
    public async Task DataTransfer_CompleteWorkflow_Success()
    {
        // Navigate to application
        await _page.GotoAsync("http://localhost:7550");
        
        // Configure source connection
        await _page.ClickAsync("#source-database-tab");
        await _page.FillAsync("#connection-name", "TestConnection");
        
        // Configure mapping
        await _page.ClickAsync("#next-step");
        
        // Execute transfer
        await _page.ClickAsync("#execute-transfer");
        
        // Verify results
        var successMessage = await _page.WaitForSelectorAsync(".success-message");
        Assert.IsNotNull(successMessage);
    }
}

Test Configuration

appsettings.Test.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=:memory:",
    "TestDatabase": "Server=(localdb)\\mssqllocaldb;Database=DataCouplerTest;Trusted_Connection=true;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "Microsoft": "Warning"
    }
  },
  "TestSettings": {
    "UseInMemoryDatabase": true,
    "MockExternalServices": true
  }
}

Test Execution Scripts

PowerShell Test Runner

# run-tests.ps1
param(
    [string]$Configuration = "Debug",
    [string]$Filter = "",
    [switch]$Coverage
)

Write-Host "Running tests..." -ForegroundColor Green

if ($Coverage) {
    dotnet test Data_Coupler.sln `
        --configuration $Configuration `
        --collect:"XPlat Code Coverage" `
        --results-directory TestResults `
        --filter $Filter
        
    # Generate coverage report
    reportgenerator `
        -reports:"TestResults/*/coverage.cobertura.xml" `
        -targetdir:"TestResults/CoverageReport" `
        -reporttypes:Html
} else {
    dotnet test Data_Coupler.sln `
        --configuration $Configuration `
        --filter $Filter
}

🔒 Security Considerations

Gestione Credenziali

Crittografia dei Dati Sensibili

public class EncryptionService : IEncryptionService
{
    private readonly IDataProtector _protector;

    public EncryptionService(IDataProtectionProvider provider)
    {
        _protector = provider.CreateProtector("DataCoupler.Credentials.v1");
    }

    public string Encrypt(string plaintext)
    {
        if (string.IsNullOrEmpty(plaintext))
            return plaintext;
            
        return _protector.Protect(plaintext);
    }

    public string Decrypt(string ciphertext)
    {
        if (string.IsNullOrEmpty(ciphertext))
            return ciphertext;
            
        try
        {
            return _protector.Unprotect(ciphertext);
        }
        catch (CryptographicException)
        {
            // Handle decryption failure
            throw new SecurityException("Failed to decrypt data");
        }
    }
}

Configurazione Data Protection

// Program.cs
builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(keyPath))
    .SetApplicationName("DataCoupler")
    .SetDefaultKeyLifetime(TimeSpan.FromDays(90));

Validazione Input

SQL Injection Prevention

// ✅ Parametrized Queries
public async Task<List<Entity>> GetDataAsync(string tableName, int id)
{
    // Validate table name against whitelist
    if (!IsValidTableName(tableName))
        throw new ArgumentException("Invalid table name");
    
    var query = $"SELECT * FROM [{tableName}] WHERE Id = @id";
    var parameters = new { id };
    
    return await _connection.QueryAsync<Entity>(query, parameters);
}

// ❌ Mai utilizzare string concatenation
public async Task<List<Entity>> GetDataUnsafe(string tableName, int id)
{
    var query = $"SELECT * FROM {tableName} WHERE Id = {id}"; // VULNERABLE
    return await _connection.QueryAsync<Entity>(query);
}

Input Sanitization

public class InputValidator
{
    private static readonly Regex AllowedTableName = new(@"^[a-zA-Z][a-zA-Z0-9_]*$");
    private static readonly Regex AllowedFieldName = new(@"^[a-zA-Z][a-zA-Z0-9_]*$");

    public static bool IsValidTableName(string tableName)
    {
        return !string.IsNullOrWhiteSpace(tableName) && 
               tableName.Length <= 128 &&
               AllowedTableName.IsMatch(tableName);
    }

    public static bool IsValidFieldName(string fieldName)
    {
        return !string.IsNullOrWhiteSpace(fieldName) && 
               fieldName.Length <= 128 &&
               AllowedFieldName.IsMatch(fieldName);
    }

    public static string SanitizeStringInput(string input, int maxLength = 255)
    {
        if (string.IsNullOrEmpty(input))
            return string.Empty;

        // Remove potential XSS characters
        input = input.Replace("<", "&lt;")
                    .Replace(">", "&gt;")
                    .Replace("\"", "&quot;")
                    .Replace("'", "&#x27;")
                    .Replace("/", "&#x2F;");

        return input.Length > maxLength ? input.Substring(0, maxLength) : input;
    }
}

Autenticazione e Autorizzazione

Connection Security

public class SecureConnectionFactory : IDataConnectionFactory
{
    public async Task<IDatabaseManager> CreateDatabaseManagerAsync(string credentialName)
    {
        var credential = await _credentialService.GetCredentialAsync(credentialName);
        if (credential == null)
            throw new UnauthorizedAccessException("Credential not found");

        // Validate credential permissions
        if (!await ValidateCredentialPermissions(credential))
            throw new UnauthorizedAccessException("Insufficient permissions");

        // Create connection with timeout
        var connectionString = BuildSecureConnectionString(credential);
        return new DatabaseManager(connectionString);
    }

    private string BuildSecureConnectionString(CredentialEntity credential)
    {
        var builder = new SqlConnectionStringBuilder
        {
            DataSource = credential.Host,
            InitialCatalog = credential.DatabaseName,
            UserID = credential.Username,
            Password = _encryptionService.Decrypt(credential.EncryptedPassword),
            
            // Security settings
            Encrypt = true,
            TrustServerCertificate = false,
            ConnectTimeout = 30,
            CommandTimeout = 300
        };

        return builder.ConnectionString;
    }
}

HTTPS e Comunicazioni Sicure

HTTPS Configuration

// Program.cs
if (!app.Environment.IsDevelopment())
{
    app.UseHsts();
    app.UseHttpsRedirection();
}

// Force HTTPS in production
app.Use(async (context, next) =>
{
    if (!context.Request.IsHttps && !app.Environment.IsDevelopment())
    {
        var httpsUrl = $"https://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}";
        context.Response.Redirect(httpsUrl, permanent: true);
        return;
    }
    await next();
});

HTTP Client Security

public class SecureHttpClientFactory
{
    public HttpClient CreateSecureClient(RestCredentialSettings settings)
    {
        var handler = new HttpClientHandler();
        
        if (!settings.IgnoreSslErrors)
        {
            handler.ServerCertificateCustomValidationCallback = 
                (sender, cert, chain, sslPolicyErrors) =>
                {
                    // Validate certificate chain
                    return sslPolicyErrors == SslPolicyErrors.None;
                };
        }

        var client = new HttpClient(handler)
        {
            Timeout = TimeSpan.FromSeconds(settings.TimeoutSeconds)
        };

        // Add security headers
        client.DefaultRequestHeaders.Add("User-Agent", "DataCoupler/1.0");
        client.DefaultRequestHeaders.Add("X-Requested-With", "XMLHttpRequest");

        return client;
    }
}

Logging e Audit

Secure Logging

public class SecurityAuditLogger
{
    private readonly ILogger<SecurityAuditLogger> _logger;

    public void LogCredentialAccess(string credentialName, string operation, string userId)
    {
        _logger.LogInformation(
            "Credential Access: {Operation} on {CredentialName} by {UserId} at {Timestamp}",
            operation, credentialName, userId, DateTime.UtcNow);
    }

    public void LogDataTransfer(string sourceType, string destinationType, int recordCount, string userId)
    {
        _logger.LogInformation(
            "Data Transfer: {RecordCount} records from {SourceType} to {DestinationType} by {UserId} at {Timestamp}",
            recordCount, sourceType, destinationType, userId, DateTime.UtcNow);
    }

    public void LogSecurityEvent(string eventType, string details, string userId)
    {
        _logger.LogWarning(
            "Security Event: {EventType} - {Details} by {UserId} at {Timestamp}",
            eventType, details, userId, DateTime.UtcNow);
    }
}

Error Handling Sicuro

Information Disclosure Prevention

public class SecureErrorHandler
{
    private readonly ILogger<SecureErrorHandler> _logger;

    public ErrorResponse HandleError(Exception ex, bool isDevelopment)
    {
        // Log full error details
        _logger.LogError(ex, "Error occurred: {Message}", ex.Message);

        // Return sanitized error to client
        if (isDevelopment)
        {
            return new ErrorResponse
            {
                Message = ex.Message,
                StackTrace = ex.StackTrace,
                Type = ex.GetType().Name
            };
        }

        // Production: return generic message
        return new ErrorResponse
        {
            Message = "An error occurred while processing your request",
            ErrorId = Guid.NewGuid().ToString() // For correlation
        };
    }
}

Database Security

Connection Security

public class DatabaseSecuritySettings
{
    public static SqlConnectionStringBuilder CreateSecureConnectionString(
        string server, string database, string username, string password)
    {
        return new SqlConnectionStringBuilder
        {
            DataSource = server,
            InitialCatalog = database,
            UserID = username,
            Password = password,
            
            // Security settings
            Encrypt = true,
            TrustServerCertificate = false,
            IntegratedSecurity = false,
            MultipleActiveResultSets = false,
            
            // Timeout settings
            ConnectTimeout = 30,
            CommandTimeout = 300,
            
            // Connection pooling
            Pooling = true,
            MinPoolSize = 0,
            MaxPoolSize = 100
        };
    }
}

Sistema di Backup e Impostazioni

Architettura del Sistema di Backup

Il sistema implementa un approccio modulare per il backup e ripristino dei dati critici:

Componenti Principali

BackupService (Data_Coupler.Services.BackupService)

  • Interfaccia: IBackupService con metodi asincroni per export/import
  • Serializzazione: Utilizza System.Text.Json per formato JSON leggibile
  • Sicurezza: Esclude automaticamente password e chiavi API dai backup
  • Transazioni: Operazioni di import wrapped in transazioni per integrità dati

Modelli di Backup (Data_Coupler.Models.BackupModels)

// Struttura principale del backup
public class SystemBackupData
{
    public BackupMetadata Metadata { get; set; }
    public List<DataCouplerProfileBackup> Profiles { get; set; }
    public List<CredentialBackup> Credentials { get; set; }
    public List<KeyAssociationBackup> KeyAssociations { get; set; }
    public List<ProfileScheduleBackup> Schedules { get; set; }
}

// Metadati per versioning e validazione
public class BackupMetadata
{
    public string Version { get; set; } = "1.0";
    public DateTime CreatedAt { get; set; }
    public string CreatedBy { get; set; }
    public List<string> IncludedComponents { get; set; }
    public string Description { get; set; }
}

Funzionalità di Export

Configurazione Flessibile

var options = new BackupOptions
{
    IncludeProfiles = true,
    IncludeCredentials = true,
    IncludeKeyAssociations = true,
    IncludeSchedules = true,
    ActiveRecordsOnly = false,
    Description = "Backup completo sistema"
};

var result = await backupService.ExportBackupAsync(options);

Componenti Supportati:

  • Profili Data Coupler: Configurazioni complete di trasferimento dati
  • Credenziali: Informazioni di connessione (senza dati sensibili)
  • Associazioni Chiavi: Mapping tra chiavi sorgente e destinazione
  • Schedulazioni: Configurazioni di esecuzione automatica

Funzionalità di Import

Validazione Rigorosa

  • Controllo formato e versione del file backup
  • Validazione integrità dati JSON
  • Verifica compatibilità componenti

Modalità di Ripristino

  • Overwrite: Sostituisce dati esistenti
  • Merge: Integra con dati esistenti
  • Preview: Mostra cosa verrà importato senza eseguire

Interfaccia Settings

Struttura a Tab Organizzata

Tab Backup

  • Export selettivo per componente
  • Import con validazione file
  • Storico backup con timestamp
  • Anteprima contenuto backup

Tab Sistema

  • Statistiche database in tempo reale
  • Configurazioni performance (batch size, timeout)
  • Informazioni sistema (versione, framework, OS)
  • Monitoraggio utilizzo memoria

Tab Sicurezza

  • Gestione crittografia credenziali
  • Configurazioni audit logging
  • Impostazioni connessioni sicure (HTTPS, SSL)
  • Backup automatici crittografati

Tab Manutenzione

  • Ottimizzazione database automatica
  • Pulizia file temporanei e log
  • Monitoraggio performance (CPU, memoria, connessioni)
  • Schedulazione manutenzione automatica
  • Storico operazioni manutenzione

Implementazione Sicurezza

Esclusione Dati Sensibili

public class CredentialBackup
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string ConnectionType { get; set; }
    public string Server { get; set; }
    public int Port { get; set; }
    public string Database { get; set; }
    public string Username { get; set; }
    // Password e ApiKey intenzionalmente esclusi
    public bool IsActive { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }
}

Logging Operazioni

_logger.LogInformation("Backup export started by {User} with options: {Options}", 
    Environment.UserName, JsonSerializer.Serialize(options));

_logger.LogInformation("Backup import completed: {ProfilesCount} profiles, {CredentialsCount} credentials restored",
    profilesRestored, credentialsRestored);

Registrazione Servizi

Dependency Injection Setup

// Program.cs
builder.Services.AddScoped<Data_Coupler.Services.IBackupService, Data_Coupler.Services.BackupService>();

Routing e Navigazione

// NavMenu.razor
<NavLink class="nav-link" href="settings">
    <span class="oi oi-cog" aria-hidden="true"></span> Impostazioni
</NavLink>

Best Practices Implementate

Error Handling Robusto

  • Try-catch con logging specifico per ogni operazione
  • Rollback automatico in caso di errore durante import
  • Messaggi di errore user-friendly con dettagli tecnici nei log

UX/UI Responsiva

  • Indicatori di progresso per operazioni lunghe
  • Toast notifications per feedback immediato
  • Validazione client-side per upload file
  • Design responsive con Bootstrap 5

Performance Ottimizzata

  • Operazioni asincrone per non bloccare UI
  • Batch processing per grandi dataset
  • Lazy loading per componenti tab non attivi

Versione: 1.1
Ultimo Aggiornamento: 20 Febbraio 2026
Framework: .NET 9.0
Sviluppatore: Alessio Dalsanto