d042863a56
- Aggiunto supporto schedulazione con intervalli flessibili (secondi/minuti/ore/giorni/settimane/mesi) - Esteso modello ProfileSchedule con campi IntervalValue e IntervalUnit - Ottimizzato ScheduledJobService per controlli ogni 30s con esecuzione parallela - Implementata interfaccia UI completa con anteprima real-time in italiano - Aggiunta migrazione database AddIntervalSchedulingFields - Implementati metodi calcolo NextExecutionTime per intervalli - Aggiunta gestione tracking anti-duplicati e cleanup automatico - Creata documentazione completa (6 file, 2500+ righe) Modifiche tecniche: - ProfileSchedule.cs: Nuovi campi e metodi CalculateNextInterval/GetScheduleDescription - ScheduledJobService.cs: Ridotto check interval a 30s, aggiunto parallel processing - ProfileScheduleService.cs: Supporto calcolo intervalli in UpdateNextExecutionTimeAsync - Scheduling.razor: Aggiunta sezione UI per configurazione intervalli - Scheduling.razor.cs: Implementato GetIntervalPreview() e gestione stato campi
1157 lines
32 KiB
Markdown
1157 lines
32 KiB
Markdown
# 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 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:**
|
|
```csharp
|
|
// 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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 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
|
|
```csharp
|
|
// 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
|
|
```csharp
|
|
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
|
|
```csharp
|
|
// ✅ 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
|
|
```csharp
|
|
// ✅ 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
|
|
```csharp
|
|
// 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
|
|
```csharp
|
|
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
|
|
```csharp
|
|
/// <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
|
|
```csharp
|
|
[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
|
|
```csharp
|
|
[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
|
|
```csharp
|
|
[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
|
|
```csharp
|
|
[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
|
|
```csharp
|
|
[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
|
|
```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
|
|
```powershell
|
|
# 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
|
|
```csharp
|
|
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
|
|
```csharp
|
|
// Program.cs
|
|
builder.Services.AddDataProtection()
|
|
.PersistKeysToFileSystem(new DirectoryInfo(keyPath))
|
|
.SetApplicationName("DataCoupler")
|
|
.SetDefaultKeyLifetime(TimeSpan.FromDays(90));
|
|
```
|
|
|
|
### Validazione Input
|
|
|
|
#### SQL Injection Prevention
|
|
```csharp
|
|
// ✅ 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
|
|
```csharp
|
|
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("<", "<")
|
|
.Replace(">", ">")
|
|
.Replace("\"", """)
|
|
.Replace("'", "'")
|
|
.Replace("/", "/");
|
|
|
|
return input.Length > maxLength ? input.Substring(0, maxLength) : input;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Autenticazione e Autorizzazione
|
|
|
|
#### Connection Security
|
|
```csharp
|
|
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
|
|
```csharp
|
|
// 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
|
|
```csharp
|
|
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
|
|
```csharp
|
|
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
|
|
```csharp
|
|
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
|
|
```csharp
|
|
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`)**
|
|
```csharp
|
|
// 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**
|
|
```csharp
|
|
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**
|
|
```csharp
|
|
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**
|
|
```csharp
|
|
_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**
|
|
```csharp
|
|
// Program.cs
|
|
builder.Services.AddScoped<Data_Coupler.Services.IBackupService, Data_Coupler.Services.BackupService>();
|
|
```
|
|
|
|
**Routing e Navigazione**
|
|
```csharp
|
|
// 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.0
|
|
**Ultimo Aggiornamento**: Settembre 2024
|
|
**Framework**: .NET 9.0
|
|
**Sviluppatore**: Alessio Dalsanto |