🚀 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.
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
# 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
# Î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
// 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
// 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
# 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
// 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
// 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
// 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
// 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));
}
}
• 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ță
// 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
-- 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)
☑️ 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ă