ASP.NET Core + MySQL

Ghid Complet ASP.NET Core + Entity Framework Core + MySQL

Învață să construiești aplicații enterprise moderne cu arhitectură N-Tier, performanță optimizată, securitate avansată și cele mai bune practici din industrie. De la concepte fundamentale până la deployment în producție.

.NET 8 EF Core 8 MySQL 8.4 Clean Architecture Docker CI/CD

🚀 Introducere în ASP.NET Core

ASP.NET Core este framework-ul modern, cross-platform și open-source de la Microsoft pentru construirea aplicațiilor web de înaltă performanță. Combinat cu Entity Framework Core și MySQL, oferă o soluție completă pentru dezvoltarea aplicațiilor enterprise.

De ce ASP.NET Core?

🚀 Performanță

Unul dintre cele mai rapide framework-uri web, optimizat pentru cloud și microservicii.

🌍 Cross-Platform

Rulează pe Windows, Linux și macOS. Deploy oriunde: Azure, AWS, Google Cloud, Docker.

🛠️ Ecosistem Bogat

Dependency Injection integrat, middleware pipeline, configurare flexibilă, tooling excelent.

Notă pentru începători

Acest ghid presupune cunoștințe de bază C# și concepte OOP. Dacă ești nou în .NET, recomandăm să începi cu documentația oficială Microsoft Learn.

⚙️ Setup & Configurare

Cerințe Sistem

  • .NET 8 SDK sau mai nou
  • Visual Studio 2022 / VS Code / Rider
  • MySQL 8.0+ sau Docker Desktop
  • Git pentru version control

Creare Proiect Nou

bash
# Creează soluție nouă
dotnet new sln -n MyApp

# Creează proiecte pentru fiecare layer
dotnet new webapi -n MyApp.Api -f net8.0
dotnet new classlib -n MyApp.Application -f net8.0
dotnet new classlib -n MyApp.Domain -f net8.0
dotnet new classlib -n MyApp.Infrastructure -f net8.0

# Adaugă proiectele la soluție
dotnet sln add MyApp.Api/MyApp.Api.csproj
dotnet sln add MyApp.Application/MyApp.Application.csproj
dotnet sln add MyApp.Domain/MyApp.Domain.csproj
dotnet sln add MyApp.Infrastructure/MyApp.Infrastructure.csproj

# Adaugă referințe între proiecte
dotnet add MyApp.Api reference MyApp.Application
dotnet add MyApp.Application reference MyApp.Domain
dotnet add MyApp.Infrastructure reference MyApp.Domain
dotnet add MyApp.Api reference MyApp.Infrastructure

Instalare Pachete NuGet

bash
# În proiectul Infrastructure
cd MyApp.Infrastructure
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Pomelo.EntityFrameworkCore.MySql
dotnet add package Microsoft.Extensions.Configuration.Abstractions

# În proiectul Api
cd ../MyApp.Api
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Swashbuckle.AspNetCore
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Serilog.AspNetCore

FluentValidation pentru Reguli Complexe

csharp
// MyApp.Application/Validators/ProductValidators.cs
using FluentValidation;
using MyApp.Application.DTOs;
using MyApp.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;

namespace MyApp.Application.Validators;

public class CreateProductValidator : AbstractValidator
{
    private readonly ApplicationDbContext _context;

    public CreateProductValidator(ApplicationDbContext context)
    {
        _context = context;

        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Product name is required")
            .Length(3, 200).WithMessage("Product name must be between 3 and 200 characters")
            .MustAsync(BeUniqueName).WithMessage("A product with this name already exists");

        RuleFor(x => x.SKU)
            .NotEmpty().WithMessage("SKU is required")
            .Matches(@"^[A-Z0-9\-]+$").WithMessage("SKU must contain only uppercase letters, numbers, and hyphens")
            .Length(3, 50).WithMessage("SKU must be between 3 and 50 characters")
            .MustAsync(BeUniqueSKU).WithMessage("This SKU is already in use");

        RuleFor(x => x.Price)
            .GreaterThan(0).WithMessage("Price must be greater than 0")
            .LessThanOrEqualTo(999999.99m).WithMessage("Price cannot exceed 999,999.99");

        RuleFor(x => x.CompareAtPrice)
            .GreaterThan(x => x.Price)
            .When(x => x.CompareAtPrice.HasValue)
            .WithMessage("Compare at price must be greater than the regular price");

        RuleFor(x => x.StockQuantity)
            .GreaterThanOrEqualTo(0).WithMessage("Stock quantity cannot be negative");

        RuleFor(x => x.CategoryId)
            .MustAsync(CategoryExists).WithMessage("Selected category does not exist");
    }

    private async Task BeUniqueName(string name, CancellationToken cancellationToken)
    {
        return !await _context.Products
            .AnyAsync(p => p.Name.ToLower() == name.ToLower(), cancellationToken);
    }

    private async Task BeUniqueSKU(string sku, CancellationToken cancellationToken)
    {
        return !await _context.Products
            .AnyAsync(p => p.SKU == sku, cancellationToken);
    }

    private async Task CategoryExists(long categoryId, CancellationToken cancellationToken)
    {
        return await _context.Categories
            .AnyAsync(c => c.Id == categoryId && c.IsActive, cancellationToken);
    }
}

public class UpdateProductValidator : AbstractValidator
{
    private readonly ApplicationDbContext _context;

    public UpdateProductValidator(ApplicationDbContext context)
    {
        _context = context;

        Include(new CreateProductValidator(context));

        RuleFor(x => x.Id)
            .MustAsync(ProductExists).WithMessage("Product not found");

        // Override uniqueness rules to exclude current product
        RuleFor(x => x)
            .MustAsync(BeUniqueNameForUpdate).WithMessage("A product with this name already exists")
            .MustAsync(BeUniqueSKUForUpdate).WithMessage("This SKU is already in use");
    }

    private async Task ProductExists(long id, CancellationToken cancellationToken)
    {
        return await _context.Products.AnyAsync(p => p.Id == id, cancellationToken);
    }

    private async Task BeUniqueNameForUpdate(UpdateProductDto dto, CancellationToken cancellationToken)
    {
        return !await _context.Products
            .AnyAsync(p => p.Name.ToLower() == dto.Name.ToLower() && p.Id != dto.Id, cancellationToken);
    }

    private async Task BeUniqueSKUForUpdate(UpdateProductDto dto, CancellationToken cancellationToken)
    {
        return !await _context.Products
            .AnyAsync(p => p.SKU == dto.SKU && p.Id != dto.Id, cancellationToken);
    }
}

AutoMapper Profiles

csharp
// MyApp.Application/Mappings/ProductMappingProfile.cs
using AutoMapper;
using MyApp.Application.DTOs;
using MyApp.Domain.Entities;

namespace MyApp.Application.Mappings;

public class ProductMappingProfile : Profile
{
    public ProductMappingProfile()
    {
        // Entity to DTO
        CreateMap()
            .ForMember(dest => dest.CategoryName, 
                opt => opt.MapFrom(src => src.Category != null ? src.Category.Name : string.Empty));
        
        CreateMap()
            .IncludeBase()
            .ForMember(dest => dest.Images, 
                opt => opt.MapFrom(src => src.Images.OrderBy(i => i.DisplayOrder)));
        
        CreateMap();
        
        // DTO to Entity
        CreateMap()
            .ForMember(dest => dest.Id, opt => opt.Ignore())
            .ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
            .ForMember(dest => dest.UpdatedAt, opt => opt.Ignore())
            .ForMember(dest => dest.IsDeleted, opt => opt.Ignore())
            .ForMember(dest => dest.Category, opt => opt.Ignore())
            .ForMember(dest => dest.OrderItems, opt => opt.Ignore())
            .ForMember(dest => dest.Images, opt => opt.Ignore());
        
        CreateMap()
            .IncludeBase();
    }
}

// Usage in Service
public class ProductService : IProductService
{
    private readonly ApplicationDbContext _context;
    private readonly IMapper _mapper;
    private readonly IValidator _createValidator;
    private readonly IValidator _updateValidator;

    public ProductService(
        ApplicationDbContext context, 
        IMapper mapper,
        IValidator createValidator,
        IValidator updateValidator)
    {
        _context = context;
        _mapper = mapper;
        _createValidator = createValidator;
        _updateValidator = updateValidator;
    }

    public async Task CreateProductAsync(CreateProductDto dto)
    {
        // Validate
        var validationResult = await _createValidator.ValidateAsync(dto);
        if (!validationResult.IsValid)
        {
            throw new ValidationException(validationResult.Errors);
        }

        // Map and save
        var product = _mapper.Map(dto);
        _context.Products.Add(product);
        await _context.SaveChangesAsync();

        // Load related data and return DTO
        await _context.Entry(product)
            .Reference(p => p.Category)
            .LoadAsync();

        return _mapper.Map(product);
    }
}

Kubernetes Deployment

yaml
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-api
  namespace: production
  labels:
    app: myapp-api
    version: v1
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp-api
  template:
    metadata:
      labels:
        app: myapp-api
        version: v1
    spec:
      containers:
      - name: api
        image: ghcr.io/myorg/myapp:latest
        ports:
        - containerPort: 80
        env:
        - name: ASPNETCORE_ENVIRONMENT
          value: "Production"
        - name: ConnectionStrings__DefaultConnection
          valueFrom:
            secretKeyRef:
              name: myapp-secrets
              key: db-connection-string
        - name: Jwt__SecretKey
          valueFrom:
            secretKeyRef:
              name: myapp-secrets
              key: jwt-secret
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 80
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 5
---
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: myapp-api-service
  namespace: production
spec:
  selector:
    app: myapp-api
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  type: ClusterIP
---
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp-api-ingress
  namespace: production
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/rate-limit: "100"
spec:
  tls:
  - hosts:
    - api.myapp.com
    secretName: myapp-tls
  rules:
  - host: api.myapp.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: myapp-api-service
            port:
              number: 80

Environment Configuration

json
// appsettings.Production.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=prod-mysql.internal;Port=3306;Database=myapp_prod;Uid=myapp_user;Pwd=${DB_PASSWORD};SslMode=Required;AllowPublicKeyRetrieval=false;ConnectionTimeout=30;DefaultCommandTimeout=30;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Warning",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.EntityFrameworkCore": "Warning"
    }
  },
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "File",
        "Args": {
          "path": "/logs/myapp-.log",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 7,
          "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
        }
      },
      {
        "Name": "Seq",
        "Args": {
          "serverUrl": "http://seq.internal:5341",
          "apiKey": "${SEQ_API_KEY}"
        }
      }
    ]
  },
  "Redis": {
    "Configuration": "redis.internal:6379,password=${REDIS_PASSWORD},ssl=False,abortConnect=False"
  },
  "Cors": {
    "AllowedOrigins": [
      "https://myapp.com",
      "https://www.myapp.com"
    ]
  }
}

📈 Monitoring & Observability

Health Checks Implementation

csharp
// MyApp.Api/Health/DatabaseHealthCheck.cs
using Microsoft.Extensions.Diagnostics.HealthChecks;
using MySqlConnector;

public class DatabaseHealthCheck : IHealthCheck
{
    private readonly string _connectionString;
    private readonly ILogger _logger;

    public DatabaseHealthCheck(IConfiguration configuration, ILogger logger)
    {
        _connectionString = configuration.GetConnectionString("DefaultConnection");
        _logger = logger;
    }

    public async Task CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            using var connection = new MySqlConnection(_connectionString);
            await connection.OpenAsync(cancellationToken);
            
            using var command = connection.CreateCommand();
            command.CommandText = "SELECT 1";
            var result = await command.ExecuteScalarAsync(cancellationToken);
            
            if (result != null && result.ToString() == "1")
            {
                return HealthCheckResult.Healthy("Database is accessible");
            }
            
            return HealthCheckResult.Unhealthy("Database check returned unexpected result");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Database health check failed");
            return HealthCheckResult.Unhealthy("Database is not accessible", ex);
        }
    }
}

// MyApp.Api/Health/RedisHealthCheck.cs
public class RedisHealthCheck : IHealthCheck
{
    private readonly IConnectionMultiplexer _redis;
    private readonly ILogger _logger;

    public RedisHealthCheck(IConnectionMultiplexer redis, ILogger logger)
    {
        _redis = redis;
        _logger = logger;
    }

    public async Task CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            var database = _redis.GetDatabase();
            await database.PingAsync();
            
            var endpoints = _redis.GetEndPoints();
            var server = _redis.GetServer(endpoints.First());
            
            if (server.IsConnected)
            {
                var info = await server.InfoAsync();
                var data = new Dictionary
                {
                    ["redis_version"] = server.Version.ToString(),
                    ["connected_clients"] = info.FirstOrDefault(i => i.Key == "connected_clients")?.First().Value ?? "unknown",
                    ["used_memory_human"] = info.FirstOrDefault(i => i.Key == "used_memory_human")?.First().Value ?? "unknown"
                };
                
                return HealthCheckResult.Healthy("Redis is accessible", data);
            }
            
            return HealthCheckResult.Unhealthy("Redis server is not connected");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Redis health check failed");
            return HealthCheckResult.Unhealthy("Redis is not accessible", ex);
        }
    }
}

// Configuration in Program.cs
builder.Services.AddHealthChecks()
    .AddCheck("database", tags: new[] { "db", "sql", "mysql" })
    .AddCheck("redis", tags: new[] { "cache", "redis" })
    .AddCheck("disk_space", () =>
    {
        var driveInfo = new DriveInfo(Path.GetPathRoot(Environment.CurrentDirectory));
        var freeSpacePercent = (driveInfo.AvailableFreeSpace / (double)driveInfo.TotalSize) * 100;
        
        return freeSpacePercent > 10
            ? HealthCheckResult.Healthy($"Free space: {freeSpacePercent:F2}%")
            : HealthCheckResult.Unhealthy($"Low disk space: {freeSpacePercent:F2}%");
    }, tags: new[] { "system" });

// Map health check endpoints
app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("db") || check.Tags.Contains("cache")
});

app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false // Only return 200 OK
});

Application Metrics with Prometheus

csharp
// MyApp.Api/Metrics/MetricsService.cs
using Prometheus;

public interface IMetricsService
{
    void RecordRequestDuration(string method, string endpoint, int statusCode, double duration);
    void RecordActiveConnections(int count);
    void RecordBusinessMetric(string metricName, double value, params string[] labels);
}

public class MetricsService : IMetricsService
{
    private readonly Counter _requestCounter;
    private readonly Histogram _requestDuration;
    private readonly Gauge _activeConnections;
    private readonly Dictionary _businessCounters;
    private readonly Dictionary _businessHistograms;

    public MetricsService()
    {
        _requestCounter = Metrics.CreateCounter(
            "http_requests_total",
            "Total number of HTTP requests",
            new CounterConfiguration
            {
                LabelNames = new[] { "method", "endpoint", "status_code" }
            });

        _requestDuration = Metrics.CreateHistogram(
            "http_request_duration_seconds",
            "HTTP request duration in seconds",
            new HistogramConfiguration
            {
                LabelNames = new[] { "method", "endpoint", "status_code" },
                Buckets = Histogram.PowersOfTenDividedBuckets(-4, 1, 5)
            });

        _activeConnections = Metrics.CreateGauge(
            "active_connections",
            "Number of active connections");

        _businessCounters = new Dictionary();
        _businessHistograms = new Dictionary();
    }

    public void RecordRequestDuration(string method, string endpoint, int statusCode, double duration)
    {
        _requestCounter.WithLabels(method, endpoint, statusCode.ToString()).Inc();
        _requestDuration.WithLabels(method, endpoint, statusCode.ToString()).Observe(duration);
    }

    public void RecordActiveConnections(int count)
    {
        _activeConnections.Set(count);
    }

    public void RecordBusinessMetric(string metricName, double value, params string[] labels)
    {
        if (!_businessCounters.ContainsKey(metricName))
        {
            _businessCounters[metricName] = Metrics.CreateCounter(
                $"business_{metricName}_total",
                $"Business metric: {metricName}",
                new CounterConfiguration { LabelNames = labels });
        }

        _businessCounters[metricName].WithLabels(labels).Inc(value);
    }
}

// Metrics middleware
public class MetricsMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IMetricsService _metrics;

    public MetricsMiddleware(RequestDelegate next, IMetricsService metrics)
    {
        _next = next;
        _metrics = metrics;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = Stopwatch.StartNew();
        
        try
        {
            await _next(context);
        }
        finally
        {
            stopwatch.Stop();
            
            var endpoint = context.GetEndpoint()?.DisplayName ?? "unknown";
            _metrics.RecordRequestDuration(
                context.Request.Method,
                endpoint,
                context.Response.StatusCode,
                stopwatch.Elapsed.TotalSeconds);
        }
    }
}

// Business metrics in services
public class OrderService : IOrderService
{
    private readonly IMetricsService _metrics;
    
    public async Task CreateOrderAsync(CreateOrderDto dto)
    {
        // ... order creation logic ...
        
        // Record business metrics
        _metrics.RecordBusinessMetric("orders_created", 1, dto.CustomerId.ToString());
        _metrics.RecordBusinessMetric("order_value", (double)order.TotalAmount, "currency", "USD");
        
        return orderDto;
    }
}

Structured Logging with Serilog

csharp
// Program.cs - Serilog configuration
using Serilog;
using Serilog.Events;
using Serilog.Enrichers.Span;

// Configure Serilog
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
    .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
    .Enrich.FromLogContext()
    .Enrich.WithMachineName()
    .Enrich.WithEnvironmentName()
    .Enrich.WithSpan()
    .Enrich.WithProperty("Application", "MyApp.Api")
    .WriteTo.Console(new JsonFormatter())
    .WriteTo.File(
        new JsonFormatter(),
        "logs/myapp-.json",
        rollingInterval: RollingInterval.Day,
        retainedFileCountLimit: 7)
    .WriteTo.Seq(Environment.GetEnvironmentVariable("SEQ_URL") ?? "http://localhost:5341")
    .CreateLogger();

try
{
    Log.Information("Starting web application");
    
    var builder = WebApplication.CreateBuilder(args);
    builder.Host.UseSerilog();
    
    // ... rest of configuration ...
    
    app.Run();
}
catch (Exception ex)
{
    Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
    Log.CloseAndFlush();
}

// Logging in controllers/services
public class ProductService : IProductService
{
    private readonly ILogger _logger;
    
    public async Task CreateProductAsync(CreateProductDto dto)
    {
        using var activity = Activity.StartActivity("CreateProduct");
        
        _logger.LogInformation(
            "Creating product {@Product} in category {CategoryId}", 
            dto, 
            dto.CategoryId);
        
        try
        {
            // ... creation logic ...
            
            _logger.LogInformation(
                "Product created successfully with ID {ProductId} and SKU {SKU}", 
                product.Id, 
                product.SKU);
                
            return productDto;
        }
        catch (Exception ex)
        {
            _logger.LogError(
                ex,
                "Failed to create product with SKU {SKU}", 
                dto.SKU);
            throw;
        }
    }
}

// Custom enricher for correlation
public class CorrelationIdEnricher : ILogEventEnricher
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public CorrelationIdEnricher(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
    {
        var correlationId = _httpContextAccessor.HttpContext?.Items["CorrelationId"]?.ToString()
            ?? Guid.NewGuid().ToString();
            
        logEvent.AddPropertyIfAbsent(
            propertyFactory.CreateProperty("CorrelationId", correlationId));
    }
}
Monitoring Stack Recomandat

Logs: Serilog → Seq/ELK Stack
Metrics: Prometheus → Grafana
Tracing: OpenTelemetry → Jaeger
APM: Application Insights / New Relic

💡 Best Practices & Tips

1. Structura Proiectului

  • ✅ Folosiți Clean Architecture pentru separarea responsabilităților
  • ✅ Păstrați Domain layer independent de infrastructure
  • ✅ Utilizați interfețe pentru decuplare între straturi
  • ✅ Implementați Unit of Work doar dacă aveți nevoie de tranzacții complexe

2. Entity Framework Core

  • ✅ Folosiți AsNoTracking() pentru query-uri read-only
  • ✅ Utilizați proiecții (Select) pentru a reduce datele transferate
  • ✅ Implementați eager loading cu Include() pentru a evita N+1
  • ✅ Folosiți AsSplitQuery() pentru multiple Include-uri
  • ✅ Configurați indexuri pentru câmpurile frecvent căutate

3. API Design

  • ✅ Respectați principiile RESTful
  • ✅ Implementați versionare API (URL path sau header)
  • ✅ Folosiți status codes HTTP corecte
  • ✅ Returnați Problem Details pentru erori
  • ✅ Documentați API-ul cu OpenAPI/Swagger

4. Performanță

csharp
// Response compression
builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
    options.Providers.Add();
    options.Providers.Add();
});

// Output caching
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromMinutes(5)));
        
    options.AddPolicy("StaticData", builder =>
        builder.Expire(TimeSpan.FromHours(1))
               .Tag("static"));
});

// Memory cache
builder.Services.AddMemoryCache(options =>
{
    options.SizeLimit = 100_000_000; // 100MB
    options.CompactionPercentage = 0.25;
});

5. Securitate

  • ✅ Implementați autentificare și autorizare robustă
  • ✅ Folosiți HTTPS întotdeauna în producție
  • ✅ Sanitizați toate input-urile utilizatorilor
  • ✅ Implementați rate limiting
  • ✅ Păstrați secretele în Azure Key Vault sau similar
  • ✅ Actualizați regular dependențele

6. MySQL Specific

sql
-- Optimizări MySQL
SET GLOBAL innodb_buffer_pool_size = 1073741824; -- 1GB
SET GLOBAL max_connections = 200;
SET GLOBAL query_cache_size = 67108864; -- 64MB

-- Monitoring queries
-- Slow queries
SELECT * FROM mysql.slow_log ORDER BY query_time DESC LIMIT 10;

-- Table sizes
SELECT 
    table_schema AS 'Database',
    table_name AS 'Table',
    ROUND(((data_length + index_length) / 1024 / 1024), 2) AS 'Size (MB)'
FROM information_schema.TABLES
WHERE table_schema = 'myapp_db'
ORDER BY (data_length + index_length) DESC;

7. Development Workflow

  • ✅ Folosiți Git Flow sau GitHub Flow
  • ✅ Implementați CI/CD pipelines
  • ✅ Scrieți teste (unit, integration, e2e)
  • ✅ Code reviews obligatorii
  • ✅ Documentați deciziile arhitecturale (ADR)
Checklist pentru Producție

☑️ Health checks configurate
☑️ Logging structurat implementat
☑️ Monitoring și alerting activ
☑️ Backup-uri automate pentru DB
☑️ SSL/TLS configurat
☑️ Rate limiting activ
☑️ Secrets externalizate
☑️ Error handling global
☑️ Documentație API completă

📦 Repository Pattern & Unit of Work

Generic Repository Implementation

csharp
// MyApp.Domain/Interfaces/IRepository.cs
using System.Linq.Expressions;

namespace MyApp.Domain.Interfaces;

public interface IRepository where T : BaseEntity
{
    // Read operations
    Task GetByIdAsync(long id, CancellationToken cancellationToken = default);
    Task GetByIdWithIncludesAsync(long id, params Expression>[] includes);
    Task> GetAllAsync(CancellationToken cancellationToken = default);
    Task> FindAsync(Expression> predicate);
    Task FirstOrDefaultAsync(Expression> predicate);
    Task AnyAsync(Expression> predicate);
    Task CountAsync(Expression>? predicate = null);
    
    // Pagination
    Task> GetPagedAsync(
        int pageNumber, 
        int pageSize, 
        Expression>? filter = null,
        Func, IOrderedQueryable>? orderBy = null,
        params Expression>[] includes);
    
    // Write operations
    Task AddAsync(T entity, CancellationToken cancellationToken = default);
    Task AddRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default);
    void Update(T entity);
    void UpdateRange(IEnumerable entities);
    void Delete(T entity);
    void DeleteRange(IEnumerable entities);
    Task DeleteByIdAsync(long id, CancellationToken cancellationToken = default);
}

// MyApp.Infrastructure/Repositories/Repository.cs
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
using MyApp.Domain.Interfaces;
using MyApp.Domain.Entities;

namespace MyApp.Infrastructure.Repositories;

public class Repository : IRepository where T : BaseEntity
{
    protected readonly ApplicationDbContext _context;
    protected readonly DbSet _dbSet;

    public Repository(ApplicationDbContext context)
    {
        _context = context;
        _dbSet = context.Set();
    }

    public virtual async Task GetByIdAsync(long id, CancellationToken cancellationToken = default)
    {
        return await _dbSet.FindAsync(new object[] { id }, cancellationToken);
    }

    public async Task GetByIdWithIncludesAsync(long id, params Expression>[] includes)
    {
        IQueryable query = _dbSet;
        
        foreach (var include in includes)
        {
            query = query.Include(include);
        }
        
        return await query.FirstOrDefaultAsync(e => e.Id == id);
    }

    public async Task> GetAllAsync(CancellationToken cancellationToken = default)
    {
        return await _dbSet.ToListAsync(cancellationToken);
    }

    public async Task> FindAsync(Expression> predicate)
    {
        return await _dbSet.Where(predicate).ToListAsync();
    }

    public async Task FirstOrDefaultAsync(Expression> predicate)
    {
        return await _dbSet.FirstOrDefaultAsync(predicate);
    }

    public async Task AnyAsync(Expression> predicate)
    {
        return await _dbSet.AnyAsync(predicate);
    }

    public async Task CountAsync(Expression>? predicate = null)
    {
        return predicate == null 
            ? await _dbSet.CountAsync() 
            : await _dbSet.CountAsync(predicate);
    }

    public async Task> GetPagedAsync(
        int pageNumber, 
        int pageSize,
        Expression>? filter = null,
        Func, IOrderedQueryable>? orderBy = null,
        params Expression>[] includes)
    {
        IQueryable query = _dbSet;

        // Apply filter
        if (filter != null)
        {
            query = query.Where(filter);
        }

        // Apply includes
        foreach (var include in includes)
        {
            query = query.Include(include);
        }

        // Get total count
        var totalCount = await query.CountAsync();

        // Apply ordering
        if (orderBy != null)
        {
            query = orderBy(query);
        }

        // Apply pagination
        var items = await query
            .Skip((pageNumber - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync();

        return new PagedResult
        {
            Items = items,
            TotalCount = totalCount,
            PageNumber = pageNumber,
            PageSize = pageSize
        };
    }

    public async Task AddAsync(T entity, CancellationToken cancellationToken = default)
    {
        await _dbSet.AddAsync(entity, cancellationToken);
        return entity;
    }

    public async Task AddRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default)
    {
        await _dbSet.AddRangeAsync(entities, cancellationToken);
    }

    public void Update(T entity)
    {
        _dbSet.Update(entity);
    }

    public void UpdateRange(IEnumerable entities)
    {
        _dbSet.UpdateRange(entities);
    }

    public void Delete(T entity)
    {
        if (_context.Entry(entity).State == EntityState.Detached)
        {
            _dbSet.Attach(entity);
        }
        _dbSet.Remove(entity);
    }

    public void DeleteRange(IEnumerable entities)
    {
        _dbSet.RemoveRange(entities);
    }

    public async Task DeleteByIdAsync(long id, CancellationToken cancellationToken = default)
    {
        var entity = await GetByIdAsync(id, cancellationToken);
        if (entity != null)
        {
            Delete(entity);
            return 1;
        }
        return 0;
    }
}

Unit of Work Pattern

csharp
// MyApp.Domain/Interfaces/IUnitOfWork.cs
namespace MyApp.Domain.Interfaces;

public interface IUnitOfWork : IDisposable
{
    IRepository Products { get; }
    IRepository Categories { get; }
    IRepository Customers { get; }
    IRepository Orders { get; }
    
    Task SaveChangesAsync(CancellationToken cancellationToken = default);
    Task BeginTransactionAsync();
    Task CommitTransactionAsync();
    Task RollbackTransactionAsync();
}

// MyApp.Infrastructure/Data/UnitOfWork.cs
using Microsoft.EntityFrameworkCore.Storage;
using MyApp.Domain.Interfaces;
using MyApp.Infrastructure.Repositories;

namespace MyApp.Infrastructure.Data;

public class UnitOfWork : IUnitOfWork
{
    private readonly ApplicationDbContext _context;
    private IDbContextTransaction? _transaction;
    
    private IRepository? _products;
    private IRepository? _categories;
    private IRepository? _customers;
    private IRepository? _orders;

    public UnitOfWork(ApplicationDbContext context)
    {
        _context = context;
    }

    public IRepository Products => 
        _products ??= new Repository(_context);
        
    public IRepository Categories => 
        _categories ??= new Repository(_context);
        
    public IRepository Customers => 
        _customers ??= new Repository(_context);
        
    public IRepository Orders => 
        _orders ??= new Repository(_context);

    public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        return await _context.SaveChangesAsync(cancellationToken);
    }

    public async Task BeginTransactionAsync()
    {
        _transaction = await _context.Database.BeginTransactionAsync();
    }

    public async Task CommitTransactionAsync()
    {
        if (_transaction != null)
        {
            await _transaction.CommitAsync();
            await _transaction.DisposeAsync();
            _transaction = null;
        }
    }

    public async Task RollbackTransactionAsync()
    {
        if (_transaction != null)
        {
            await _transaction.RollbackAsync();
            await _transaction.DisposeAsync();
            _transaction = null;
        }
    }

    public void Dispose()
    {
        _transaction?.Dispose();
        _context.Dispose();
    }
}

// Usage example in Service
public class OrderService : IOrderService
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly ILogger _logger;

    public OrderService(IUnitOfWork unitOfWork, ILogger logger)
    {
        _unitOfWork = unitOfWork;
        _logger = logger;
    }

    public async Task CreateOrderAsync(CreateOrderDto dto)
    {
        await _unitOfWork.BeginTransactionAsync();
        
        try
        {
            // Create order
            var order = new Order
            {
                CustomerId = dto.CustomerId,
                OrderDate = DateTime.UtcNow,
                OrderNumber = GenerateOrderNumber(),
                Status = OrderStatus.Pending
            };
            
            await _unitOfWork.Orders.AddAsync(order);
            
            // Add order items and update stock
            foreach (var item in dto.Items)
            {
                var product = await _unitOfWork.Products.GetByIdAsync(item.ProductId);
                if (product == null)
                    throw new NotFoundException($"Product {item.ProductId} not found");
                
                if (product.StockQuantity < item.Quantity)
                    throw new InsufficientStockException($"Insufficient stock for product {product.Name}");
                
                // Update stock
                product.StockQuantity -= item.Quantity;
                _unitOfWork.Products.Update(product);
                
                // Create order item
                order.OrderItems.Add(new OrderItem
                {
                    ProductId = item.ProductId,
                    Quantity = item.Quantity,
                    UnitPrice = product.Price
                });
            }
            
            await _unitOfWork.SaveChangesAsync();
            await _unitOfWork.CommitTransactionAsync();
            
            _logger.LogInformation("Order {OrderNumber} created successfully", order.OrderNumber);
            
            return MapToDto(order);
        }
        catch (Exception ex)
        {
            await _unitOfWork.RollbackTransactionAsync();
            _logger.LogError(ex, "Error creating order");
            throw;
        }
    }
}
Repository Pattern - Pro și Contra

Pro: Abstractizare, testabilitate, schimbare ușoară a provider-ului de date.
Contra: Complexitate adițională, EF Core este deja un repository/UoW.
Recomandare: Pentru aplicații simple, folosiți direct DbContext. Pentru aplicații complexe cu logică de business sofisticată, Repository Pattern poate fi benefic.

🔍 LINQ & Query Optimization

Query-uri Eficiente cu LINQ

csharp
// MyApp.Application/Services/ProductQueryService.cs
public class ProductQueryService : IProductQueryService
{
    private readonly ApplicationDbContext _context;

    public ProductQueryService(ApplicationDbContext context)
    {
        _context = context;
    }

    // 1. Basic filtering and projection
    public async Task> GetActiveProductsAsync()
    {
        return await _context.Products
            .AsNoTracking()
            .Where(p => p.IsActive && !p.IsDeleted)
            .OrderBy(p => p.Name)
            .Select(p => new ProductListDto
            {
                Id = p.Id,
                Name = p.Name,
                Price = p.Price,
                CategoryName = p.Category.Name
            })
            .ToListAsync();
    }

    // 2. Complex filtering with dynamic predicates
    public async Task> SearchProductsAsync(ProductSearchCriteria criteria)
    {
        var query = _context.Products
            .AsNoTracking()
            .Include(p => p.Category)
            .Where(p => !p.IsDeleted);

        // Dynamic filtering
        if (!string.IsNullOrWhiteSpace(criteria.SearchTerm))
        {
            var searchTerm = criteria.SearchTerm.ToLower();
            query = query.Where(p => 
                p.Name.ToLower().Contains(searchTerm) ||
                p.Description.ToLower().Contains(searchTerm) ||
                p.SKU.ToLower().Contains(searchTerm));
        }

        if (criteria.CategoryId.HasValue)
        {
            query = query.Where(p => p.CategoryId == criteria.CategoryId.Value);
        }

        if (criteria.MinPrice.HasValue)
        {
            query = query.Where(p => p.Price >= criteria.MinPrice.Value);
        }

        if (criteria.MaxPrice.HasValue)
        {
            query = query.Where(p => p.Price <= criteria.MaxPrice.Value);
        }

        if (criteria.InStockOnly)
        {
            query = query.Where(p => p.StockQuantity > 0);
        }

        // Count before pagination
        var totalCount = await query.CountAsync();

        // Sorting
        query = criteria.SortBy switch
        {
            "name" => criteria.SortDescending ? 
                query.OrderByDescending(p => p.Name) : 
                query.OrderBy(p => p.Name),
            "price" => criteria.SortDescending ? 
                query.OrderByDescending(p => p.Price) : 
                query.OrderBy(p => p.Price),
            "created" => criteria.SortDescending ? 
                query.OrderByDescending(p => p.CreatedAt) : 
                query.OrderBy(p => p.CreatedAt),
            _ => query.OrderByDescending(p => p.Id)
        };

        // Pagination
        var items = await query
            .Skip((criteria.PageNumber - 1) * criteria.PageSize)
            .Take(criteria.PageSize)
            .Select(p => new ProductDto
            {
                Id = p.Id,
                Name = p.Name,
                Description = p.Description,
                SKU = p.SKU,
                Price = p.Price,
                CompareAtPrice = p.CompareAtPrice,
                StockQuantity = p.StockQuantity,
                CategoryName = p.Category.Name,
                IsActive = p.IsActive,
                CreatedAt = p.CreatedAt
            })
            .ToListAsync();

        return new PagedResult
        {
            Items = items,
            TotalCount = totalCount,
            PageNumber = criteria.PageNumber,
            PageSize = criteria.PageSize
        };
    }

    // 3. Aggregation queries
    public async Task GetProductStatisticsAsync()
    {
        var stats = await _context.Products
            .Where(p => !p.IsDeleted)
            .GroupBy(p => 1) // Group all into single result
            .Select(g => new ProductStatisticsDto
            {
                TotalProducts = g.Count(),
                ActiveProducts = g.Count(p => p.IsActive),
                TotalValue = g.Sum(p => p.Price * p.StockQuantity),
                AveragePrice = g.Average(p => p.Price),
                OutOfStockCount = g.Count(p => p.StockQuantity == 0),
                LowStockCount = g.Count(p => p.StockQuantity > 0 && p.StockQuantity <= 10)
            })
            .FirstOrDefaultAsync() ?? new ProductStatisticsDto();

        return stats;
    }

    // 4. Grouped results
    public async Task> GetProductsPerCategoryAsync()
    {
        return await _context.Categories
            .AsNoTracking()
            .Where(c => c.IsActive)
            .Select(c => new CategoryProductCountDto
            {
                CategoryId = c.Id,
                CategoryName = c.Name,
                ProductCount = c.Products.Count(p => !p.IsDeleted && p.IsActive),
                TotalValue = c.Products
                    .Where(p => !p.IsDeleted && p.IsActive)
                    .Sum(p => p.Price * p.StockQuantity)
            })
            .OrderByDescending(x => x.ProductCount)
            .ToListAsync();
    }

    // 5. Complex joins and subqueries
    public async Task> GetTopSellingProductsAsync(int top = 10)
    {
        var startDate = DateTime.UtcNow.AddMonths(-1);

        return await _context.Products
            .AsNoTracking()
            .Where(p => !p.IsDeleted && p.IsActive)
            .Select(p => new ProductWithSalesDto
            {
                ProductId = p.Id,
                ProductName = p.Name,
                CategoryName = p.Category.Name,
                Price = p.Price,
                TotalQuantitySold = p.OrderItems
                    .Where(oi => oi.Order.OrderDate >= startDate && 
                                oi.Order.Status != OrderStatus.Cancelled)
                    .Sum(oi => oi.Quantity),
                TotalRevenue = p.OrderItems
                    .Where(oi => oi.Order.OrderDate >= startDate && 
                                oi.Order.Status != OrderStatus.Cancelled)
                    .Sum(oi => oi.Quantity * oi.UnitPrice)
            })
            .Where(x => x.TotalQuantitySold > 0)
            .OrderByDescending(x => x.TotalQuantitySold)
            .Take(top)
            .ToListAsync();
    }

    // 6. Raw SQL for complex queries
    public async Task> GetMonthlySalesAsync(int year)
    {
        var sql = @"
            SELECT 
                MONTH(o.order_date) as Month,
                COUNT(DISTINCT o.id) as OrderCount,
                COUNT(DISTINCT o.customer_id) as UniqueCustomers,
                SUM(oi.quantity * oi.unit_price) as TotalRevenue
            FROM orders o
            INNER JOIN order_items oi ON o.id = oi.order_id
            WHERE YEAR(o.order_date) = {0}
                AND o.status != {1}
            GROUP BY MONTH(o.order_date)
            ORDER BY Month";

        return await _context.Set()
            .FromSqlRaw(sql, year, OrderStatus.Cancelled)
            .ToListAsync();
    }
}

// Supporting DTOs
public class ProductSearchCriteria
{
    public string? SearchTerm { get; set; }
    public long? CategoryId { get; set; }
    public decimal? MinPrice { get; set; }
    public decimal? MaxPrice { get; set; }
    public bool InStockOnly { get; set; }
    public string SortBy { get; set; } = "created";
    public bool SortDescending { get; set; } = true;
    public int PageNumber { get; set; } = 1;
    public int PageSize { get; set; } = 20;
}
Atenție la N+1 Queries!

Folosiți întotdeauna .Include() pentru eager loading sau .AsNoTracking() pentru query-uri read-only. Activați logging pentru EF Core în development pentru a detecta probleme de performanță.

⚡ Optimizare Performanță

Strategii de Caching

csharp
// MyApp.Application/Services/CachedProductService.cs
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;

public interface ICachedProductService
{
    Task GetProductAsync(long id);
    Task> GetProductsByCategoryAsync(long categoryId);
    Task InvalidateProductCacheAsync(long productId);
    Task InvalidateCategoryCacheAsync(long categoryId);
}

public class CachedProductService : ICachedProductService
{
    private readonly IProductService _productService;
    private readonly IMemoryCache _memoryCache;
    private readonly IDistributedCache _distributedCache;
    private readonly ILogger _logger;

    private const string ProductCacheKeyPrefix = "product:";
    private const string CategoryProductsCacheKeyPrefix = "category:products:";
    private static readonly TimeSpan DefaultCacheDuration = TimeSpan.FromMinutes(5);

    public CachedProductService(
        IProductService productService,
        IMemoryCache memoryCache,
        IDistributedCache distributedCache,
        ILogger logger)
    {
        _productService = productService;
        _memoryCache = memoryCache;
        _distributedCache = distributedCache;
        _logger = logger;
    }

    public async Task GetProductAsync(long id)
    {
        var cacheKey = $"{ProductCacheKeyPrefix}{id}";

        // Try memory cache first (L1)
        if (_memoryCache.TryGetValue(cacheKey, out var cachedProduct))
        {
            _logger.LogDebug("Product {ProductId} found in memory cache", id);
            return cachedProduct;
        }

        // Try distributed cache (L2)
        var distributedCacheKey = cacheKey;
        var cachedJson = await _distributedCache.GetStringAsync(distributedCacheKey);
        
        if (!string.IsNullOrEmpty(cachedJson))
        {
            _logger.LogDebug("Product {ProductId} found in distributed cache", id);
            var product = JsonSerializer.Deserialize(cachedJson);
            
            // Add to memory cache
            _memoryCache.Set(cacheKey, product, DefaultCacheDuration);
            
            return product;
        }

        // Fetch from database
        _logger.LogDebug("Product {ProductId} not in cache, fetching from database", id);
        var dbProduct = await _productService.GetProductByIdAsync(id);
        
        if (dbProduct != null)
        {
            // Add to both caches
            await SetCacheAsync(cacheKey, dbProduct);
        }

        return dbProduct;
    }

    public async Task> GetProductsByCategoryAsync(long categoryId)
    {
        var cacheKey = $"{CategoryProductsCacheKeyPrefix}{categoryId}";

        // Check memory cache
        if (_memoryCache.TryGetValue>(cacheKey, out var cachedProducts))
        {
            return cachedProducts;
        }

        // Check distributed cache
        var cachedJson = await _distributedCache.GetStringAsync(cacheKey);
        if (!string.IsNullOrEmpty(cachedJson))
        {
            var products = JsonSerializer.Deserialize>(cachedJson) ?? new List();
            _memoryCache.Set(cacheKey, products, DefaultCacheDuration);
            return products;
        }

        // Fetch from database
        var dbProducts = await _productService.GetProductsByCategoryAsync(categoryId);
        
        // Cache the results
        await SetCacheAsync(cacheKey, dbProducts, TimeSpan.FromMinutes(10));

        return dbProducts;
    }

    public async Task InvalidateProductCacheAsync(long productId)
    {
        var cacheKey = $"{ProductCacheKeyPrefix}{productId}";
        _memoryCache.Remove(cacheKey);
        await _distributedCache.RemoveAsync(cacheKey);
        
        _logger.LogInformation("Cache invalidated for product {ProductId}", productId);
    }

    public async Task InvalidateCategoryCacheAsync(long categoryId)
    {
        var cacheKey = $"{CategoryProductsCacheKeyPrefix}{categoryId}";
        _memoryCache.Remove(cacheKey);
        await _distributedCache.RemoveAsync(cacheKey);
        
        _logger.LogInformation("Cache invalidated for category {CategoryId}", categoryId);
    }

    private async Task SetCacheAsync(string key, T value, TimeSpan? expiration = null)
    {
        var cacheExpiration = expiration ?? DefaultCacheDuration;
        
        // Set in memory cache
        _memoryCache.Set(key, value, cacheExpiration);
        
        // Set in distributed cache
        var json = JsonSerializer.Serialize(value);
        await _distributedCache.SetStringAsync(key, json, new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = cacheExpiration
        });
    }
}

// Configuration in Program.cs
builder.Services.AddMemoryCache();
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
    options.InstanceName = "MyApp";
});

// Decorator pattern for automatic cache invalidation
public class CacheInvalidationDecorator : IProductService
{
    private readonly IProductService _inner;
    private readonly ICachedProductService _cache;

    public CacheInvalidationDecorator(IProductService inner, ICachedProductService cache)
    {
        _inner = inner;
        _cache = cache;
    }

    public async Task UpdateProductAsync(UpdateProductDto dto)
    {
        var result = await _inner.UpdateProductAsync(dto);
        
        // Invalidate caches
        await _cache.InvalidateProductCacheAsync(dto.Id);
        
        // Also invalidate category cache if category changed
        var oldProduct = await _inner.GetProductByIdAsync(dto.Id);
        if (oldProduct?.CategoryId != dto.CategoryId)
        {
            await _cache.InvalidateCategoryCacheAsync(oldProduct.CategoryId);
            await _cache.InvalidateCategoryCacheAsync(dto.CategoryId);
        }

        return result;
    }

    // Implement other methods...
}

Query Performance Optimization

csharp
// MyApp.Infrastructure/Extensions/QueryableExtensions.cs
public static class QueryableExtensions
{
    // 1. Compiled queries for frequently used queries
    private static readonly Func> GetProductByIdCompiled =
        EF.CompileAsyncQuery((ApplicationDbContext context, long id) =>
            context.Products
                .Include(p => p.Category)
                .Include(p => p.Images)
                .FirstOrDefault(p => p.Id == id));

    public static Task GetProductByIdOptimizedAsync(
        this ApplicationDbContext context, long id)
    {
        return GetProductByIdCompiled(context, id);
    }

    // 2. Split queries for multiple includes
    public static IQueryable AsSplitQuery(this IQueryable query)
    {
        return query.AsSplitQuery();
    }

    // 3. Projection helpers
    public static IQueryable SelectProductList(
        this IQueryable query)
    {
        return query.Select(p => new ProductListDto
        {
            Id = p.Id,
            Name = p.Name,
            Price = p.Price,
            CategoryName = p.Category.Name,
            StockStatus = p.StockQuantity == 0 ? "Out of Stock" :
                         p.StockQuantity <= 10 ? "Low Stock" : "In Stock"
        });
    }

    // 4. Conditional includes
    public static IQueryable IncludeIf(
        this IQueryable query,
        bool condition,
        Expression> navigationPropertyPath)
        where T : class
    {
        return condition ? query.Include(navigationPropertyPath) : query;
    }

    // 5. Pagination with total count in single query
    public static async Task<(List Items, int TotalCount)> GetPagedAsync(
        this IQueryable query,
        int pageNumber,
        int pageSize,
        CancellationToken cancellationToken = default)
    {
        var totalCount = await query.CountAsync(cancellationToken);
        
        var items = await query
            .Skip((pageNumber - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync(cancellationToken);

        return (items, totalCount);
    }
}

// Performance monitoring middleware
public class PerformanceLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;
    private readonly DiagnosticSource _diagnosticSource;

    public PerformanceLoggingMiddleware(
        RequestDelegate next,
        ILogger logger,
        DiagnosticSource diagnosticSource)
    {
        _next = next;
        _logger = logger;
        _diagnosticSource = diagnosticSource;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = Stopwatch.StartNew();
        var requestId = Guid.NewGuid().ToString();

        context.Items["RequestId"] = requestId;

        try
        {
            await _next(context);
        }
        finally
        {
            stopwatch.Stop();
            
            if (stopwatch.ElapsedMilliseconds > 500)
            {
                _logger.LogWarning(
                    "Slow request detected. RequestId: {RequestId}, " +
                    "Method: {Method}, Path: {Path}, " +
                    "StatusCode: {StatusCode}, Duration: {Duration}ms",
                    requestId,
                    context.Request.Method,
                    context.Request.Path,
                    context.Response.StatusCode,
                    stopwatch.ElapsedMilliseconds);
            }

            // Emit metrics
            _diagnosticSource.Write("RequestDuration", new
            {
                RequestId = requestId,
                Duration = stopwatch.ElapsedMilliseconds,
                Path = context.Request.Path.Value,
                StatusCode = context.Response.StatusCode
            });
        }
    }
}

Database Indexing Strategy

sql
-- Performance indexes for common queries
-- Products table
CREATE INDEX IX_products_category_active ON products(category_id, is_active) 
    WHERE is_deleted = 0;

CREATE INDEX IX_products_price_stock ON products(price, stock_quantity) 
    WHERE is_active = 1 AND is_deleted = 0;

CREATE INDEX IX_products_created_date ON products(created_at DESC);

-- Full-text search index
ALTER TABLE products ADD FULLTEXT(name, description);

-- Orders table
CREATE INDEX IX_orders_customer_date ON orders(customer_id, order_date DESC);
CREATE INDEX IX_orders_status_date ON orders(status, order_date);

-- Order items table  
CREATE INDEX IX_order_items_product ON order_items(product_id);

-- Composite index for reporting
CREATE INDEX IX_order_items_reporting 
    ON order_items(order_id, product_id, quantity, unit_price);

-- Analyze table statistics
ANALYZE TABLE products;
ANALYZE TABLE orders;
ANALYZE TABLE order_items;

🔒 Securitate & Autentificare

JWT Authentication

csharp
// MyApp.Api/Services/AuthenticationService.cs
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;

public interface IAuthenticationService
{
    Task AuthenticateAsync(LoginDto loginDto);
    Task RefreshTokenAsync(string refreshToken);
    Task RevokeRefreshTokenAsync(string refreshToken);
}

public class AuthenticationService : IAuthenticationService
{
    private readonly ApplicationDbContext _context;
    private readonly IConfiguration _configuration;
    private readonly ILogger _logger;

    public AuthenticationService(
        ApplicationDbContext context,
        IConfiguration configuration,
        ILogger logger)
    {
        _context = context;
        _configuration = configuration;
        _logger = logger;
    }

    public async Task AuthenticateAsync(LoginDto loginDto)
    {
        // Find user
        var user = await _context.Users
            .Include(u => u.Roles)
            .FirstOrDefaultAsync(u => u.Email == loginDto.Email);

        if (user == null || !VerifyPassword(loginDto.Password, user.PasswordHash))
        {
            return new AuthenticationResult { Success = false, Error = "Invalid credentials" };
        }

        // Check if account is active
        if (!user.IsActive)
        {
            return new AuthenticationResult { Success = false, Error = "Account is disabled" };
        }

        // Generate tokens
        var accessToken = GenerateAccessToken(user);
        var refreshToken = GenerateRefreshToken();

        // Save refresh token
        user.RefreshTokens.Add(new RefreshToken
        {
            Token = refreshToken,
            ExpiresAt = DateTime.UtcNow.AddDays(7),
            CreatedAt = DateTime.UtcNow,
            CreatedByIp = loginDto.IpAddress
        });

        await _context.SaveChangesAsync();

        _logger.LogInformation("User {Email} authenticated successfully", user.Email);

        return new AuthenticationResult
        {
            Success = true,
            AccessToken = accessToken,
            RefreshToken = refreshToken,
            ExpiresIn = 3600, // 1 hour
            User = new UserDto
            {
                Id = user.Id,
                Email = user.Email,
                Name = user.Name,
                Roles = user.Roles.Select(r => r.Name).ToList()
            }
        };
    }

    private string GenerateAccessToken(User user)
    {
        var key = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(_configuration["Jwt:SecretKey"]));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var claims = new List
        {
            new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
            new Claim(ClaimTypes.Email, user.Email),
            new Claim(ClaimTypes.Name, user.Name),
            new Claim("jti", Guid.NewGuid().ToString())
        };

        // Add role claims
        foreach (var role in user.Roles)
        {
            claims.Add(new Claim(ClaimTypes.Role, role.Name));
        }

        var token = new JwtSecurityToken(
            issuer: _configuration["Jwt:Issuer"],
            audience: _configuration["Jwt:Audience"],
            claims: claims,
            expires: DateTime.UtcNow.AddHours(1),
            signingCredentials: credentials
        );

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    private string GenerateRefreshToken()
    {
        var randomNumber = new byte[32];
        using var rng = RandomNumberGenerator.Create();
        rng.GetBytes(randomNumber);
        return Convert.ToBase64String(randomNumber);
    }

    private bool VerifyPassword(string password, string passwordHash)
    {
        // Using BCrypt.Net-Next
        return BCrypt.Net.BCrypt.Verify(password, passwordHash);
    }
}

// JWT Configuration in Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"])),
            ClockSkew = TimeSpan.Zero
        };

        options.Events = new JwtBearerEvents
        {
            OnTokenValidated = context =>
            {
                var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
                var jti = claimsIdentity?.FindFirst("jti")?.Value;
                
                // Check if token is blacklisted
                var tokenService = context.HttpContext.RequestServices
                    .GetRequiredService();
                    
                if (tokenService.IsBlacklisted(jti))
                {
                    context.Fail("Token has been revoked");
                }

                return Task.CompletedTask;
            },
            OnAuthenticationFailed = context =>
            {
                if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                {
                    context.Response.Headers.Add("Token-Expired", "true");
                }
                return Task.CompletedTask;
            }
        };
    });

// Authorization policies
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy => 
        policy.RequireRole("Admin"));
        
    options.AddPolicy("CanManageProducts", policy => 
        policy.RequireRole("Admin", "ProductManager"));
        
    options.AddPolicy("CanViewReports", policy => 
        policy.RequireClaim("permission", "reports.view"));
});

Input Validation & Security Headers

csharp
// MyApp.Api/Middleware/SecurityHeadersMiddleware.cs
public class SecurityHeadersMiddleware
{
    private readonly RequestDelegate _next;

    public SecurityHeadersMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Add security headers
        context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
        context.Response.Headers.Add("X-Frame-Options", "DENY");
        context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
        context.Response.Headers.Add("Referrer-Policy", "strict-origin-when-cross-origin");
        context.Response.Headers.Add("Permissions-Policy", "geolocation=(), microphone=(), camera=()");
        
        // Content Security Policy
        context.Response.Headers.Add("Content-Security-Policy", 
            "default-src 'self'; " +
            "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdnjs.cloudflare.com; " +
            "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
            "font-src 'self' https://fonts.gstatic.com; " +
            "img-src 'self' data: https:; " +
            "connect-src 'self' https://api.myapp.com");

        await _next(context);
    }
}

// Rate limiting
builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.Create(
        httpContext => RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(),
            factory: partition => new FixedWindowRateLimiterOptions
            {
                AutoReplenishment = true,
                PermitLimit = 100,
                QueueLimit = 0,
                Window = TimeSpan.FromMinute(1)
            }));

    options.AddPolicy("ApiLimit", httpContext =>
        RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(),
            factory: partition => new FixedWindowRateLimiterOptions
            {
                AutoReplenishment = true,
                PermitLimit = 30,
                Window = TimeSpan.FromMinute(1)
            }));

    options.OnRejected = async (context, token) =>
    {
        context.HttpContext.Response.StatusCode = 429;
        await context.HttpContext.Response.WriteAsync(
            "Too many requests. Please try again later.", cancellationToken: token);
    };
});

// SQL Injection prevention with parameterized queries
public class SecureProductRepository : IProductRepository
{
    private readonly ApplicationDbContext _context;

    public async Task> SearchProductsAsync(string searchTerm)
    {
        // SAFE: Uses parameterized query
        return await _context.Products
            .FromSqlInterpolated($@"
                SELECT * FROM products 
                WHERE MATCH(name, description) AGAINST({searchTerm} IN NATURAL LANGUAGE MODE)
                AND is_deleted = 0")
            .ToListAsync();
    }

    public async Task> GetProductReportAsync(
        DateTime startDate, DateTime endDate, int[] categoryIds)
    {
        // SAFE: Parameters are properly sanitized
        var parameters = new List
        {
            new MySqlParameter("@startDate", startDate),
            new MySqlParameter("@endDate", endDate)
        };

        var categoryFilter = categoryIds.Length > 0
            ? $"AND p.category_id IN ({string.Join(",", categoryIds.Select((_, i) => $"@cat{i}"))})"
            : "";

        for (int i = 0; i < categoryIds.Length; i++)
        {
            parameters.Add(new MySqlParameter($"@cat{i}", categoryIds[i]));
        }

        var sql = $@"
            SELECT 
                p.id as ProductId,
                p.name as ProductName,
                c.name as CategoryName,
                COUNT(DISTINCT o.id) as OrderCount,
                SUM(oi.quantity) as TotalQuantity,
                SUM(oi.quantity * oi.unit_price) as TotalRevenue
            FROM products p
            JOIN categories c ON p.category_id = c.id
            LEFT JOIN order_items oi ON p.id = oi.product_id
            LEFT JOIN orders o ON oi.order_id = o.id 
                AND o.order_date >= @startDate 
                AND o.order_date <= @endDate
            WHERE p.is_deleted = 0 {categoryFilter}
            GROUP BY p.id, p.name, c.name
            ORDER BY TotalRevenue DESC";

        return await _context.Set()
            .FromSqlRaw(sql, parameters.ToArray())
            .ToListAsync();
    }
}
Securitate Critică

• NU stocați niciodată parole în plain text
• Folosiți HTTPS în producție
• Implementați rate limiting pentru API
• Validați și sanitizați TOATE input-urile
• Păstrați dependențele actualizate

🧪 Testing Strategies

Unit Testing cu xUnit

csharp
// MyApp.Application.Tests/Services/ProductServiceTests.cs
using Xunit;
using Moq;
using FluentAssertions;
using Microsoft.Extensions.Logging;

public class ProductServiceTests
{
    private readonly Mock> _productRepositoryMock;
    private readonly Mock> _categoryRepositoryMock;
    private readonly Mock _unitOfWorkMock;
    private readonly Mock _mapperMock;
    private readonly Mock> _loggerMock;
    private readonly ProductService _sut; // System Under Test

    public ProductServiceTests()
    {
        _productRepositoryMock = new Mock>();
        _categoryRepositoryMock = new Mock>();
        _unitOfWorkMock = new Mock();
        _mapperMock = new Mock();
        _loggerMock = new Mock>();

        _unitOfWorkMock.Setup(u => u.Products).Returns(_productRepositoryMock.Object);
        _unitOfWorkMock.Setup(u => u.Categories).Returns(_categoryRepositoryMock.Object);

        _sut = new ProductService(
            _unitOfWorkMock.Object,
            _mapperMock.Object,
            _loggerMock.Object);
    }

    [Fact]
    public async Task GetProductByIdAsync_WhenProductExists_ReturnsProductDto()
    {
        // Arrange
        var productId = 1L;
        var product = new Product
        {
            Id = productId,
            Name = "Test Product",
            Price = 99.99m,
            CategoryId = 1,
            Category = new Category { Id = 1, Name = "Test Category" }
        };

        var expectedDto = new ProductDto
        {
            Id = productId,
            Name = "Test Product",
            Price = 99.99m,
            CategoryName = "Test Category"
        };

        _productRepositoryMock
            .Setup(r => r.GetByIdWithIncludesAsync(productId, It.IsAny>[]>()))
            .ReturnsAsync(product);

        _mapperMock
            .Setup(m => m.Map(product))
            .Returns(expectedDto);

        // Act
        var result = await _sut.GetProductByIdAsync(productId);

        // Assert
        result.Should().NotBeNull();
        result.Should().BeEquivalentTo(expectedDto);
        
        _productRepositoryMock.Verify(
            r => r.GetByIdWithIncludesAsync(productId, It.IsAny>[]>()), 
            Times.Once);
    }

    [Fact]
    public async Task GetProductByIdAsync_WhenProductNotFound_ReturnsNull()
    {
        // Arrange
        var productId = 999L;
        
        _productRepositoryMock
            .Setup(r => r.GetByIdWithIncludesAsync(productId, It.IsAny>[]>()))
            .ReturnsAsync((Product?)null);

        // Act
        var result = await _sut.GetProductByIdAsync(productId);

        // Assert
        result.Should().BeNull();
    }

    [Theory]
    [InlineData("", "Product name is required")]
    [InlineData("AB", "Product name must be at least 3 characters")]
    public async Task CreateProductAsync_WithInvalidName_ThrowsValidationException(
        string name, string expectedError)
    {
        // Arrange
        var dto = new CreateProductDto
        {
            Name = name,
            Price = 50m,
            CategoryId = 1
        };

        // Act & Assert
        var exception = await Assert.ThrowsAsync(
            () => _sut.CreateProductAsync(dto));
            
        exception.Errors.Should().Contain(e => e.ErrorMessage == expectedError);
    }

    [Fact]
    public async Task CreateProductAsync_WithValidData_CreatesAndReturnsProduct()
    {
        // Arrange
        var dto = new CreateProductDto
        {
            Name = "New Product",
            Description = "Description",
            SKU = "SKU-001",
            Price = 149.99m,
            CategoryId = 1
        };

        var category = new Category { Id = 1, Name = "Electronics", IsActive = true };
        var product = new Product
        {
            Name = dto.Name,
            Description = dto.Description,
            SKU = dto.SKU,
            Price = dto.Price,
            CategoryId = dto.CategoryId
        };

        var savedProduct = new Product
        {
            Id = 1,
            Name = dto.Name,
            Description = dto.Description,
            SKU = dto.SKU,
            Price = dto.Price,
            CategoryId = dto.CategoryId,
            Category = category
        };

        var expectedDto = new ProductDto
        {
            Id = 1,
            Name = dto.Name,
            Price = dto.Price,
            CategoryName = category.Name
        };

        _categoryRepositoryMock
            .Setup(r => r.GetByIdAsync(dto.CategoryId, default))
            .ReturnsAsync(category);

        _productRepositoryMock
            .Setup(r => r.AnyAsync(It.IsAny>>()))
            .ReturnsAsync(false);

        _mapperMock
            .Setup(m => m.Map(dto))
            .Returns(product);

        _productRepositoryMock
            .Setup(r => r.AddAsync(It.IsAny(), default))
            .ReturnsAsync(savedProduct);

        _mapperMock
            .Setup(m => m.Map(It.IsAny()))
            .Returns(expectedDto);

        _unitOfWorkMock
            .Setup(u => u.SaveChangesAsync(default))
            .ReturnsAsync(1);

        // Act
        var result = await _sut.CreateProductAsync(dto);

        // Assert
        result.Should().NotBeNull();
        result.Should().BeEquivalentTo(expectedDto);

        _productRepositoryMock.Verify(r => r.AddAsync(It.IsAny(), default), Times.Once);
        _unitOfWorkMock.Verify(u => u.SaveChangesAsync(default), Times.Once);
    }
}

// Test fixtures and helpers
public class ProductTestDataGenerator : IEnumerable
{
    private readonly List _data = new List
    {
        new object[] { 0m, false },
        new object[] { -10m, false },
        new object[] { 0.01m, true },
        new object[] { 999999.99m, true },
        new object[] { 1000000m, false }
    };

    public IEnumerator GetEnumerator() => _data.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

[Theory]
[ClassData(typeof(ProductTestDataGenerator))]
public void ValidatePrice_ShouldReturnExpectedResult(decimal price, bool expectedValid)
{
    var validator = new ProductPriceValidator();
    var result = validator.IsValid(price);
    result.Should().Be(expectedValid);
}

Integration Testing

csharp
// MyApp.Api.Tests/IntegrationTests/ProductsControllerTests.cs
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System.Net.Http.Json;
using Xunit;
using FluentAssertions;

public class ProductsControllerTests : IClassFixture>
{
    private readonly WebApplicationFactory _factory;
    private readonly HttpClient _client;

    public ProductsControllerTests(WebApplicationFactory factory)
    {
        _factory = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                // Remove the existing DbContext registration
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType == typeof(DbContextOptions));
                if (descriptor != null)
                    services.Remove(descriptor);

                // Add in-memory database for testing
                services.AddDbContext(options =>
                {
                    options.UseInMemoryDatabase("TestDb");
                });

                // Seed test data
                var sp = services.BuildServiceProvider();
                using var scope = sp.CreateScope();
                var db = scope.ServiceProvider.GetRequiredService();
                db.Database.EnsureCreated();
                SeedTestData(db);
            });
        });

        _client = _factory.CreateClient();
    }

    private void SeedTestData(ApplicationDbContext db)
    {
        if (!db.Categories.Any())
        {
            db.Categories.AddRange(
                new Category { Id = 1, Name = "Electronics", IsActive = true },
                new Category { Id = 2, Name = "Books", IsActive = true }
            );

            db.Products.AddRange(
                new Product 
                { 
                    Id = 1, 
                    Name = "Laptop", 
                    SKU = "LAP-001",
                    Price = 999.99m, 
                    CategoryId = 1,
                    StockQuantity = 10,
                    IsActive = true 
                },
                new Product 
                { 
                    Id = 2, 
                    Name = "Programming Book", 
                    SKU = "BOOK-001",
                    Price = 49.99m, 
                    CategoryId = 2,
                    StockQuantity = 50,
                    IsActive = true 
                }
            );

            db.SaveChanges();
        }
    }

    [Fact]
    public async Task GetProducts_ReturnsSuccessAndCorrectContentType()
    {
        // Act
        var response = await _client.GetAsync("/api/v1/products");

        // Assert
        response.EnsureSuccessStatusCode();
        response.Content.Headers.ContentType.ToString()
            .Should().Be("application/json; charset=utf-8");
    }

    [Fact]
    public async Task GetProducts_ReturnsExpectedProducts()
    {
        // Act
        var response = await _client.GetAsync("/api/v1/products?pageSize=10");
        var result = await response.Content.ReadFromJsonAsync>();

        // Assert
        result.Should().NotBeNull();
        result.Items.Should().HaveCount(2);
        result.TotalCount.Should().Be(2);
        result.Items.Should().Contain(p => p.Name == "Laptop");
        result.Items.Should().Contain(p => p.Name == "Programming Book");
    }

    [Fact]
    public async Task GetProductById_ExistingProduct_ReturnsProduct()
    {
        // Act
        var response = await _client.GetAsync("/api/v1/products/1");
        var product = await response.Content.ReadFromJsonAsync();

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);
        product.Should().NotBeNull();
        product.Id.Should().Be(1);
        product.Name.Should().Be("Laptop");
    }

    [Fact]
    public async Task GetProductById_NonExistingProduct_ReturnsNotFound()
    {
        // Act
        var response = await _client.GetAsync("/api/v1/products/999");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.NotFound);
    }

    [Fact]
    public async Task CreateProduct_ValidData_ReturnsCreatedProduct()
    {
        // Arrange
        var newProduct = new CreateProductDto
        {
            Name = "New Test Product",
            Description = "Test Description",
            SKU = "TEST-001",
            Price = 299.99m,
            StockQuantity = 20,
            CategoryId = 1,
            IsActive = true
        };

        // Act
        var response = await _client.PostAsJsonAsync("/api/v1/products", newProduct);
        var createdProduct = await response.Content.ReadFromJsonAsync();

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.Created);
        response.Headers.Location.Should().NotBeNull();
        
        createdProduct.Should().NotBeNull();
        createdProduct.Name.Should().Be(newProduct.Name);
        createdProduct.Price.Should().Be(newProduct.Price);
    }

    [Fact]
    public async Task CreateProduct_InvalidData_ReturnsBadRequest()
    {
        // Arrange
        var invalidProduct = new CreateProductDto
        {
            Name = "", // Invalid: empty name
            Price = -10, // Invalid: negative price
            CategoryId = 999 // Invalid: non-existing category
        };

        // Act
        var response = await _client.PostAsJsonAsync("/api/v1/products", invalidProduct);
        var problemDetails = await response.Content.ReadFromJsonAsync();

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
        problemDetails.Should().NotBeNull();
        problemDetails.Errors.Should().ContainKey("Name");
        problemDetails.Errors.Should().ContainKey("Price");
    }

    [Fact]
    public async Task UpdateProduct_ValidData_ReturnsNoContent()
    {
        // Arrange
        var updateDto = new UpdateProductDto
        {
            Id = 1,
            Name = "Updated Laptop",
            Description = "Updated Description",
            SKU = "LAP-001",
            Price = 1199.99m,
            StockQuantity = 15,
            CategoryId = 1,
            IsActive = true
        };

        // Act
        var response = await _client.PutAsJsonAsync("/api/v1/products/1", updateDto);

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.NoContent);

        // Verify the update
        var getResponse = await _client.GetAsync("/api/v1/products/1");
        var updatedProduct = await getResponse.Content.ReadFromJsonAsync();
        
        updatedProduct.Name.Should().Be("Updated Laptop");
        updatedProduct.Price.Should().Be(1199.99m);
    }

    [Fact]
    public async Task DeleteProduct_ExistingProduct_ReturnsNoContent()
    {
        // Arrange - Create a product to delete
        var productToDelete = new CreateProductDto
        {
            Name = "Product to Delete",
            SKU = "DEL-001",
            Price = 99.99m,
            CategoryId = 1
        };

        var createResponse = await _client.PostAsJsonAsync("/api/v1/products", productToDelete);
        var createdProduct = await createResponse.Content.ReadFromJsonAsync();

        // Act
        var deleteResponse = await _client.DeleteAsync($"/api/v1/products/{createdProduct.Id}");

        // Assert
        deleteResponse.StatusCode.Should().Be(HttpStatusCode.NoContent);

        // Verify deletion
        var getResponse = await _client.GetAsync($"/api/v1/products/{createdProduct.Id}");
        getResponse.StatusCode.Should().Be(HttpStatusCode.NotFound);
    }
}

// Custom WebApplicationFactory for advanced scenarios
public class CustomWebApplicationFactory 
    : WebApplicationFactory where TStartup : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // Add test-specific services
            services.AddSingleton();
            
            // Configure test authentication
            services.AddAuthentication("Test")
                .AddScheme(
                    "Test", options => { });
        });

        builder.UseEnvironment("Testing");
    }
}

// Test authentication handler for integration tests
public class TestAuthenticationHandler : AuthenticationHandler
{
    public TestAuthenticationHandler(IOptionsMonitor options,
        ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task HandleAuthenticateAsync()
    {
        var claims = new[]
        {
            new Claim(ClaimTypes.Name, "Test User"),
            new Claim(ClaimTypes.NameIdentifier, "123"),
            new Claim(ClaimTypes.Role, "Admin")
        };

        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "Test");

        return Task.FromResult(AuthenticateResult.Success(ticket));
    }
}

🚢 Deployment & DevOps

Dockerfile pentru Producție

dockerfile
# src/MyApp.Api/Dockerfile
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# Copy solution and project files
COPY ["MyApp.sln", "./"]
COPY ["src/MyApp.Api/MyApp.Api.csproj", "src/MyApp.Api/"]
COPY ["src/MyApp.Application/MyApp.Application.csproj", "src/MyApp.Application/"]
COPY ["src/MyApp.Domain/MyApp.Domain.csproj", "src/MyApp.Domain/"]
COPY ["src/MyApp.Infrastructure/MyApp.Infrastructure.csproj", "src/MyApp.Infrastructure/"]

# Restore dependencies
RUN dotnet restore

# Copy everything else
COPY . .

# Build and publish
WORKDIR "/src/src/MyApp.Api"
RUN dotnet build "MyApp.Api.csproj" -c Release -o /app/build
RUN dotnet publish "MyApp.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false

# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app

# Install culture data (for globalization)
RUN apt-get update && apt-get install -y \
    locales \
    && rm -rf /var/lib/apt/lists/*

# Create non-root user
RUN groupadd -g 1000 dotnet && \
    useradd -r -u 1000 -g dotnet dotnet

# Copy published app
COPY --from=build /app/publish .

# Set up environment
ENV ASPNETCORE_URLS=http://+:80
ENV ASPNETCORE_ENVIRONMENT=Production
EXPOSE 80

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost/health || exit 1

# Switch to non-root user
USER dotnet

ENTRYPOINT ["dotnet", "MyApp.Api.dll"]

GitHub Actions CI/CD Pipeline

yaml
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

env:
  DOTNET_VERSION: '8.0.x'
  DOCKER_REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    name: Build and Test
    runs-on: ubuntu-latest
    
    services:
      mysql:
        image: mysql:8.4
        env:
          MYSQL_ROOT_PASSWORD: Test123!
          MYSQL_DATABASE: testdb
        ports:
          - 3306:3306
        options: >-
          --health-cmd="mysqladmin ping"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=3

    steps:
    - uses: actions/checkout@v3
    
    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: ${{ env.DOTNET_VERSION }}
    
    - name: Restore dependencies
      run: dotnet restore
    
    - name: Build
      run: dotnet build --no-restore --configuration Release
    
    - name: Run unit tests
      run: dotnet test --no-build --configuration Release --verbosity normal \
           --logger "trx;LogFileName=test-results.trx" \
           --collect:"XPlat Code Coverage"
    
    - name: Run integration tests
      env:
        ConnectionStrings__DefaultConnection: "Server=localhost;Port=3306;Database=testdb;Uid=root;Pwd=Test123!;"
      run: |
        dotnet test tests/MyApp.Api.Tests/MyApp.Api.Tests.csproj \
          --no-build --configuration Release
    
    - name: Upload test results
      uses: actions/upload-artifact@v3
      if: always()
      with:
        name: test-results
        path: |
          **/*.trx
          **/coverage.cobertura.xml
    
    - name: Code coverage report
      uses: danielpalme/ReportGenerator-GitHub-Action@5.1.23
      with:
        reports: '**/coverage.cobertura.xml'
        targetdir: 'coveragereport'
        reporttypes: 'HtmlInline;Cobertura'
    
    - name: Upload coverage reports
      uses: actions/upload-artifact@v3
      with:
        name: coverage-report
        path: coveragereport/

  build-docker:
    name: Build Docker Image
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == 'push'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2
    
    - name: Log in to Container Registry
      uses: docker/login-action@v2
      with:
        registry: ${{ env.DOCKER_REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
    
    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v4
      with:
        images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}
        tags: |
          type=ref,event=branch
          type=ref,event=pr
          type=semver,pattern={{version}}
          type=semver,pattern={{major}}.{{minor}}
          type=sha,prefix={{branch}}-
    
    - name: Build and push Docker image
      uses: docker/build-push-action@v4
      with:
        context: .
        file: ./src/MyApp.Api/Dockerfile
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max

  deploy:
    name: Deploy to Production
    needs: build-docker
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: production
    
    steps:
    - name: Deploy to Kubernetes
      uses: azure/k8s-deploy@v4
      with:
        namespace: production
        manifests: |
          k8s/deployment.yaml
          k8s/service.yaml
          k8s/ingress.yaml
        images: |
          ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:main-${{ github.sha }}
        kubectl-version: 'latest'

        
        

🏗️ Arhitectură N-Tier / Clean Architecture

Arhitectura N-Tier (sau Clean Architecture) separă aplicația în straturi distincte, fiecare cu responsabilități clare. Aceasta facilitează testarea, mentenanța și scalabilitatea.

text
MyApp/
├── src/
│   ├── MyApp.Api/                 # Presentation Layer
│   │   ├── Controllers/           # REST API endpoints
│   │   ├── Filters/               # Action filters, exception filters
│   │   ├── Middleware/            # Custom middleware
│   │   ├── Program.cs             # Application entry point
│   │   └── appsettings.json       # Configuration
│   │
│   ├── MyApp.Application/         # Business Logic Layer
│   │   ├── DTOs/                  # Data Transfer Objects
│   │   ├── Interfaces/            # Service contracts
│   │   ├── Services/              # Business logic implementation
│   │   ├── Validators/            # FluentValidation rules
│   │   └── Mappings/              # AutoMapper profiles
│   │
│   ├── MyApp.Domain/              # Domain Layer (Core)
│   │   ├── Entities/              # Business entities
│   │   ├── Enums/                 # Domain enumerations
│   │   ├── Exceptions/            # Domain exceptions
│   │   └── ValueObjects/          # Value objects
│   │
│   └── MyApp.Infrastructure/      # Data Access Layer
│       ├── Data/                  # EF Core DbContext
│       ├── Migrations/            # Database migrations
│       ├── Repositories/          # Repository implementations
│       └── Configurations/        # Entity configurations
│
├── tests/
│   ├── MyApp.Api.Tests/          # Integration tests
│   ├── MyApp.Application.Tests/   # Unit tests for services
│   └── MyApp.Domain.Tests/        # Domain logic tests
│
├── docker-compose.yml             # Docker configuration
├── .gitignore
├── README.md
└── MyApp.sln                      # Solution file

Principiile Clean Architecture

🎯 Dependency Rule

Dependențele merg doar într-o direcție: dinspre exterior spre interior. Domain layer nu depinde de nimic, toate celelalte depind de el.

🔄 Inversion of Control

Folosim interfețe și dependency injection pentru a decupla straturile. Implementările concrete sunt injectate la runtime.

Beneficii ale acestei abordări

• Testabilitate crescută • Mentenanță ușoară • Flexibilitate în schimbarea tehnologiilor • Separarea clară a responsabilităților • Reutilizarea codului

🐳 Docker & MySQL Setup

Docker Compose pentru Development

yaml
version: '3.9'

services:
  mysql:
    image: mysql:8.4
    container_name: myapp_mysql
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-Admin123!}
      MYSQL_DATABASE: ${MYSQL_DATABASE:-myapp_db}
      MYSQL_USER: ${MYSQL_USER:-myapp_user}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD:-MyApp123!}
    command: >
      --character-set-server=utf8mb4
      --collation-server=utf8mb4_unicode_ci
      --default-authentication-plugin=mysql_native_password
      --max_connections=1000
      --innodb_buffer_pool_size=256M
      --innodb_log_file_size=64M
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      timeout: 10s
      retries: 10
    networks:
      - myapp_network

  phpmyadmin:
    image: phpmyadmin:latest
    container_name: myapp_phpmyadmin
    depends_on:
      - mysql
    environment:
      PMA_HOST: mysql
      PMA_PORT: 3306
      PMA_ARBITRARY: 1
    ports:
      - "8080:80"
    networks:
      - myapp_network

  api:
    build:
      context: .
      dockerfile: src/MyApp.Api/Dockerfile
    container_name: myapp_api
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ConnectionStrings__DefaultConnection=Server=mysql;Port=3306;Database=myapp_db;Uid=myapp_user;Pwd=MyApp123!;
    ports:
      - "5000:80"
      - "5001:443"
    depends_on:
      mysql:
        condition: service_healthy
    networks:
      - myapp_network

volumes:
  mysql_data:

networks:
  myapp_network:
    driver: bridge

Connection String în appsettings.json

json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Port=3306;Database=myapp_db;Uid=myapp_user;Pwd=MyApp123!;SslMode=Preferred;AllowPublicKeyRetrieval=True;ConnectionTimeout=30;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.EntityFrameworkCore": "Information"
    }
  },
  "AllowedHosts": "*"
}

💾 Entity Framework Core Configuration

DbContext Setup

csharp
// MyApp.Infrastructure/Data/ApplicationDbContext.cs
using Microsoft.EntityFrameworkCore;
using MyApp.Domain.Entities;
using System.Reflection;

namespace MyApp.Infrastructure.Data;

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions options)
        : base(options)
    {
    }

    // DbSets
    public DbSet Products => Set();
    public DbSet Categories => Set();
    public DbSet Customers => Set();
    public DbSet Orders => Set();
    public DbSet OrderItems => Set();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        
        // Apply all configurations from assembly
        modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
        
        // Global query filters
        modelBuilder.Entity().HasQueryFilter(p => !p.IsDeleted);
        modelBuilder.Entity().HasQueryFilter(c => !c.IsDeleted);
        
        // Seed data
        SeedData(modelBuilder);
    }

    private void SeedData(ModelBuilder modelBuilder)
    {
        // Categories
        modelBuilder.Entity().HasData(
            new Category { Id = 1, Name = "Electronics", Description = "Electronic devices and accessories" },
            new Category { Id = 2, Name = "Books", Description = "Physical and digital books" },
            new Category { Id = 3, Name = "Clothing", Description = "Apparel and fashion items" }
        );
    }

    // Override SaveChanges for audit fields
    public override int SaveChanges()
    {
        UpdateAuditFields();
        return base.SaveChanges();
    }

    public override async Task SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        UpdateAuditFields();
        return await base.SaveChangesAsync(cancellationToken);
    }

    private void UpdateAuditFields()
    {
        var entries = ChangeTracker.Entries()
            .Where(e => e.Entity is BaseEntity && 
                       (e.State == EntityState.Added || e.State == EntityState.Modified));

        foreach (var entry in entries)
        {
            var entity = (BaseEntity)entry.Entity;
            
            if (entry.State == EntityState.Added)
            {
                entity.CreatedAt = DateTime.UtcNow;
            }
            
            entity.UpdatedAt = DateTime.UtcNow;
        }
    }
}

Configurare în Program.cs

csharp
// MyApp.Api/Program.cs
using Microsoft.EntityFrameworkCore;
using MyApp.Infrastructure.Data;
using Pomelo.EntityFrameworkCore.MySql.Infrastructure;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Configure MySQL with EF Core
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var serverVersion = ServerVersion.AutoDetect(connectionString);

builder.Services.AddDbContext(options =>
    options.UseMySql(connectionString, serverVersion, mySqlOptions =>
    {
        mySqlOptions.EnableRetryOnFailure(
            maxRetryCount: 5,
            maxRetryDelay: TimeSpan.FromSeconds(10),
            errorNumbersToAdd: null);
        
        mySqlOptions.CommandTimeout(30);
        mySqlOptions.EnableStringComparisonTranslations();
    })
    .EnableSensitiveDataLogging(builder.Environment.IsDevelopment())
    .EnableDetailedErrors(builder.Environment.IsDevelopment())
);

// Add repositories and services
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
builder.Services.AddScoped();
builder.Services.AddScoped();

// Add AutoMapper
builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());

// Add CORS
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowAll", policy =>
    {
        policy.AllowAnyOrigin()
              .AllowAnyMethod()
              .AllowAnyHeader();
    });
});

var app = builder.Build();

// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
    
    // Apply migrations automatically in development
    using var scope = app.Services.CreateScope();
    var dbContext = scope.ServiceProvider.GetRequiredService();
    await dbContext.Database.MigrateAsync();
}

app.UseHttpsRedirection();
app.UseCors("AllowAll");
app.UseAuthorization();
app.MapControllers();

// Health check endpoint
app.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow }));

app.Run();

📊 Modele & Relații

Entități de Bază

csharp
// MyApp.Domain/Entities/BaseEntity.cs
namespace MyApp.Domain.Entities;

public abstract class BaseEntity
{
    public long Id { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }
    public bool IsDeleted { get; set; }
}

// MyApp.Domain/Entities/Category.cs
using System.ComponentModel.DataAnnotations;

namespace MyApp.Domain.Entities;

public class Category : BaseEntity
{
    [Required]
    [MaxLength(100)]
    public string Name { get; set; } = string.Empty;
    
    [MaxLength(500)]
    public string? Description { get; set; }
    
    [MaxLength(200)]
    public string? ImageUrl { get; set; }
    
    public bool IsActive { get; set; } = true;
    
    // Navigation property
    public virtual ICollection Products { get; set; } = new List();
}

// MyApp.Domain/Entities/Product.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MyApp.Domain.Entities;

public class Product : BaseEntity
{
    [Required]
    [MaxLength(200)]
    public string Name { get; set; } = string.Empty;
    
    [MaxLength(1000)]
    public string? Description { get; set; }
    
    [Required]
    [MaxLength(50)]
    public string SKU { get; set; } = string.Empty;
    
    [Column(TypeName = "decimal(18,2)")]
    [Range(0, 999999.99)]
    public decimal Price { get; set; }
    
    [Column(TypeName = "decimal(18,2)")]
    public decimal? CompareAtPrice { get; set; }
    
    [Range(0, int.MaxValue)]
    public int StockQuantity { get; set; }
    
    public bool IsActive { get; set; } = true;
    
    // Foreign key
    public long CategoryId { get; set; }
    
    // Navigation properties
    public virtual Category Category { get; set; } = null!;
    public virtual ICollection OrderItems { get; set; } = new List();
    public virtual ICollection Images { get; set; } = new List();
}

// MyApp.Domain/Entities/Customer.cs
namespace MyApp.Domain.Entities;

public class Customer : BaseEntity
{
    [Required]
    [MaxLength(100)]
    public string FirstName { get; set; } = string.Empty;
    
    [Required]
    [MaxLength(100)]
    public string LastName { get; set; } = string.Empty;
    
    [Required]
    [EmailAddress]
    [MaxLength(200)]
    public string Email { get; set; } = string.Empty;
    
    [Phone]
    [MaxLength(20)]
    public string? Phone { get; set; }
    
    public DateTime? DateOfBirth { get; set; }
    
    // Computed property
    public string FullName => $"{FirstName} {LastName}";
    
    // Navigation properties
    public virtual ICollection Orders { get; set; } = new List();
    public virtual ICollection
Addresses { get; set; } = new List
(); }

Configurare Relații cu Fluent API

csharp
// MyApp.Infrastructure/Configurations/ProductConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using MyApp.Domain.Entities;

namespace MyApp.Infrastructure.Configurations;

public class ProductConfiguration : IEntityTypeConfiguration
{
    public void Configure(EntityTypeBuilder builder)
    {
        // Table name
        builder.ToTable("products");
        
        // Primary key
        builder.HasKey(p => p.Id);
        
        // Indexes
        builder.HasIndex(p => p.SKU).IsUnique();
        builder.HasIndex(p => p.CategoryId);
        builder.HasIndex(p => new { p.IsActive, p.IsDeleted });
        
        // Properties
        builder.Property(p => p.Name)
            .IsRequired()
            .HasMaxLength(200)
            .UseCollation("utf8mb4_0900_ai_ci");
            
        builder.Property(p => p.Price)
            .HasPrecision(18, 2)
            .IsRequired();
            
        // Relationships
        builder.HasOne(p => p.Category)
            .WithMany(c => c.Products)
            .HasForeignKey(p => p.CategoryId)
            .OnDelete(DeleteBehavior.Restrict);
            
        builder.HasMany(p => p.Images)
            .WithOne(i => i.Product)
            .HasForeignKey(i => i.ProductId)
            .OnDelete(DeleteBehavior.Cascade);
    }
}

// MyApp.Infrastructure/Configurations/OrderConfiguration.cs
public class OrderConfiguration : IEntityTypeConfiguration
{
    public void Configure(EntityTypeBuilder builder)
    {
        builder.ToTable("orders");
        
        // Composite index for performance
        builder.HasIndex(o => new { o.CustomerId, o.OrderDate });
        builder.HasIndex(o => o.OrderNumber).IsUnique();
        
        // Value object configuration
        builder.OwnsOne(o => o.ShippingAddress, address =>
        {
            address.Property(a => a.Street).HasMaxLength(200).IsRequired();
            address.Property(a => a.City).HasMaxLength(100).IsRequired();
            address.Property(a => a.State).HasMaxLength(50);
            address.Property(a => a.PostalCode).HasMaxLength(20).IsRequired();
            address.Property(a => a.Country).HasMaxLength(100).IsRequired();
        });
        
        // One-to-many relationship
        builder.HasMany(o => o.OrderItems)
            .WithOne(oi => oi.Order)
            .HasForeignKey(oi => oi.OrderId)
            .OnDelete(DeleteBehavior.Cascade);
            
        // Computed column
        builder.Property(o => o.TotalAmount)
            .HasComputedColumnSql("(SELECT SUM(oi.quantity * oi.unit_price) FROM order_items oi WHERE oi.order_id = id)");
    }
}

🔄 Migrații & Database Seeding

Comenzi pentru Migrații

bash
# Instalare EF Core CLI global
dotnet tool install --global dotnet-ef

# Creare migrație inițială
dotnet ef migrations add InitialCreate --project src/MyApp.Infrastructure --startup-project src/MyApp.Api

# Aplicare migrații în baza de date
dotnet ef database update --project src/MyApp.Infrastructure --startup-project src/MyApp.Api

# Generare script SQL pentru producție
dotnet ef migrations script --idempotent --output migrations.sql --project src/MyApp.Infrastructure --startup-project src/MyApp.Api

# Rollback la o migrație anterioară
dotnet ef database update PreviousMigrationName --project src/MyApp.Infrastructure --startup-project src/MyApp.Api

# Ștergere ultimă migrație (doar dacă nu a fost aplicată)
dotnet ef migrations remove --project src/MyApp.Infrastructure --startup-project src/MyApp.Api

Database Seeding Strategy

csharp
// MyApp.Infrastructure/Data/DatabaseSeeder.cs
using Bogus;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using MyApp.Domain.Entities;

namespace MyApp.Infrastructure.Data;

public class DatabaseSeeder
{
    private readonly ApplicationDbContext _context;
    private readonly ILogger _logger;

    public DatabaseSeeder(ApplicationDbContext context, ILogger logger)
    {
        _context = context;
        _logger = logger;
    }

    public async Task SeedAsync()
    {
        try
        {
            await _context.Database.MigrateAsync();
            
            if (!await _context.Categories.AnyAsync())
            {
                _logger.LogInformation("Seeding categories...");
                await SeedCategoriesAsync();
            }
            
            if (!await _context.Products.AnyAsync())
            {
                _logger.LogInformation("Seeding products...");
                await SeedProductsAsync();
            }
            
            if (!await _context.Customers.AnyAsync())
            {
                _logger.LogInformation("Seeding customers...");
                await SeedCustomersAsync();
            }
            
            _logger.LogInformation("Database seeding completed successfully");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An error occurred while seeding the database");
            throw;
        }
    }

    private async Task SeedCategoriesAsync()
    {
        var categories = new List
        {
            new() { Name = "Electronics", Description = "Electronic devices and accessories", IsActive = true },
            new() { Name = "Books", Description = "Physical and digital books", IsActive = true },
            new() { Name = "Clothing", Description = "Apparel and fashion items", IsActive = true },
            new() { Name = "Home & Garden", Description = "Home improvement and gardening", IsActive = true },
            new() { Name = "Sports & Outdoors", Description = "Sports equipment and outdoor gear", IsActive = true }
        };
        
        _context.Categories.AddRange(categories);
        await _context.SaveChangesAsync();
    }

    private async Task SeedProductsAsync()
    {
        var categories = await _context.Categories.ToListAsync();
        var faker = new Faker()
            .RuleFor(p => p.Name, f => f.Commerce.ProductName())
            .RuleFor(p => p.Description, f => f.Commerce.ProductDescription())
            .RuleFor(p => p.SKU, f => f.Commerce.Ean13())
            .RuleFor(p => p.Price, f => f.Random.Decimal(10, 1000))
            .RuleFor(p => p.CompareAtPrice, f => f.Random.Bool(0.3f) ? f.Random.Decimal(100, 1500) : null)
            .RuleFor(p => p.StockQuantity, f => f.Random.Int(0, 100))
            .RuleFor(p => p.CategoryId, f => f.PickRandom(categories).Id)
            .RuleFor(p => p.IsActive, f => f.Random.Bool(0.9f));
        
        var products = faker.Generate(100);
        _context.Products.AddRange(products);
        await _context.SaveChangesAsync();
    }

    private async Task SeedCustomersAsync()
    {
        var faker = new Faker()
            .RuleFor(c => c.FirstName, f => f.Name.FirstName())
            .RuleFor(c => c.LastName, f => f.Name.LastName())
            .RuleFor(c => c.Email, (f, c) => f.Internet.Email(c.FirstName, c.LastName))
            .RuleFor(c => c.Phone, f => f.Phone.PhoneNumber())
            .RuleFor(c => c.DateOfBirth, f => f.Date.Past(50, DateTime.Now.AddYears(-18)));
        
        var customers = faker.Generate(50);
        _context.Customers.AddRange(customers);
        await _context.SaveChangesAsync();
    }
}

// Extension method pentru Program.cs
public static class DatabaseSeederExtensions
{
    public static async Task SeedDatabaseAsync(this IHost host)
    {
        using var scope = host.Services.CreateScope();
        var seeder = scope.ServiceProvider.GetRequiredService();
        await seeder.SeedAsync();
        return host;
    }
}
Important pentru Producție

Nu folosiți Database.MigrateAsync() automat în producție! Utilizați deployment scripts sau tool-uri CI/CD pentru a aplica migrațiile controlat.

🌐 REST API Design

Controller Pattern cu Best Practices

csharp
// MyApp.Api/Controllers/ProductsController.cs
using Microsoft.AspNetCore.Mvc;
using MyApp.Application.DTOs;
using MyApp.Application.Interfaces;
using MyApp.Application.Common;

namespace MyApp.Api.Controllers;

[ApiController]
[Route("api/v1/[controller]")]
[Produces("application/json")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;
    private readonly ILogger _logger;

    public ProductsController(IProductService productService, ILogger logger)
    {
        _productService = productService;
        _logger = logger;
    }

    /// 
    /// Get all products with pagination and filtering
    /// 
    /// Query parameters for filtering and pagination
    /// Paginated list of products
    [HttpGet]
    [ProducesResponseType(typeof(PagedResult), StatusCodes.Status200OK)]
    public async Task>> GetProducts(
        [FromQuery] ProductQueryParameters parameters)
    {
        var products = await _productService.GetProductsAsync(parameters);
        
        // Add pagination headers
        Response.Headers.Add("X-Total-Count", products.TotalCount.ToString());
        Response.Headers.Add("X-Page-Number", products.PageNumber.ToString());
        Response.Headers.Add("X-Page-Size", products.PageSize.ToString());
        
        return Ok(products);
    }

    /// 
    /// Get a specific product by ID
    /// 
    /// Product ID
    /// Product details
    [HttpGet("{id:long}")]
    [ProducesResponseType(typeof(ProductDetailDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task> GetProduct(long id)
    {
        var product = await _productService.GetProductByIdAsync(id);
        
        if (product == null)
        {
            return NotFound(new ProblemDetails
            {
                Title = "Product not found",
                Detail = $"Product with ID {id} was not found",
                Status = StatusCodes.Status404NotFound,
                Instance = HttpContext.Request.Path
            });
        }
        
        return Ok(product);
    }

    /// 
    /// Create a new product
    /// 
    /// Product creation data
    /// Created product
    [HttpPost]
    [ProducesResponseType(typeof(ProductDto), StatusCodes.Status201Created)]
    [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
    public async Task> CreateProduct([FromBody] CreateProductDto dto)
    {
        var product = await _productService.CreateProductAsync(dto);
        
        return CreatedAtAction(
            nameof(GetProduct), 
            new { id = product.Id }, 
            product);
    }

    /// 
    /// Update an existing product
    /// 
    /// Product ID
    /// Updated product data
    /// No content
    [HttpPut("{id:long}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
    public async Task UpdateProduct(long id, [FromBody] UpdateProductDto dto)
    {
        if (id != dto.Id)
        {
            return BadRequest(new ProblemDetails
            {
                Title = "ID mismatch",
                Detail = "The ID in the URL does not match the ID in the request body",
                Status = StatusCodes.Status400BadRequest
            });
        }
        
        var result = await _productService.UpdateProductAsync(dto);
        
        if (!result)
        {
            return NotFound();
        }
        
        return NoContent();
    }

    /// 
    /// Delete a product
    /// 
    /// Product ID
    /// No content
    [HttpDelete("{id:long}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task DeleteProduct(long id)
    {
        var result = await _productService.DeleteProductAsync(id);
        
        if (!result)
        {
            return NotFound();
        }
        
        return NoContent();
    }

    /// 
    /// Bulk update product prices
    /// 
    /// List of price updates
    /// Update results
    [HttpPatch("bulk-price-update")]
    [ProducesResponseType(typeof(BulkUpdateResult), StatusCodes.Status200OK)]
    public async Task> BulkUpdatePrices(
        [FromBody] List updates)
    {
        var result = await _productService.BulkUpdatePricesAsync(updates);
        return Ok(result);
    }
}

Minimal APIs Alternative

csharp
// MyApp.Api/Endpoints/ProductEndpoints.cs
using Microsoft.AspNetCore.Http.HttpResults;
using MyApp.Application.DTOs;
using MyApp.Application.Interfaces;

namespace MyApp.Api.Endpoints;

public static class ProductEndpoints
{
    public static void MapProductEndpoints(this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/v1/products")
            .WithTags("Products")
            .WithOpenApi();

        group.MapGet("/", GetProducts)
            .WithName("GetProducts")
            .WithSummary("Get all products")
            .Produces>();

        group.MapGet("/{id:long}", GetProduct)
            .WithName("GetProduct")
            .WithSummary("Get product by ID")
            .Produces()
            .Produces(404);

        group.MapPost("/", CreateProduct)
            .WithName("CreateProduct")
            .WithSummary("Create new product")
            .Produces(201)
            .ProducesValidationProblem();

        group.MapPut("/{id:long}", UpdateProduct)
            .WithName("UpdateProduct")
            .WithSummary("Update product")
            .Produces(204)
            .Produces(404)
            .ProducesValidationProblem();

        group.MapDelete("/{id:long}", DeleteProduct)
            .WithName("DeleteProduct")
            .WithSummary("Delete product")
            .Produces(204)
            .Produces(404);

        group.MapGet("/search", SearchProducts)
            .WithName("SearchProducts")
            .WithSummary("Search products")
            .Produces>();
    }

    private static async Task>, BadRequest>> GetProducts(
        [AsParameters] ProductQueryParameters parameters,
        IProductService productService)
    {
        var products = await productService.GetProductsAsync(parameters);
        return TypedResults.Ok(products);
    }

    private static async Task, NotFound>> GetProduct(
        long id,
        IProductService productService)
    {
        var product = await productService.GetProductByIdAsync(id);
        return product is not null 
            ? TypedResults.Ok(product) 
            : TypedResults.NotFound();
    }

    private static async Task, ValidationProblem>> CreateProduct(
        CreateProductDto dto,
        IProductService productService,
        LinkGenerator linkGenerator,
        HttpContext httpContext)
    {
        var product = await productService.CreateProductAsync(dto);
        var location = linkGenerator.GetPathByName(
            httpContext, 
            "GetProduct", 
            values: new { id = product.Id });
            
        return TypedResults.Created(location, product);
    }

    private static async Task> UpdateProduct(
        long id,
        UpdateProductDto dto,
        IProductService productService)
    {
        if (id != dto.Id)
        {
            return TypedResults.ValidationProblem(new Dictionary
            {
                ["id"] = new[] { "ID mismatch between route and body" }
            });
        }

        var result = await productService.UpdateProductAsync(dto);
        return result ? TypedResults.NoContent() : TypedResults.NotFound();
    }

    private static async Task> DeleteProduct(
        long id,
        IProductService productService)
    {
        var result = await productService.DeleteProductAsync(id);
        return result ? TypedResults.NoContent() : TypedResults.NotFound();
    }

    private static async Task>> SearchProducts(
        string q,
        IProductService productService)
    {
        var products = await productService.SearchProductsAsync(q);
        return TypedResults.Ok(products);
    }
}

✔️ Validare & Data Transfer Objects

DTO-uri cu Validare

csharp
// MyApp.Application/DTOs/ProductDtos.cs
using System.ComponentModel.DataAnnotations;

namespace MyApp.Application.DTOs;

public record ProductDto
{
    public long Id { get; init; }
    public string Name { get; init; } = string.Empty;
    public string? Description { get; init; }
    public string SKU { get; init; } = string.Empty;
    public decimal Price { get; init; }
    public decimal? CompareAtPrice { get; init; }
    public int StockQuantity { get; init; }
    public string CategoryName { get; init; } = string.Empty;
    public bool IsActive { get; init; }
    public DateTime CreatedAt { get; init; }
}

public record ProductDetailDto : ProductDto
{
    public long CategoryId { get; init; }
    public List Images { get; init; } = new();
    public DateTime UpdatedAt { get; init; }
}

public record CreateProductDto
{
    [Required(ErrorMessage = "Product name is required")]
    [StringLength(200, MinimumLength = 3, ErrorMessage = "Product name must be between 3 and 200 characters")]
    public string Name { get; init; } = string.Empty;
    
    [StringLength(1000, ErrorMessage = "Description cannot exceed 1000 characters")]
    public string? Description { get; init; }
    
    [Required(ErrorMessage = "SKU is required")]
    [RegularExpression(@"^[A-Z0-9\-]+$", ErrorMessage = "SKU must contain only uppercase letters, numbers, and hyphens")]
    [StringLength(50, ErrorMessage = "SKU cannot exceed 50 characters")]
    public string SKU { get; init; } = string.Empty;
    
    [Required(ErrorMessage = "Price is required")]
    [Range(0.01, 999999.99, ErrorMessage = "Price must be between 0.01 and 999,999.99")]
    public decimal Price { get; init; }
    
    [Range(0.01, 999999.99, ErrorMessage = "Compare at price must be between 0.01 and 999,999.99")]
    public decimal? CompareAtPrice { get; init; }
    
    [Range(0, int.MaxValue, ErrorMessage = "Stock quantity must be non-negative")]
    public int StockQuantity { get; init; }
    
    [Required(ErrorMessage = "Category is required")]
    [Range(1, long.MaxValue, ErrorMessage = "Invalid category ID")]
    public long CategoryId { get; init; }
    
    public bool IsActive { get; init; } = true;
}

public record UpdateProductDto : CreateProductDto
{
    [Required]
    public long Id { get; init; }
}

public record ProductPriceUpdateDto
{
    [Required]
    public long Id { get; init; }
    
    [Required]
    [Range(0.01, 999999.99)]
    public decimal NewPrice { get; init; }
}

public record ProductImageDto
{
    public long Id { get; init; }
    public string Url { get; init; } = string.Empty;
    public string? Alt { get; init; }
    public int DisplayOrder { get; init; }
}


public record ProductQueryParameters
{
    public string? SearchTerm { get; init; }
    public long? CategoryId { get; init; }
    public decimal? MinPrice { get; init; }
    public decimal? MaxPrice { get; init; }
    public bool? InStock { get; init; }
    public string SortBy { get; init; } = "created";
    public bool SortDescending { get; init; } = true;
    
    [Range(1, int.MaxValue, ErrorMessage = "Page number must be greater than 0")]
    public int PageNumber { get; init; } = 1;
    
    [Range(1, 100, ErrorMessage = "Page size must be between 1 and 100")]
    public int PageSize { get; init; } = 20;
}

public record BulkUpdateResult
{
    public int TotalRequested { get; init; }
    public int SuccessfulUpdates { get; init; }
    public List Errors { get; init; } = new();
}

Global Exception Handling

csharp
// MyApp.Api/Middleware/GlobalExceptionHandlerMiddleware.cs
using Microsoft.AspNetCore.Mvc;
using System.Net;
using System.Text.Json;
using FluentValidation;

namespace MyApp.Api.Middleware;

public class GlobalExceptionHandlerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;
    private readonly IHostEnvironment _environment;

    public GlobalExceptionHandlerMiddleware(
        RequestDelegate next,
        ILogger logger,
        IHostEnvironment environment)
    {
        _next = next;
        _logger = logger;
        _environment = environment;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unhandled exception occurred");
            await HandleExceptionAsync(context, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        var response = context.Response;
        response.ContentType = "application/json";

        var problemDetails = exception switch
        {
            ValidationException validationEx => new ValidationProblemDetails(
                validationEx.Errors.GroupBy(e => e.PropertyName)
                    .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray()))
            {
                Title = "Validation Error",
                Status = StatusCodes.Status400BadRequest,
                Instance = context.Request.Path
            },
            
            NotFoundException => new ProblemDetails
            {
                Title = "Resource Not Found",
                Detail = exception.Message,
                Status = StatusCodes.Status404NotFound,
                Instance = context.Request.Path
            },
            
            UnauthorizedAccessException => new ProblemDetails
            {
                Title = "Unauthorized",
                Detail = "You are not authorized to access this resource",
                Status = StatusCodes.Status401Unauthorized,
                Instance = context.Request.Path
            },
            
            ArgumentException argEx => new ProblemDetails
            {
                Title = "Bad Request",
                Detail = argEx.Message,
                Status = StatusCodes.Status400BadRequest,
                Instance = context.Request.Path
            },
            
            _ => new ProblemDetails
            {
                Title = "Internal Server Error",
                Detail = _environment.IsDevelopment() ? exception.Message : "An error occurred while processing your request",
                Status = StatusCodes.Status500InternalServerError,
                Instance = context.Request.Path
            }
        };

        response.StatusCode = problemDetails.Status ?? StatusCodes.Status500InternalServerError;

        if (_environment.IsDevelopment() && exception is not ValidationException)
        {
            problemDetails.Extensions.Add("stackTrace", exception.StackTrace);
        }

        var jsonOptions = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            WriteIndented = true
        };

        await response.WriteAsync(JsonSerializer.Serialize(problemDetails, jsonOptions));
    }
}

// Custom exceptions
public class NotFoundException : Exception
{
    public NotFoundException(string message) : base(message) { }
    public NotFoundException(string name, object key) 
        : base($"Entity \"{name}\" ({key}) was not found.") { }
}

public class InsufficientStockException : Exception
{
    public InsufficientStockException(string message) : base(message) { }
}

// Register in Program.cs
app.UseMiddleware();

Model Validation Filter

csharp
// MyApp.Api/Filters/ValidateModelAttribute.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace MyApp.Api.Filters;

public class ValidateModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            var problemDetails = new ValidationProblemDetails(context.ModelState)
            {
                Title = "Validation Error",
                Status = StatusCodes.Status400BadRequest,
                Instance = context.HttpContext.Request.Path
            };

            context.Result = new BadRequestObjectResult(problemDetails);
        }
    }
}

// Usage in controller
[ApiController]
[ValidateModel]  // Apply to all actions
public class ProductsController : ControllerBase
{
    // Actions will automatically validate model state
}

// Or apply globally in Program.cs
builder.Services.Configure(options =>
{
    options.InvalidModelStateResponseFactory = context =>
    {
        var problemDetails = new ValidationProblemDetails(context.ModelState)
        {
            Title = "Validation Error",
            Status = StatusCodes.Status400BadRequest,
            Instance = context.HttpContext.Request.Path
        };

        return new BadRequestObjectResult(problemDetails);
    };
});
Validation Best Practices

• Folosiți Data Annotations pentru validări simple
• FluentValidation pentru logică complexă
• Validare la nivel de DTO și Entity
• Mesaje de eroare consistente și localizate
• Returnați Problem Details pentru erori structurate

🚀 Caching & Performance Optimization

Multi-Level Caching Strategy

csharp
// MyApp.Application/Services/CacheService.cs
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;

namespace MyApp.Application.Services;

public interface ICacheService
{
    Task GetAsync(string key, CancellationToken cancellationToken = default);
    Task SetAsync(string key, T value, TimeSpan? expiry = null, CancellationToken cancellationToken = default);
    Task RemoveAsync(string key, CancellationToken cancellationToken = default);
    Task RemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default);
}

public class HybridCacheService : ICacheService
{
    private readonly IMemoryCache _memoryCache;
    private readonly IDistributedCache _distributedCache;
    private readonly ILogger _logger;
    private readonly TimeSpan _defaultMemoryCacheExpiry = TimeSpan.FromMinutes(5);
    private readonly TimeSpan _defaultDistributedCacheExpiry = TimeSpan.FromMinutes(30);

    public HybridCacheService(
        IMemoryCache memoryCache,
        IDistributedCache distributedCache,
        ILogger logger)
    {
        _memoryCache = memoryCache;
        _distributedCache = distributedCache;
        _logger = logger;
    }

    public async Task GetAsync(string key, CancellationToken cancellationToken = default)
    {
        // Try L1 cache (memory) first
        if (_memoryCache.TryGetValue(key, out var memoryValue) && memoryValue is T memoryCachedValue)
        {
            _logger.LogDebug("Cache hit (L1): {Key}", key);
            return memoryCachedValue;
        }

        // Try L2 cache (distributed)
        try
        {
            var distributedValue = await _distributedCache.GetStringAsync(key, cancellationToken);
            if (!string.IsNullOrEmpty(distributedValue))
            {
                var deserializedValue = JsonSerializer.Deserialize(distributedValue);
                if (deserializedValue != null)
                {
                    // Store in L1 cache for faster access
                    _memoryCache.Set(key, deserializedValue, _defaultMemoryCacheExpiry);
                    _logger.LogDebug("Cache hit (L2): {Key}", key);
                    return deserializedValue;
                }
            }
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Error accessing distributed cache for key: {Key}", key);
        }

        _logger.LogDebug("Cache miss: {Key}", key);
        return default;
    }

    public async Task SetAsync(string key, T value, TimeSpan? expiry = null, CancellationToken cancellationToken = default)
    {
        if (value == null) return;

        var memoryExpiry = expiry ?? _defaultMemoryCacheExpiry;
        var distributedExpiry = expiry ?? _defaultDistributedCacheExpiry;

        // Set in L1 cache (memory)
        _memoryCache.Set(key, value, memoryExpiry);

        // Set in L2 cache (distributed)
        try
        {
            var serializedValue = JsonSerializer.Serialize(value);
            await _distributedCache.SetStringAsync(
                key, 
                serializedValue,
                new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = distributedExpiry
                },
                cancellationToken);

            _logger.LogDebug("Cache set: {Key}", key);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Error setting distributed cache for key: {Key}", key);
        }
    }

    public async Task RemoveAsync(string key, CancellationToken cancellationToken = default)
    {
        _memoryCache.Remove(key);
        
        try
        {
            await _distributedCache.RemoveAsync(key, cancellationToken);
            _logger.LogDebug("Cache removed: {Key}", key);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Error removing from distributed cache for key: {Key}", key);
        }
    }

    public Task RemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default)
    {
        // This is a simplified implementation
        // In production, you might want to use Redis SCAN or maintain a key registry
        _logger.LogWarning("RemoveByPatternAsync is not fully implemented for this cache provider");
        return Task.CompletedTask;
    }
}

// Cache-aside pattern implementation
public class CachedProductService : IProductService
{
    private readonly IProductService _inner;
    private readonly ICacheService _cache;
    private const string PRODUCT_CACHE_KEY = "product:{0}";
    private const string PRODUCTS_CACHE_KEY = "products:{0}";

    public CachedProductService(IProductService inner, ICacheService cache)
    {
        _inner = inner;
        _cache = cache;
    }

    public async Task GetProductByIdAsync(long id)
    {
        var cacheKey = string.Format(PRODUCT_CACHE_KEY, id);
        
        var cachedProduct = await _cache.GetAsync(cacheKey);
        if (cachedProduct != null)
        {
            return cachedProduct;
        }

        var product = await _inner.GetProductByIdAsync(id);
        if (product != null)
        {
            await _cache.SetAsync(cacheKey, product, TimeSpan.FromMinutes(10));
        }

        return product;
    }

    public async Task CreateProductAsync(CreateProductDto dto)
    {
        var product = await _inner.CreateProductAsync(dto);
        
        // Cache the new product
        var cacheKey = string.Format(PRODUCT_CACHE_KEY, product.Id);
        await _cache.SetAsync(cacheKey, product, TimeSpan.FromMinutes(10));
        
        // Invalidate related caches
        await InvalidateProductListCaches();
        
        return product;
    }

    public async Task UpdateProductAsync(UpdateProductDto dto)
    {
        var result = await _inner.UpdateProductAsync(dto);
        
        if (result)
        {
            // Remove from cache to force refresh
            var cacheKey = string.Format(PRODUCT_CACHE_KEY, dto.Id);
            await _cache.RemoveAsync(cacheKey);
            await InvalidateProductListCaches();
        }
        
        return result;
    }

    private async Task InvalidateProductListCaches()
    {
        // Implement pattern-based cache invalidation
        await _cache.RemoveByPatternAsync("products:*");
    }
}

Response Caching & Output Caching

csharp
// Program.cs - Configure caching
builder.Services.AddResponseCaching();
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromMinutes(5)));
        
    options.AddPolicy("StaticContent", builder =>
        builder.Expire(TimeSpan.FromHours(1))
               .Tag("static"));
               
    options.AddPolicy("ProductsList", builder =>
        builder.Expire(TimeSpan.FromMinutes(2))
               .Tag("products")
               .SetVaryByQuery("page", "pageSize", "categoryId"));
});

// In controller
[HttpGet]
[ResponseCache(Duration = 300, VaryByQueryKeys = new[] { "page", "pageSize" })]
[OutputCache(PolicyName = "ProductsList")]
public async Task>> GetProducts(
    [FromQuery] ProductQueryParameters parameters)
{
    // Implementation
}

// Cache invalidation
public class ProductController : ControllerBase
{
    private readonly IOutputCacheStore _outputCacheStore;
    
    [HttpPost]
    public async Task> CreateProduct(CreateProductDto dto)
    {
        var result = await _productService.CreateProductAsync(dto);
        
        // Invalidate output cache
        await _outputCacheStore.EvictByTagAsync("products", default);
        
        return CreatedAtAction(nameof(GetProduct), new { id = result.Id }, result);
    }
}

⏰ Background Services & Jobs

Hosted Services pentru Task-uri Background

csharp
// MyApp.Api/Services/EmailProcessingService.cs
using System.Threading.Channels;

namespace MyApp.Api.Services;

public interface IEmailQueue
{
    ValueTask QueueEmailAsync(EmailMessage message);
    ValueTask DequeueEmailAsync(CancellationToken cancellationToken);
}

public class EmailQueue : IEmailQueue
{
    private readonly Channel _queue;

    public EmailQueue(int capacity = 100)
    {
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait,
            SingleReader = false,
            SingleWriter = false
        };

        _queue = Channel.CreateBounded(options);
    }

    public async ValueTask QueueEmailAsync(EmailMessage message)
    {
        await _queue.Writer.WriteAsync(message);
    }

    public async ValueTask DequeueEmailAsync(CancellationToken cancellationToken)
    {
        return await _queue.Reader.ReadAsync(cancellationToken);
    }
}

public class EmailProcessingService : BackgroundService
{
    private readonly IEmailQueue _emailQueue;
    private readonly IEmailService _emailService;
    private readonly ILogger _logger;

    public EmailProcessingService(
        IEmailQueue emailQueue,
        IEmailService emailService,
        ILogger logger)
    {
        _emailQueue = emailQueue;
        _emailService = emailService;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var message in GetEmailMessages(stoppingToken))
        {
            try
            {
                await _emailService.SendEmailAsync(message);
                _logger.LogInformation("Email sent successfully to {Recipient}", message.To);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error sending email to {Recipient}", message.To);
                // Implement retry logic or dead letter queue
            }
        }
    }

    private async IAsyncEnumerable GetEmailMessages(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            EmailMessage? message = null;
            try
            {
                message = await _emailQueue.DequeueEmailAsync(cancellationToken);
            }
            catch (OperationCanceledException)
            {
                break;
            }

            if (message != null)
            {
                yield return message;
            }
        }
    }
}

// Data cleanup service
public class DataCleanupService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger _logger;
    private readonly PeriodicTimer _timer;

    public DataCleanupService(IServiceProvider serviceProvider, ILogger logger)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
        _timer = new PeriodicTimer(TimeSpan.FromHours(24)); // Run daily
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (await _timer.WaitForNextTickAsync(stoppingToken))
        {
            _logger.LogInformation("Starting data cleanup job");
            
            try
            {
                using var scope = _serviceProvider.CreateScope();
                var dbContext = scope.ServiceProvider.GetRequiredService();
                
                await CleanupOldDataAsync(dbContext);
                
                _logger.LogInformation("Data cleanup job completed successfully");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error during data cleanup job");
            }
        }
    }

    private async Task CleanupOldDataAsync(ApplicationDbContext dbContext)
    {
        var cutoffDate = DateTime.UtcNow.AddDays(-90);
        
        // Clean up soft-deleted records older than 90 days
        var oldDeletedProducts = await dbContext.Products
            .Where(p => p.IsDeleted && p.UpdatedAt < cutoffDate)
            .ToListAsync();
            
        dbContext.Products.RemoveRange(oldDeletedProducts);
        
        // Clean up expired sessions, logs, etc.
        var expiredSessions = await dbContext.UserSessions
            .Where(s => s.ExpiresAt < DateTime.UtcNow)
            .ToListAsync();
            
        dbContext.UserSessions.RemoveRange(expiredSessions);
        
        await dbContext.SaveChangesAsync();
        
        _logger.LogInformation("Cleaned up {ProductCount} products and {SessionCount} sessions", 
            oldDeletedProducts.Count, expiredSessions.Count);
    }

    public override void Dispose()
    {
        _timer?.Dispose();
        base.Dispose();
    }
}

// Register services in Program.cs
builder.Services.AddSingleton();
builder.Services.AddHostedService();
builder.Services.AddHostedService();

Integration cu Hangfire pentru Job-uri Complexe

csharp
// Install Hangfire
// dotnet add package Hangfire.AspNetCore
// dotnet add package Hangfire.MySql

// Program.cs
builder.Services.AddHangfire(configuration => configuration
    .SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
    .UseSimpleAssemblyNameTypeSerializer()
    .UseRecommendedSerializerSettings()
    .UseStorage(new MySqlStorage(connectionString, new MySqlStorageOptions
    {
        TransactionIsolationLevel = IsolationLevel.ReadCommitted,
        QueuePollInterval = TimeSpan.FromSeconds(15),
        JobExpirationCheckInterval = TimeSpan.FromHours(1),
        CountersAggregateInterval = TimeSpan.FromMinutes(5),
        PrepareSchemaIfNecessary = true,
        DashboardJobListLimit = 50000,
        TransactionTimeout = TimeSpan.FromMinutes(1),
        TablesPrefix = "hangfire_"
    })));

builder.Services.AddHangfireServer(options =>
{
    options.WorkerCount = Environment.ProcessorCount * 2;
    options.Queues = new[] { "critical", "default", "background" };
});

// Job definitions
public interface IReportService
{
    Task GenerateMonthlyReportAsync();
    Task ProcessLargeDatasetAsync(int batchSize);
    Task SendBulkEmailsAsync(List recipients, string template);
}

public class ReportService : IReportService
{
    private readonly ApplicationDbContext _context;
    private readonly IEmailService _emailService;
    private readonly ILogger _logger;

    public ReportService(ApplicationDbContext context, IEmailService emailService, ILogger logger)
    {
        _context = context;
        _emailService = emailService;
        _logger = logger;
    }

    [AutomaticRetry(Attempts = 3)]
    [Queue("background")]
    public async Task GenerateMonthlyReportAsync()
    {
        _logger.LogInformation("Starting monthly report generation");
        
        var startDate = DateTime.UtcNow.AddMonths(-1).Date;
        var endDate = DateTime.UtcNow.Date;
        
        var reportData = await _context.Orders
            .Where(o => o.OrderDate >= startDate && o.OrderDate < endDate)
            .GroupBy(o => o.OrderDate.Date)
            .Select(g => new
            {
                Date = g.Key,
                OrderCount = g.Count(),
                TotalRevenue = g.Sum(o => o.TotalAmount)
            })
            .ToListAsync();
        
        // Generate PDF report
        var pdfContent = GeneratePdfReport(reportData);
        
        // Send to stakeholders
        await _emailService.SendEmailWithAttachmentAsync(
            "stakeholders@company.com",
            "Monthly Sales Report",
            "Please find attached the monthly sales report.",
            pdfContent,
            "monthly-report.pdf");
        
        _logger.LogInformation("Monthly report generated and sent successfully");
    }

    [Queue("critical")]
    public async Task ProcessLargeDatasetAsync(int batchSize)
    {
        var totalRecords = await _context.Products.CountAsync();
        var batches = (int)Math.Ceiling((double)totalRecords / batchSize);
        
        for (int i = 0; i < batches; i++)
        {
            var batch = await _context.Products
                .Skip(i * batchSize)
                .Take(batchSize)
                .ToListAsync();
            
            // Process batch
            await ProcessProductBatch(batch);
            
            _logger.LogInformation("Processed batch {BatchNumber}/{TotalBatches}", i + 1, batches);
            
            // Small delay to prevent overwhelming the database
            await Task.Delay(100);
        }
    }

    private byte[] GeneratePdfReport(object reportData)
    {
        // Implementation using iTextSharp, PdfSharp, or similar
        return new byte[0]; // Placeholder
    }

    private async Task ProcessProductBatch(List products)
    {
        // Batch processing logic
        await Task.CompletedTask;
    }
}

// Schedule jobs
public class JobScheduler
{
    private readonly IRecurringJobManager _recurringJobManager;

    public JobScheduler(IRecurringJobManager recurringJobManager)
    {
        _recurringJobManager = recurringJobManager;
    }

    public void ScheduleJobs()
    {
        // Monthly reports on 1st day of month at 2 AM
        _recurringJobManager.AddOrUpdate(
            "monthly-report",
            service => service.GenerateMonthlyReportAsync(),
            "0 2 1 * *",
            TimeZoneInfo.FindSystemTimeZoneById("UTC"));

        // Daily cleanup at 3 AM
        _recurringJobManager.AddOrUpdate(
            "daily-cleanup", 
            service => service.CleanupOldDataAsync(),
            Cron.Daily(3),
            TimeZoneInfo.FindSystemTimeZoneById("UTC"));
    }
}

// In controller - trigger background jobs
[HttpPost("generate-report")]
public IActionResult TriggerReportGeneration()
{
    BackgroundJob.Enqueue(service => service.GenerateMonthlyReportAsync());
    return Accepted();
}

[HttpPost("process-data/{batchSize}")]
public IActionResult ProcessLargeDataset(int batchSize)
{
    BackgroundJob.Enqueue(service => service.ProcessLargeDatasetAsync(batchSize));
    return Accepted();
}

🔢 API Versioning

Setup API Versioning

csharp
// Install package: dotnet add package Asp.Versioning.Http

// Program.cs
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ApiVersionReader = ApiVersionReader.Combine(
        new UrlSegmentApiVersionReader(),
        new QueryStringApiVersionReader("version"),
        new HeaderApiVersionReader("X-Version"),
        new MediaTypeApiVersionReader("ver")
    );
}).AddApiExplorer(setup =>
{
    setup.GroupNameFormat = "'v'VVV";
    setup.SubstituteApiVersionInUrl = true;
});

// V1 Controller
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class ProductsV1Controller : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsV1Controller(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet]
    public async Task>> GetProducts()
    {
        var products = await _productService.GetProductsAsync();
        return Ok(products.Select(MapToV1Dto));
    }

    [HttpGet("{id}")]
    public async Task> GetProduct(long id)
    {
        var product = await _productService.GetProductByIdAsync(id);
        return product == null ? NotFound() : Ok(MapToV1Dto(product));
    }

    private ProductV1Dto MapToV1Dto(ProductDto product)
    {
        return new ProductV1Dto
        {
            Id = product.Id,
            Name = product.Name,
            Price = product.Price,
            Category = product.CategoryName
        };
    }
}

// V2 Controller with enhanced features
[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class ProductsV2Controller : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsV2Controller(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet]
    public async Task>> GetProducts(
        [FromQuery] ProductQueryParameters parameters)
    {
        var products = await _productService.GetProductsAsync(parameters);
        return Ok(new PagedResult
        {
            Items = products.Items.Select(MapToV2Dto).ToList(),
            TotalCount = products.TotalCount,
            PageNumber = products.PageNumber,
            PageSize = products.PageSize
        });
    }

    [HttpGet("{id}")]
    public async Task> GetProduct(long id)
    {
        var product = await _productService.GetProductDetailByIdAsync(id);
        return product == null ? NotFound() : Ok(MapToV2DetailDto(product));
    }

    [HttpPost]
    public async Task> CreateProduct([FromBody] CreateProductV2Dto dto)
    {
        var product = await _productService.CreateProductAsync(MapFromV2Dto(dto));
        return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, MapToV2Dto(product));
    }

    private ProductV2Dto MapToV2Dto(ProductDto product)
    {
        return new ProductV2Dto
        {
            Id = product.Id,
            Name = product.Name,
            Description = product.Description,
            Price = product.Price,
            CompareAtPrice = product.CompareAtPrice,
            StockQuantity = product.StockQuantity,
            Category = new CategorySummaryDto
            {
                Id = product.CategoryId,
                Name = product.CategoryName
            },
            CreatedAt = product.CreatedAt,
            IsActive = product.IsActive
        };
    }
}

// DTOs for different versions
public record ProductV1Dto
{
    public long Id { get; init; }
    public string Name { get; init; } = string.Empty;
    public decimal Price { get; init; }
    public string Category { get; init; } = string.Empty;
}

public record ProductV2Dto
{
    public long Id { get; init; }
    public string Name { get; init; } = string.Empty;
    public string? Description { get; init; }
    public decimal Price { get; init; }
    public decimal? CompareAtPrice { get; init; }
    public int StockQuantity { get; init; }
    public CategorySummaryDto Category { get; init; } = new();
    public DateTime CreatedAt { get; init; }
    public bool IsActive { get; init; }
}

public record CategorySummaryDto
{
    public long Id { get; init; }
    public string Name { get; init; } = string.Empty;
}

// Swagger configuration for multiple versions
builder.Services.ConfigureOptions();

public class ConfigureSwaggerOptions : IConfigureNamedOptions
{
    private readonly IApiVersionDescriptionProvider _provider;

    public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider)
    {
        _provider = provider;
    }

    public void Configure(string name, SwaggerGenOptions options)
    {
        Configure(options);
    }

    public void Configure(SwaggerGenOptions options)
    {
        foreach (var description in _provider.ApiVersionDescriptions)
        {
            options.SwaggerDoc(description.GroupName, CreateVersionInfo(description));
        }
    }

    private OpenApiInfo CreateVersionInfo(ApiVersionDescription description)
    {
        var info = new OpenApiInfo()
        {
            Title = "MyApp API",
            Version = description.ApiVersion.ToString(),
            Description = "A comprehensive API for MyApp with MySQL backend.",
            Contact = new OpenApiContact
            {
                Name = "Development Team",
                Email = "dev@myapp.com"
            }
        };

        if (description.IsDeprecated)
        {
            info.Description += " This API version has been deprecated.";
        }

        return info;
    }
}

// Configure Swagger UI for multiple versions
app.UseSwaggerUI(options =>
{
    var descriptions = app.DescribeApiVersions();
    
    foreach (var description in descriptions)
    {
        var url = $"/swagger/{description.GroupName}/swagger.json";
        var name = description.GroupName.ToUpperInvariant();
        options.SwaggerEndpoint(url, name);
    }
    
    options.RoutePrefix = "swagger";
    options.DocExpansion(DocExpansion.List);
});

🎯 Concluzie & Resurse Suplimentare

Acest ghid comprehensive acoperă aspectele esențiale pentru dezvoltarea aplicațiilor moderne ASP.NET Core cu Entity Framework Core și MySQL. De la arhitectura de bază până la deployment în producție, ați învățat să construiți aplicații robuste, scalabile și performante.

Puncte Cheie

🏗️ Arhitectură

  • Clean Architecture pentru separarea responsabilităților
  • Dependency Injection pentru decuplare
  • Repository Pattern pentru abstractizarea datelor

⚡ Performanță

  • Multi-level caching cu Redis
  • Query optimization cu EF Core
  • Response caching și output caching

🔒 Securitate

  • JWT authentication și authorization
  • Input validation și sanitization
  • Rate limiting și security headers

🚀 DevOps

  • Docker containerization
  • CI/CD cu GitHub Actions
  • Kubernetes deployment

Resurse Suplimentare

Resursa Descriere Link
.NET Documentation Documentația oficială Microsoft docs.microsoft.com/dotnet
EF Core Docs Ghidul complet Entity Framework Core docs.microsoft.com/ef/core
MySQL Documentation Documentația oficială MySQL dev.mysql.com/doc
Clean Architecture Template oficial pentru Clean Architecture GitHub Template
Felicitări! 🎉

Ați parcurs cu succes acest ghid comprehensive. Acum aveți cunoștințele necesare pentru a dezvolta aplicații enterprise-grade cu ASP.NET Core și MySQL. Continuați să exersați și să explorați tehnologiile pentru a vă perfecționa abilitățile.

Următorii Pași

  1. Construiți un proiect propriu - Aplicați cunoștințele într-un proiect real
  2. Explorați microserviciile - Învățați despre arhitectura distribuită
  3. Studiați cloud patterns - Azure, AWS sau Google Cloud Platform
  4. Aprofundați DevOps - GitOps, Infrastructure as Code, Monitoring
  5. Contribuiți la proiecte open source - Participați la comunitatea .NET
public record ProductQueryParameters { public string? SearchTerm { get; init; } public long? CategoryId { get; init; } public decimal? MinPrice { get; init; } public decimal? MaxPrice { get; init; } public bool? InStock { get; init; } public string SortBy { get; init; } = "created"; public bool SortDescending { get; init; } = true; [Range(1, int.MaxValue, ErrorMessage = "Page number must be greater than 0")] public int PageNumber { get; init; } = 1; [Range(1, 100, ErrorMessage = "Page size must be between 1 and 100")] public int PageSize { get; init; } = 20; } public record BulkUpdateResult { public int TotalRequested { get; init; } public int SuccessfulUpdates { get; init; } public List Errors { get; init; } = new(); }

Global Exception Handling

csharp
// MyApp.Api/Middleware/GlobalExceptionHandlerMiddleware.cs
using Microsoft.AspNetCore.Mvc;
using System.Net;
using System.Text.Json;
using FluentValidation;

namespace MyApp.Api.Middleware;

public class GlobalExceptionHandlerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;
    private readonly IHostEnvironment _environment;

    public GlobalExceptionHandlerMiddleware(
        RequestDelegate next,
        ILogger logger,
        IHostEnvironment environment)
    {
        _next = next;
        _logger = logger;
        _environment = environment;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unhandled exception occurred");
            await HandleExceptionAsync(context, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        var response = context.Response;
        response.ContentType = "application/json";

        var problemDetails = exception switch
        {
            ValidationException validationEx => new ValidationProblemDetails(
                validationEx.Errors.GroupBy(e => e.PropertyName)
                    .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray()))
            {
                Title = "Validation Error",
                Status = StatusCodes.Status400BadRequest,
                Instance = context.Request.Path
            },
            
            NotFoundException => new ProblemDetails
            {
                Title = "Resource Not Found",
                Detail = exception.Message,
                Status = StatusCodes.Status404NotFound,
                Instance = context.Request.Path
            },
            
            UnauthorizedAccessException => new ProblemDetails
            {
                Title = "Unauthorized",
                Detail = "You are not authorized to access this resource",
                Status = StatusCodes.Status401Unauthorized,
                Instance = context.Request.Path
            },
            
            ArgumentException argEx => new ProblemDetails
            {
                Title = "Bad Request",
                Detail = argEx.Message,
                Status = StatusCodes.Status400BadRequest,
                Instance = context.Request.Path
            },
            
            _ => new ProblemDetails
            {
                Title = "Internal Server Error",
                Detail = _environment.IsDevelopment() ? exception.Message : "An error occurred while processing your request",
                Status = StatusCodes.Status500InternalServerError,
                Instance = context.Request.Path
            }
        };

        response.StatusCode = problemDetails.Status ?? StatusCodes.Status500InternalServerError;

        if (_environment.IsDevelopment() && exception is not ValidationException)
        {
            problemDetails.Extensions.Add("stackTrace", exception.StackTrace);
        }

        var jsonOptions = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            WriteIndented = true
        };

        await response.WriteAsync(JsonSerializer.Serialize(problemDetails, jsonOptions));
    }
}

// Custom exceptions
public class NotFoundException : Exception
{
    public NotFoundException(string message) : base(message) { }
    public NotFoundException(string name, object key) 
        : base($"Entity \"{name}\" ({key}) was not found.") { }
}

public class InsufficientStockException : Exception
{
    public InsufficientStockException(string message) : base(message) { }
}

// Register in Program.cs
app.UseMiddleware();

Model Validation Filter

csharp
// MyApp.Api/Filters/ValidateModelAttribute.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace MyApp.Api.Filters;

public class ValidateModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            var problemDetails = new ValidationProblemDetails(context.ModelState)
            {
                Title = "Validation Error",
                Status = StatusCodes.Status400BadRequest,
                Instance = context.HttpContext.Request.Path
            };

            context.Result = new BadRequestObjectResult(problemDetails);
        }
    }
}

// Usage in controller
[ApiController]
[ValidateModel]  // Apply to all actions
public class ProductsController : ControllerBase
{
    // Actions will automatically validate model state
}

// Or apply globally in Program.cs
builder.Services.Configure(options =>
{
    options.InvalidModelStateResponseFactory = context =>
    {
        var problemDetails = new ValidationProblemDetails(context.ModelState)
        {
            Title = "Validation Error",
            Status = StatusCodes.Status400BadRequest,
            Instance = context.HttpContext.Request.Path
        };

        return new BadRequestObjectResult(problemDetails);
    };
});
Validation Best Practices

• Folosiți Data Annotations pentru validări simple
• FluentValidation pentru logică complexă
• Validare la nivel de DTO și Entity
• Mesaje de eroare consistente și localizate
• Returnați Problem Details pentru erori structurate

🚀 Caching & Performance Optimization

Multi-Level Caching Strategy

csharp
// MyApp.Application/Services/CacheService.cs
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;

namespace MyApp.Application.Services;

public interface ICacheService
{
    Task GetAsync(string key, CancellationToken cancellationToken = default);
    Task SetAsync(string key, T value, TimeSpan? expiry = null, CancellationToken cancellationToken = default);
    Task RemoveAsync(string key, CancellationToken cancellationToken = default);
    Task RemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default);
}

public class HybridCacheService : ICacheService
{
    private readonly IMemoryCache _memoryCache;
    private readonly IDistributedCache _distributedCache;
    private readonly ILogger _logger;
    private readonly TimeSpan _defaultMemoryCacheExpiry = TimeSpan.FromMinutes(5);
    private readonly TimeSpan _defaultDistributedCacheExpiry = TimeSpan.FromMinutes(30);

    public HybridCacheService(
        IMemoryCache memoryCache,
        IDistributedCache distributedCache,
        ILogger logger)
    {
        _memoryCache = memoryCache;
        _distributedCache = distributedCache;
        _logger = logger;
    }

    public async Task GetAsync(string key, CancellationToken cancellationToken = default)
    {
        // Try L1 cache (memory) first
        if (_memoryCache.TryGetValue(key, out var memoryValue) && memoryValue is T memoryCachedValue)
        {
            _logger.LogDebug("Cache hit (L1): {Key}", key);
            return memoryCachedValue;
        }

        // Try L2 cache (distributed)
        try
        {
            var distributedValue = await _distributedCache.GetStringAsync(key, cancellationToken);
            if (!string.IsNullOrEmpty(distributedValue))
            {
                var deserializedValue = JsonSerializer.Deserialize(distributedValue);
                if (deserializedValue != null)
                {
                    // Store in L1 cache for faster access
                    _memoryCache.Set(key, deserializedValue, _defaultMemoryCacheExpiry);
                    _logger.LogDebug("Cache hit (L2): {Key}", key);
                    return deserializedValue;
                }
            }
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Error accessing distributed cache for key: {Key}", key);
        }

        _logger.LogDebug("Cache miss: {Key}", key);
        return default;
    }

    public async Task SetAsync(string key, T value, TimeSpan? expiry = null, CancellationToken cancellationToken = default)
    {
        if (value == null) return;

        var memoryExpiry = expiry ?? _defaultMemoryCacheExpiry;
        var distributedExpiry = expiry ?? _defaultDistributedCacheExpiry;

        // Set in L1 cache (memory)
        _memoryCache.Set(key, value, memoryExpiry);

        // Set in L2 cache (distributed)
        try
        {
            var serializedValue = JsonSerializer.Serialize(value);
            await _distributedCache.SetStringAsync(
                key, 
                serializedValue,
                new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = distributedExpiry
                },
                cancellationToken);

            _logger.LogDebug("Cache set: {Key}", key);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Error setting distributed cache for key: {Key}", key);
        }
    }

    public async Task RemoveAsync(string key, CancellationToken cancellationToken = default)
    {
        _memoryCache.Remove(key);
        
        try
        {
            await _distributedCache.RemoveAsync(key, cancellationToken);
            _logger.LogDebug("Cache removed: {Key}", key);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Error removing from distributed cache for key: {Key}", key);
        }
    }

    public Task RemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default)
    {
        // This is a simplified implementation
        // In production, you might want to use Redis SCAN or maintain a key registry
        _logger.LogWarning("RemoveByPatternAsync is not fully implemented for this cache provider");
        return Task.CompletedTask;
    }
}

// Cache-aside pattern implementation
public class CachedProductService : IProductService
{
    private readonly IProductService _inner;
    private readonly ICacheService _cache;
    private const string PRODUCT_CACHE_KEY = "product:{0}";
    private const string PRODUCTS_CACHE_KEY = "products:{0}";

    public CachedProductService(IProductService inner, ICacheService cache)
    {
        _inner = inner;
        _cache = cache;
    }

    public async Task GetProductByIdAsync(long id)
    {
        var cacheKey = string.Format(PRODUCT_CACHE_KEY, id);
        
        var cachedProduct = await _cache.GetAsync(cacheKey);
        if (cachedProduct != null)
        {
            return cachedProduct;
        }

        var product = await _inner.GetProductByIdAsync(id);
        if (product != null)
        {
            await _cache.SetAsync(cacheKey, product, TimeSpan.FromMinutes(10));
        }

        return product;
    }

    public async Task CreateProductAsync(CreateProductDto dto)
    {
        var product = await _inner.CreateProductAsync(dto);
        
        // Cache the new product
        var cacheKey = string.Format(PRODUCT_CACHE_KEY, product.Id);
        await _cache.SetAsync(cacheKey, product, TimeSpan.FromMinutes(10));
        
        // Invalidate related caches
        await InvalidateProductListCaches();
        
        return product;
    }

    public async Task UpdateProductAsync(UpdateProductDto dto)
    {
        var result = await _inner.UpdateProductAsync(dto);
        
        if (result)
        {
            // Remove from cache to force refresh
            var cacheKey = string.Format(PRODUCT_CACHE_KEY, dto.Id);
            await _cache.RemoveAsync(cacheKey);
            await InvalidateProductListCaches();
        }
        
        return result;
    }

    private async Task InvalidateProductListCaches()
    {
        // Implement pattern-based cache invalidation
        await _cache.RemoveByPatternAsync("products:*");
    }
}

Response Caching & Output Caching

csharp
// Program.cs - Configure caching
builder.Services.AddResponseCaching();
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromMinutes(5)));
        
    options.AddPolicy("StaticContent", builder =>
        builder.Expire(TimeSpan.FromHours(1))
               .Tag("static"));
               
    options.AddPolicy("ProductsList", builder =>
        builder.Expire(TimeSpan.FromMinutes(2))
               .Tag("products")
               .SetVaryByQuery("page", "pageSize", "categoryId"));
});

// In controller
[HttpGet]
[ResponseCache(Duration = 300, VaryByQueryKeys = new[] { "page", "pageSize" })]
[OutputCache(PolicyName = "ProductsList")]
public async Task>> GetProducts(
    [FromQuery] ProductQueryParameters parameters)
{
    // Implementation
}

// Cache invalidation
public class ProductController : ControllerBase
{
    private readonly IOutputCacheStore _outputCacheStore;
    
    [HttpPost]
    public async Task> CreateProduct(CreateProductDto dto)
    {
        var result = await _productService.CreateProductAsync(dto);
        
        // Invalidate output cache
        await _outputCacheStore.EvictByTagAsync("products", default);
        
        return CreatedAtAction(nameof(GetProduct), new { id = result.Id }, result);
    }
}

⏰ Background Services & Jobs

Hosted Services pentru Task-uri Background

csharp
// MyApp.Api/Services/EmailProcessingService.cs
using System.Threading.Channels;

namespace MyApp.Api.Services;

public interface IEmailQueue
{
    ValueTask QueueEmailAsync(EmailMessage message);
    ValueTask DequeueEmailAsync(CancellationToken cancellationToken);
}

public class EmailQueue : IEmailQueue
{
    private readonly Channel _queue;

    public EmailQueue(int capacity = 100)
    {
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait,
            SingleReader = false,
            SingleWriter = false
        };

        _queue = Channel.CreateBounded(options);
    }

    public async ValueTask QueueEmailAsync(EmailMessage message)
    {
        await _queue.Writer.WriteAsync(message);
    }

    public async ValueTask DequeueEmailAsync(CancellationToken cancellationToken)
    {
        return await _queue.Reader.ReadAsync(cancellationToken);
    }
}

public class EmailProcessingService : BackgroundService
{
    private readonly IEmailQueue _emailQueue;
    private readonly IEmailService _emailService;
    private readonly ILogger _logger;

    public EmailProcessingService(
        IEmailQueue emailQueue,
        IEmailService emailService,
        ILogger logger)
    {
        _emailQueue = emailQueue;
        _emailService = emailService;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var message in GetEmailMessages(stoppingToken))
        {
            try
            {
                await _emailService.SendEmailAsync(message);
                _logger.LogInformation("Email sent successfully to {Recipient}", message.To);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error sending email to {Recipient}", message.To);
                // Implement retry logic or dead letter queue
            }
        }
    }

    private async IAsyncEnumerable GetEmailMessages(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            EmailMessage? message = null;
            try
            {
                message = await _emailQueue.DequeueEmailAsync(cancellationToken);
            }
            catch (OperationCanceledException)
            {
                break;
            }

            if (message != null)
            {
                yield return message;
            }
        }
    }
}

// Data cleanup service
public class DataCleanupService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger _logger;
    private readonly PeriodicTimer _timer;

    public DataCleanupService(IServiceProvider serviceProvider, ILogger logger)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
        _timer = new PeriodicTimer(TimeSpan.FromHours(24)); // Run daily
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (await _timer.WaitForNextTickAsync(stoppingToken))
        {
            _logger.LogInformation("Starting data cleanup job");
            
            try
            {
                using var scope = _serviceProvider.CreateScope();
                var dbContext = scope.ServiceProvider.GetRequiredService();
                
                await CleanupOldDataAsync(dbContext);
                
                _logger.LogInformation("Data cleanup job completed successfully");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error during data cleanup job");
            }
        }
    }

    private async Task CleanupOldDataAsync(ApplicationDbContext dbContext)
    {
        var cutoffDate = DateTime.UtcNow.AddDays(-90);
        
        // Clean up soft-deleted records older than 90 days
        var oldDeletedProducts = await dbContext.Products
            .Where(p => p.IsDeleted && p.UpdatedAt < cutoffDate)
            .ToListAsync();
            
        dbContext.Products.RemoveRange(oldDeletedProducts);
        
        // Clean up expired sessions, logs, etc.
        var expiredSessions = await dbContext.UserSessions
            .Where(s => s.ExpiresAt < DateTime.UtcNow)
            .ToListAsync();
            
        dbContext.UserSessions.RemoveRange(expiredSessions);
        
        await dbContext.SaveChangesAsync();
        
        _logger.LogInformation("Cleaned up {ProductCount} products and {SessionCount} sessions", 
            oldDeletedProducts.Count, expiredSessions.Count);
    }

    public override void Dispose()
    {
        _timer?.Dispose();
        base.Dispose();
    }
}

// Register services in Program.cs
builder.Services.AddSingleton();
builder.Services.AddHostedService();
builder.Services.AddHostedService();

Integration cu Hangfire pentru Job-uri Complexe

csharp
// Install Hangfire
// dotnet add package Hangfire.AspNetCore
// dotnet add package Hangfire.MySql

// Program.cs
builder.Services.AddHangfire(configuration => configuration
    .SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
    .UseSimpleAssemblyNameTypeSerializer()
    .UseRecommendedSerializerSettings()
    .UseStorage(new MySqlStorage(connectionString, new MySqlStorageOptions
    {
        TransactionIsolationLevel = IsolationLevel.ReadCommitted,
        QueuePollInterval = TimeSpan.FromSeconds(15),
        JobExpirationCheckInterval = TimeSpan.FromHours(1),
        CountersAggregateInterval = TimeSpan.FromMinutes(5),
        PrepareSchemaIfNecessary = true,
        DashboardJobListLimit = 50000,
        TransactionTimeout = TimeSpan.FromMinutes(1),
        TablesPrefix = "hangfire_"
    })));

builder.Services.AddHangfireServer(options =>
{
    options.WorkerCount = Environment.ProcessorCount * 2;
    options.Queues = new[] { "critical", "default", "background" };
});

// Job definitions
public interface IReportService
{
    Task GenerateMonthlyReportAsync();
    Task ProcessLargeDatasetAsync(int batchSize);
    Task SendBulkEmailsAsync(List recipients, string template);
}

public class ReportService : IReportService
{
    private readonly ApplicationDbContext _context;
    private readonly IEmailService _emailService;
    private readonly ILogger _logger;

    public ReportService(ApplicationDbContext context, IEmailService emailService, ILogger logger)
    {
        _context = context;
        _emailService = emailService;
        _logger = logger;
    }

    [AutomaticRetry(Attempts = 3)]
    [Queue("background")]
    public async Task GenerateMonthlyReportAsync()
    {
        _logger.LogInformation("Starting monthly report generation");
        
        var startDate = DateTime.UtcNow.AddMonths(-1).Date;
        var endDate = DateTime.UtcNow.Date;
        
        var reportData = await _context.Orders
            .Where(o => o.OrderDate >= startDate && o.OrderDate < endDate)
            .GroupBy(o => o.OrderDate.Date)
            .Select(g => new
            {
                Date = g.Key,
                OrderCount = g.Count(),
                TotalRevenue = g.Sum(o => o.TotalAmount)
            })
            .ToListAsync();
        
        // Generate PDF report
        var pdfContent = GeneratePdfReport(reportData);
        
        // Send to stakeholders
        await _emailService.SendEmailWithAttachmentAsync(
            "stakeholders@company.com",
            "Monthly Sales Report",
            "Please find attached the monthly sales report.",
            pdfContent,
            "monthly-report.pdf");
        
        _logger.LogInformation("Monthly report generated and sent successfully");
    }

    [Queue("critical")]
    public async Task ProcessLargeDatasetAsync(int batchSize)
    {
        var totalRecords = await _context.Products.CountAsync();
        var batches = (int)Math.Ceiling((double)totalRecords / batchSize);
        
        for (int i = 0; i < batches; i++)
        {
            var batch = await _context.Products
                .Skip(i * batchSize)
                .Take(batchSize)
                .ToListAsync();
            
            // Process batch
            await ProcessProductBatch(batch);
            
            _logger.LogInformation("Processed batch {BatchNumber}/{TotalBatches}", i + 1, batches);
            
            // Small delay to prevent overwhelming the database
            await Task.Delay(100);
        }
    }

    private byte[] GeneratePdfReport(object reportData)
    {
        // Implementation using iTextSharp, PdfSharp, or similar
        return new byte[0]; // Placeholder
    }

    private async Task ProcessProductBatch(List products)
    {
        // Batch processing logic
        await Task.CompletedTask;
    }
}

// Schedule jobs
public class JobScheduler
{
    private readonly IRecurringJobManager _recurringJobManager;

    public JobScheduler(IRecurringJobManager recurringJobManager)
    {
        _recurringJobManager = recurringJobManager;
    }

    public void ScheduleJobs()
    {
        // Monthly reports on 1st day of month at 2 AM
        _recurringJobManager.AddOrUpdate(
            "monthly-report",
            service => service.GenerateMonthlyReportAsync(),
            "0 2 1 * *",
            TimeZoneInfo.FindSystemTimeZoneById("UTC"));

        // Daily cleanup at 3 AM
        _recurringJobManager.AddOrUpdate(
            "daily-cleanup", 
            service => service.CleanupOldDataAsync(),
            Cron.Daily(3),
            TimeZoneInfo.FindSystemTimeZoneById("UTC"));
    }
}

// In controller - trigger background jobs
[HttpPost("generate-report")]
public IActionResult TriggerReportGeneration()
{
    BackgroundJob.Enqueue(service => service.GenerateMonthlyReportAsync());
    return Accepted();
}

[HttpPost("process-data/{batchSize}")]
public IActionResult ProcessLargeDataset(int batchSize)
{
    BackgroundJob.Enqueue(service => service.ProcessLargeDatasetAsync(batchSize));
    return Accepted();
}

🔢 API Versioning

Setup API Versioning

csharp
// Install package: dotnet add package Asp.Versioning.Http

// Program.cs
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ApiVersionReader = ApiVersionReader.Combine(
        new UrlSegmentApiVersionReader(),
        new QueryStringApiVersionReader("version"),
        new HeaderApiVersionReader("X-Version"),
        new MediaTypeApiVersionReader("ver")
    );
}).AddApiExplorer(setup =>
{
    setup.GroupNameFormat = "'v'VVV";
    setup.SubstituteApiVersionInUrl = true;
});

// V1 Controller
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class ProductsV1Controller : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsV1Controller(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet]
    public async Task>> GetProducts()
    {
        var products = await _productService.GetProductsAsync();
        return Ok(products.Select(MapToV1Dto));
    }

    [HttpGet("{id}")]
    public async Task> GetProduct(long id)
    {
        var product = await _productService.GetProductByIdAsync(id);
        return product == null ? NotFound() : Ok(MapToV1Dto(product));
    }

    private ProductV1Dto MapToV1Dto(ProductDto product)
    {
        return new ProductV1Dto
        {
            Id = product.Id,
            Name = product.Name,
            Price = product.Price,
            Category = product.CategoryName
        };
    }
}

// V2 Controller with enhanced features
[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class ProductsV2Controller : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsV2Controller(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet]
    public async Task>> GetProducts(
        [FromQuery] ProductQueryParameters parameters)
    {
        var products = await _productService.GetProductsAsync(parameters);
        return Ok(new PagedResult
        {
            Items = products.Items.Select(MapToV2Dto).ToList(),
            TotalCount = products.TotalCount,
            PageNumber = products.PageNumber,
            PageSize = products.PageSize
        });
    }

    [HttpGet("{id}")]
    public async Task> GetProduct(long id)
    {
        var product = await _productService.GetProductDetailByIdAsync(id);
        return product == null ? NotFound() : Ok(MapToV2DetailDto(product));
    }

    [HttpPost]
    public async Task> CreateProduct([FromBody] CreateProductV2Dto dto)
    {
        var product = await _productService.CreateProductAsync(MapFromV2Dto(dto));
        return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, MapToV2Dto(product));
    }

    private ProductV2Dto MapToV2Dto(ProductDto product)
    {
        return new ProductV2Dto
        {
            Id = product.Id,
            Name = product.Name,
            Description = product.Description,
            Price = product.Price,
            CompareAtPrice = product.CompareAtPrice,
            StockQuantity = product.StockQuantity,
            Category = new CategorySummaryDto
            {
                Id = product.CategoryId,
                Name = product.CategoryName
            },
            CreatedAt = product.CreatedAt,
            IsActive = product.IsActive
        };
    }
}

// DTOs for different versions
public record ProductV1Dto
{
    public long Id { get; init; }
    public string Name { get; init; } = string.Empty;
    public decimal Price { get; init; }
    public string Category { get; init; } = string.Empty;
}

public record ProductV2Dto
{
    public long Id { get; init; }
    public string Name { get; init; } = string.Empty;
    public string? Description { get; init; }
    public decimal Price { get; init; }
    public decimal? CompareAtPrice { get; init; }
    public int StockQuantity { get; init; }
    public CategorySummaryDto Category { get; init; } = new();
    public DateTime CreatedAt { get; init; }
    public bool IsActive { get; init; }
}

public record CategorySummaryDto
{
    public long Id { get; init; }
    public string Name { get; init; } = string.Empty;
}

// Swagger configuration for multiple versions
builder.Services.ConfigureOptions();

public class ConfigureSwaggerOptions : IConfigureNamedOptions
{
    private readonly IApiVersionDescriptionProvider _provider;

    public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider)
    {
        _provider = provider;
    }

    public void Configure(string name, SwaggerGenOptions options)
    {
        Configure(options);
    }

    public void Configure(SwaggerGenOptions options)
    {
        foreach (var description in _provider.ApiVersionDescriptions)
        {
            options.SwaggerDoc(description.GroupName, CreateVersionInfo(description));
        }
    }

    private OpenApiInfo CreateVersionInfo(ApiVersionDescription description)
    {
        var info = new OpenApiInfo()
        {
            Title = "MyApp API",
            Version = description.ApiVersion.ToString(),
            Description = "A comprehensive API for MyApp with MySQL backend.",
            Contact = new OpenApiContact
            {
                Name = "Development Team",
                Email = "dev@myapp.com"
            }
        };

        if (description.IsDeprecated)
        {
            info.Description += " This API version has been deprecated.";
        }

        return info;
    }
}

// Configure Swagger UI for multiple versions
app.UseSwaggerUI(options =>
{
    var descriptions = app.DescribeApiVersions();
    
    foreach (var description in descriptions)
    {
        var url = $"/swagger/{description.GroupName}/swagger.json";
        var name = description.GroupName.ToUpperInvariant();
        options.SwaggerEndpoint(url, name);
    }
    
    options.RoutePrefix = "swagger";
    options.DocExpansion(DocExpansion.List);
});

🎯 Concluzie & Resurse Suplimentare

Acest ghid comprehensive acoperă aspectele esențiale pentru dezvoltarea aplicațiilor moderne ASP.NET Core cu Entity Framework Core și MySQL. De la arhitectura de bază până la deployment în producție, ați învățat să construiți aplicații robuste, scalabile și performante.

Puncte Cheie

🏗️ Arhitectură

  • Clean Architecture pentru separarea responsabilităților
  • Dependency Injection pentru decuplare
  • Repository Pattern pentru abstractizarea datelor

⚡ Performanță

  • Multi-level caching cu Redis
  • Query optimization cu EF Core
  • Response caching și output caching

🔒 Securitate

  • JWT authentication și authorization
  • Input validation și sanitization
  • Rate limiting și security headers

🚀 DevOps

  • Docker containerization
  • CI/CD cu GitHub Actions
  • Kubernetes deployment

Resurse Suplimentare

Resursa Descriere Link
.NET Documentation Documentația oficială Microsoft docs.microsoft.com/dotnet
EF Core Docs Ghidul complet Entity Framework Core docs.microsoft.com/ef/core
MySQL Documentation Documentația oficială MySQL dev.mysql.com/doc
Clean Architecture Template oficial pentru Clean Architecture GitHub Template
Felicitări! 🎉

Ați parcurs cu succes acest ghid comprehensive. Acum aveți cunoștințele necesare pentru a dezvolta aplicații enterprise-grade cu ASP.NET Core și MySQL. Continuați să exersați și să explorați tehnologiile pentru a vă perfecționa abilitățile.

Următorii Pași

  1. Construiți un proiect propriu - Aplicați cunoștințele într-un proiect real
  2. Explorați microserviciile - Învățați despre arhitectura distribuită
  3. Studiați cloud patterns - Azure, AWS sau Google Cloud Platform
  4. Aprofundați DevOps - GitOps, Infrastructure as Code, Monitoring
  5. Contribuiți la proiecte open source - Participați la comunitatea .NET