Instrucciones de Copilot para Proyectos .NET Web API¶
Requisitos de Proceso Importantes¶
- SIEMPRE presenta un plan detallado y espera aprobación explícita antes de implementar cualquier cambio de código
- No proceder con la implementación hasta recibir confirmación del usuario
- Al presentar el plan, proporciona un desglose paso a paso de todos los archivos a crear o modificar
- Pregunta directamente: "¿Apruebas este plan antes de proceder con la implementación?"
Convenciones de Nomenclatura¶
🔤 Patrones Generales de Nomenclatura¶
| Tipo de Elemento | Patrón/Estilo | Ejemplo(s) |
|---|---|---|
| Clases | PascalCase |
UserService, ProductController |
| Interfaces | IPascalCase |
IUserService, IRepository<T> |
| Métodos | PascalCase |
GetUserById(), CreateAsync() |
| Propiedades | PascalCase |
UserId, FirstName |
| Variables Locales | camelCase |
userId, totalAmount |
| Parámetros | camelCase |
userId, requestModel |
| Campos Privados | _camelCase |
_userService, _logger |
| Constantes | PascalCase |
MaxRetryAttempts, DefaultTimeout |
| Enums | PascalCase |
UserStatus, OrderType |
| Namespaces | PascalCase.PascalCase |
MyApp.Services, MyApp.Models |
🧠 Mejores Prácticas de Nomenclatura¶
- Usar nombres descriptivos y completos (evitar abreviaciones como
Usr,Mgr) - Para métodos booleanos, usar prefijos como
Is,Has,Can,Should - Para métodos async, usar sufijo
Async - Para controladores, usar sufijo
Controller - Para servicios, usar sufijo
Service - Para DTOs, usar sufijos como
Dto,Request,Response - Evitar nombres que coincidan con palabras reservadas de C#
✅ Estilo de Código¶
📐 Formato y Estructura¶
- Usar 4 espacios para indentación
- Usar Allman style para llaves (nueva línea)
- Longitud máxima de línea: 120 caracteres
- Usar EditorConfig para mantener consistencia
- Configurar StyleCop y Roslyn Analyzers
// Correcto - Allman style
public class UserService
{
private readonly IUserRepository _userRepository;
public UserService(IUserRepository userRepository)
{
_userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository));
}
public async Task<User> GetUserByIdAsync(int userId)
{
if (userId <= 0)
{
throw new ArgumentException("User ID must be positive", nameof(userId));
}
return await _userRepository.GetByIdAsync(userId);
}
}
🔠 Using Statements y Namespaces¶
- Ordenar using statements alfabéticamente
- Separar using statements del sistema de los de terceros y locales
- Usar file-scoped namespaces en .NET 6+
- Usar global using para dependencias comunes
// Global usings (GlobalUsings.cs)
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading.Tasks;
global using Microsoft.AspNetCore.Mvc;
global using Microsoft.Extensions.Logging;
// En archivos específicos
using MyApp.Models;
using MyApp.Services;
using MyApp.DTOs;
namespace MyApp.Controllers;
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
// Implementation
}
🏗️ Estructura del Proyecto¶
Estructura Recomendada para Web API¶
MyApp.WebApi/
├── Controllers/
│ ├── BaseController.cs
│ ├── UsersController.cs
│ └── ProductsController.cs
├── Models/
│ ├── Entities/
│ │ ├── BaseEntity.cs
│ │ ├── User.cs
│ │ └── Product.cs
│ └── DTOs/
│ ├── UserDto.cs
│ ├── CreateUserRequest.cs
│ └── UpdateUserRequest.cs
├── Services/
│ ├── Interfaces/
│ │ ├── IUserService.cs
│ │ └── IProductService.cs
│ ├── UserService.cs
│ └── ProductService.cs
├── Repositories/
│ ├── Interfaces/
│ │ ├── IRepository.cs
│ │ └── IUserRepository.cs
│ ├── BaseRepository.cs
│ └── UserRepository.cs
├── Data/
│ ├── ApplicationDbContext.cs
│ ├── Configurations/
│ │ ├── UserConfiguration.cs
│ │ └── ProductConfiguration.cs
│ └── Migrations/
├── Middleware/
│ ├── ExceptionHandlingMiddleware.cs
│ └── RequestLoggingMiddleware.cs
├── Extensions/
│ ├── ServiceCollectionExtensions.cs
│ └── ApplicationBuilderExtensions.cs
├── Validators/
│ ├── CreateUserRequestValidator.cs
│ └── UpdateUserRequestValidator.cs
├── Filters/
│ ├── ValidationFilter.cs
│ └── AuthorizationFilter.cs
├── Configuration/
│ ├── AppSettings.cs
│ └── DatabaseSettings.cs
├── GlobalUsings.cs
├── Program.cs
└── appsettings.json
📝 Estándares de Documentación¶
XML Documentation¶
- Documentar todas las clases, métodos y propiedades públicas
- Usar tags XML estándar:
<summary>,<param>,<returns>,<exception> - Incluir ejemplos cuando sea apropiado
/// <summary>
/// Service for managing user operations.
/// </summary>
public class UserService : IUserService
{
/// <summary>
/// Retrieves a user by their unique identifier.
/// </summary>
/// <param name="userId">The unique identifier of the user.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>The user if found; otherwise, null.</returns>
/// <exception cref="ArgumentException">Thrown when userId is less than or equal to zero.</exception>
/// <example>
/// <code>
/// var user = await userService.GetUserByIdAsync(123);
/// if (user != null)
/// {
/// Console.WriteLine($"Found user: {user.Name}");
/// }
/// </code>
/// </example>
public async Task<User?> GetUserByIdAsync(int userId, CancellationToken cancellationToken = default)
{
if (userId <= 0)
{
throw new ArgumentException("User ID must be positive", nameof(userId));
}
return await _userRepository.GetByIdAsync(userId, cancellationToken);
}
}
Swagger/OpenAPI Documentation¶
- Configurar Swagger para documentación automática de API
- Usar atributos para enriquecer la documentación
- Incluir ejemplos de request/response
/// <summary>
/// Creates a new user in the system.
/// </summary>
/// <param name="request">The user creation request.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The created user information.</returns>
/// <response code="201">User created successfully</response>
/// <response code="400">Invalid request data</response>
/// <response code="409">User already exists</response>
[HttpPost]
[ProducesResponseType(typeof(UserDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
public async Task<ActionResult<UserDto>> CreateUserAsync(
[FromBody] CreateUserRequest request,
CancellationToken cancellationToken = default)
{
var user = await _userService.CreateUserAsync(request, cancellationToken);
return CreatedAtAction(nameof(GetUserByIdAsync), new { id = user.Id }, user);
}
🧪 Guías de Testing¶
Estructura de Tests¶
- Usar xUnit como framework de testing principal
- Organizar tests en proyectos separados por tipo (Unit, Integration, E2E)
- Usar FluentAssertions para assertions más legibles
- Implementar Test Fixtures para setup común
Patrones de Testing¶
public class UserServiceTests
{
private readonly Mock<IUserRepository> _userRepositoryMock;
private readonly Mock<ILogger<UserService>> _loggerMock;
private readonly UserService _userService;
public UserServiceTests()
{
_userRepositoryMock = new Mock<IUserRepository>();
_loggerMock = new Mock<ILogger<UserService>>();
_userService = new UserService(_userRepositoryMock.Object, _loggerMock.Object);
}
[Fact]
public async Task GetUserByIdAsync_WithValidId_ShouldReturnUser()
{
// Arrange
const int userId = 1;
var expectedUser = new User { Id = userId, Name = "John Doe" };
_userRepositoryMock
.Setup(x => x.GetByIdAsync(userId, It.IsAny<CancellationToken>()))
.ReturnsAsync(expectedUser);
// Act
var result = await _userService.GetUserByIdAsync(userId);
// Assert
result.Should().NotBeNull();
result.Should().BeEquivalentTo(expectedUser);
_userRepositoryMock.Verify(x => x.GetByIdAsync(userId, It.IsAny<CancellationToken>()), Times.Once);
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(-100)]
public async Task GetUserByIdAsync_WithInvalidId_ShouldThrowArgumentException(int invalidId)
{
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentException>(
() => _userService.GetUserByIdAsync(invalidId));
exception.ParamName.Should().Be("userId");
exception.Message.Should().Contain("User ID must be positive");
}
}
Integration Tests¶
public class UsersControllerIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly HttpClient _client;
public UsersControllerIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
_client = _factory.CreateClient();
}
[Fact]
public async Task GetUsers_ShouldReturnOkWithUsersList()
{
// Act
var response = await _client.GetAsync("/api/users");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
var users = JsonSerializer.Deserialize<List<UserDto>>(content, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
users.Should().NotBeNull();
users.Should().BeOfType<List<UserDto>>();
}
}
📦 Gestión de Dependencias y Paquetes¶
Paquetes Recomendados¶
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<WarningsNotAsErrors>CS1591</WarningsNotAsErrors>
</PropertyGroup>
<ItemGroup>
<!-- Core packages -->
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<!-- Entity Framework -->
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
<!-- Validation -->
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
<!-- Mapping -->
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<!-- Authentication -->
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<!-- Logging -->
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
</ItemGroup>
<!-- Test packages (in test projects) -->
<ItemGroup Condition="'$(IsTestProject)' == 'true'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Moq" Version="4.20.69" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
</ItemGroup>
</Project>
🌐 Configuración de API y Controllers¶
Base Controller¶
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public abstract class BaseController : ControllerBase
{
protected readonly ILogger Logger;
protected BaseController(ILogger logger)
{
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
protected ActionResult<T> HandleResult<T>(T result)
{
if (result == null)
{
return NotFound();
}
return Ok(result);
}
protected ActionResult HandleResult(bool success)
{
return success ? Ok() : BadRequest();
}
}
Controller Implementation¶
/// <summary>
/// Controller for managing users.
/// </summary>
[ApiController]
[Route("api/[controller]")]
[Tags("Users")]
public class UsersController : BaseController
{
private readonly IUserService _userService;
public UsersController(IUserService userService, ILogger<UsersController> logger)
: base(logger)
{
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
}
/// <summary>
/// Gets all users with pagination.
/// </summary>
/// <param name="pageNumber">Page number (default: 1).</param>
/// <param name="pageSize">Page size (default: 10, max: 100).</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Paginated list of users.</returns>
[HttpGet]
[ProducesResponseType(typeof(PagedResult<UserDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResult<UserDto>>> GetUsersAsync(
[FromQuery] int pageNumber = 1,
[FromQuery] int pageSize = 10,
CancellationToken cancellationToken = default)
{
if (pageSize > 100)
{
pageSize = 100;
}
var result = await _userService.GetUsersAsync(pageNumber, pageSize, cancellationToken);
return Ok(result);
}
/// <summary>
/// Gets a user by ID.
/// </summary>
/// <param name="id">User ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>User information.</returns>
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<UserDto>> GetUserByIdAsync(
int id,
CancellationToken cancellationToken = default)
{
var user = await _userService.GetUserByIdAsync(id, cancellationToken);
return HandleResult(user);
}
}
🔒 Manejo de Errores y Middleware¶
Global Exception Handling¶
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
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 static async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
var response = context.Response;
response.ContentType = "application/json";
var problemDetails = exception switch
{
ArgumentException argEx => new ProblemDetails
{
Status = StatusCodes.Status400BadRequest,
Title = "Bad Request",
Detail = argEx.Message,
Instance = context.Request.Path
},
NotFoundException notFoundEx => new ProblemDetails
{
Status = StatusCodes.Status404NotFound,
Title = "Not Found",
Detail = notFoundEx.Message,
Instance = context.Request.Path
},
ValidationException validationEx => new ValidationProblemDetails(validationEx.Errors)
{
Status = StatusCodes.Status400BadRequest,
Title = "Validation Error",
Instance = context.Request.Path
},
_ => new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Title = "Internal Server Error",
Detail = "An error occurred while processing your request.",
Instance = context.Request.Path
}
};
response.StatusCode = problemDetails.Status ?? StatusCodes.Status500InternalServerError;
var jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
await response.WriteAsync(JsonSerializer.Serialize(problemDetails, jsonOptions));
}
}
Custom Exceptions¶
public abstract class BaseException : Exception
{
protected BaseException(string message) : base(message) { }
protected BaseException(string message, Exception innerException) : base(message, innerException) { }
}
public class NotFoundException : BaseException
{
public NotFoundException(string entityName, object key)
: base($"{entityName} with key '{key}' was not found.")
{
}
}
public class ValidationException : BaseException
{
public IDictionary<string, string[]> Errors { get; }
public ValidationException(IDictionary<string, string[]> errors)
: base("One or more validation errors occurred.")
{
Errors = errors;
}
}
public class BusinessRuleException : BaseException
{
public BusinessRuleException(string message) : base(message) { }
}
🔧 Dependency Injection y Configuration¶
Service Registration¶
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
// Services
services.AddScoped<IUserService, UserService>();
services.AddScoped<IProductService, ProductService>();
// Repositories
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IProductRepository, ProductRepository>();
// AutoMapper
services.AddAutoMapper(typeof(Program));
// FluentValidation
services.AddValidatorsFromAssemblyContaining<CreateUserRequestValidator>();
return services;
}
public static IServiceCollection AddInfrastructureServices(
this IServiceCollection services,
IConfiguration configuration)
{
// Database
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
// Authentication
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = configuration["Jwt:Issuer"],
ValidAudience = configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(configuration["Jwt:Key"]!))
};
});
return services;
}
}
Program.cs Configuration¶
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "My API",
Version = "v1",
Description = "A sample API with best practices"
});
// Include XML comments
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
// Custom services
builder.Services.AddApplicationServices();
builder.Services.AddInfrastructureServices(builder.Configuration);
// Logging
builder.Host.UseSerilog((context, configuration) =>
configuration.ReadFrom.Configuration(context.Configuration));
var app = builder.Build();
// Configure pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
// Custom middleware
app.UseMiddleware<ExceptionHandlingMiddleware>();
app.UseMiddleware<RequestLoggingMiddleware>();
app.MapControllers();
// Database migration
using (var scope = app.Services.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await context.Database.MigrateAsync();
}
app.Run();
🗄️ Entity Framework y Data Access¶
DbContext Configuration¶
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
public DbSet<User> Users => Set<User>();
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Apply configurations
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
// Global query filters
modelBuilder.Entity<User>().HasQueryFilter(u => !u.IsDeleted);
modelBuilder.Entity<Product>().HasQueryFilter(p => !p.IsDeleted);
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
// Audit fields
var entries = ChangeTracker.Entries<BaseEntity>();
foreach (var entry in entries)
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedAt = DateTime.UtcNow;
entry.Entity.UpdatedAt = DateTime.UtcNow;
break;
case EntityState.Modified:
entry.Entity.UpdatedAt = DateTime.UtcNow;
break;
}
}
return await base.SaveChangesAsync(cancellationToken);
}
}
Entity Configuration¶
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.ToTable("Users");
builder.HasKey(u => u.Id);
builder.Property(u => u.Email)
.IsRequired()
.HasMaxLength(256);
builder.Property(u => u.FirstName)
.IsRequired()
.HasMaxLength(100);
builder.Property(u => u.LastName)
.IsRequired()
.HasMaxLength(100);
builder.HasIndex(u => u.Email)
.IsUnique();
// Relationships
builder.HasMany(u => u.Orders)
.WithOne(o => o.User)
.HasForeignKey(o => o.UserId)
.OnDelete(DeleteBehavior.Cascade);
}
}
Repository Pattern¶
public interface IRepository<T> where T : BaseEntity
{
Task<T?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
Task<IEnumerable<T>> GetAllAsync(CancellationToken cancellationToken = default);
Task<PagedResult<T>> GetPagedAsync(int pageNumber, int pageSize, CancellationToken cancellationToken = default);
Task<T> AddAsync(T entity, CancellationToken cancellationToken = default);
Task UpdateAsync(T entity, CancellationToken cancellationToken = default);
Task DeleteAsync(int id, CancellationToken cancellationToken = default);
}
public class BaseRepository<T> : IRepository<T> where T : BaseEntity
{
protected readonly ApplicationDbContext Context;
protected readonly DbSet<T> DbSet;
public BaseRepository(ApplicationDbContext context)
{
Context = context;
DbSet = context.Set<T>();
}
public virtual async Task<T?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
{
return await DbSet.FindAsync(new object[] { id }, cancellationToken);
}
public virtual async Task<IEnumerable<T>> GetAllAsync(CancellationToken cancellationToken = default)
{
return await DbSet.ToListAsync(cancellationToken);
}
public virtual async Task<PagedResult<T>> GetPagedAsync(
int pageNumber,
int pageSize,
CancellationToken cancellationToken = default)
{
var totalCount = await DbSet.CountAsync(cancellationToken);
var items = await DbSet
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return new PagedResult<T>(items, totalCount, pageNumber, pageSize);
}
public virtual async Task<T> AddAsync(T entity, CancellationToken cancellationToken = default)
{
DbSet.Add(entity);
await Context.SaveChangesAsync(cancellationToken);
return entity;
}
public virtual async Task UpdateAsync(T entity, CancellationToken cancellationToken = default)
{
DbSet.Update(entity);
await Context.SaveChangesAsync(cancellationToken);
}
public virtual async Task DeleteAsync(int id, CancellationToken cancellationToken = default)
{
var entity = await GetByIdAsync(id, cancellationToken);
if (entity != null)
{
entity.IsDeleted = true;
await UpdateAsync(entity, cancellationToken);
}
}
}
🔐 Authentication y Authorization¶
JWT Configuration¶
public class JwtService : IJwtService
{
private readonly IConfiguration _configuration;
private readonly ILogger<JwtService> _logger;
public JwtService(IConfiguration configuration, ILogger<JwtService> logger)
{
_configuration = configuration;
_logger = logger;
}
public string GenerateToken(User user)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]!));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.Name, $"{user.FirstName} {user.LastName}"),
new Claim("userId", user.Id.ToString())
};
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddHours(24),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public ClaimsPrincipal? ValidateToken(string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]!);
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = _configuration["Jwt:Issuer"],
ValidAudience = _configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(key),
ClockSkew = TimeSpan.Zero
};
var principal = tokenHandler.ValidateToken(token, validationParameters, out _);
return principal;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Token validation failed");
return null;
}
}
}
Authorization Policies¶
public static class AuthorizationPolicies
{
public const string RequireAdminRole = "RequireAdminRole";
public const string RequireUserRole = "RequireUserRole";
public const string RequireEmailVerified = "RequireEmailVerified";
}
// In Program.cs
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(AuthorizationPolicies.RequireAdminRole, policy =>
policy.RequireRole("Admin"));
options.AddPolicy(AuthorizationPolicies.RequireUserRole, policy =>
policy.RequireRole("User", "Admin"));
options.AddPolicy(AuthorizationPolicies.RequireEmailVerified, policy =>
policy.RequireClaim("email_verified", "true"));
});
🚀 Rendimiento y Optimización¶
Caching¶
public class CachedUserService : IUserService
{
private readonly IUserService _userService;
private readonly IMemoryCache _cache;
private readonly ILogger<CachedUserService> _logger;
private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(15);
public CachedUserService(
IUserService userService,
IMemoryCache cache,
ILogger<CachedUserService> logger)
{
_userService = userService;
_cache = cache;
_logger = logger;
}
public async Task<User?> GetUserByIdAsync(int userId, CancellationToken cancellationToken = default)
{
var cacheKey = $"user_{userId}";
if (_cache.TryGetValue(cacheKey, out User? cachedUser))
{
_logger.LogDebug("User {UserId} retrieved from cache", userId);
return cachedUser;
}
var user = await _userService.GetUserByIdAsync(userId, cancellationToken);
if (user != null)
{
_cache.Set(cacheKey, user, _cacheDuration);
_logger.LogDebug("User {UserId} cached for {Duration}", userId, _cacheDuration);
}
return user;
}
}
Background Services¶
public class EmailNotificationService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<EmailNotificationService> _logger;
public EmailNotificationService(
IServiceProvider serviceProvider,
ILogger<EmailNotificationService> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = _serviceProvider.CreateScope();
var emailService = scope.ServiceProvider.GetRequiredService<IEmailService>();
await emailService.ProcessPendingEmailsAsync(stoppingToken);
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing emails");
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
}
}
📊 Logging y Monitoreo¶
Structured Logging con Serilog¶
// Program.cs
builder.Host.UseSerilog((context, configuration) =>
{
configuration
.ReadFrom.Configuration(context.Configuration)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithEnvironmentName()
.WriteTo.Console(outputTemplate:
"[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
.WriteTo.File("logs/app-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 7);
});
// Usage in services
public class UserService : IUserService
{
private readonly ILogger<UserService> _logger;
public async Task<User> CreateUserAsync(CreateUserRequest request, CancellationToken cancellationToken)
{
using var activity = _logger.BeginScope("Creating user {Email}", request.Email);
_logger.LogInformation("Starting user creation for {Email}", request.Email);
try
{
var user = new User
{
Email = request.Email,
FirstName = request.FirstName,
LastName = request.LastName
};
await _userRepository.AddAsync(user, cancellationToken);
_logger.LogInformation("User {UserId} created successfully", user.Id);
return user;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create user for {Email}", request.Email);
throw;
}
}
}
Health Checks¶
// Program.cs
builder.Services.AddHealthChecks()
.AddDbContextCheck<ApplicationDbContext>()
.AddSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")!)
.AddCheck<EmailServiceHealthCheck>("email_service");
// Custom health check
public class EmailServiceHealthCheck : IHealthCheck
{
private readonly IEmailService _emailService;
public EmailServiceHealthCheck(IEmailService emailService)
{
_emailService = emailService;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var isHealthy = await _emailService.IsHealthyAsync(cancellationToken);
return isHealthy
? HealthCheckResult.Healthy("Email service is responsive")
: HealthCheckResult.Unhealthy("Email service is not responding");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Email service check failed", ex);
}
}
}
// Configure health check endpoints
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
🔧 Configuración Avanzada¶
Options Pattern¶
public class EmailSettings
{
public const string SectionName = "Email";
public string SmtpServer { get; set; } = string.Empty;
public int SmtpPort { get; set; }
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public bool EnableSsl { get; set; }
public string FromAddress { get; set; } = string.Empty;
public string FromName { get; set; } = string.Empty;
}
// Registration
builder.Services.Configure<EmailSettings>(
builder.Configuration.GetSection(EmailSettings.SectionName));
// Usage
public class EmailService : IEmailService
{
private readonly EmailSettings _emailSettings;
public EmailService(IOptions<EmailSettings> emailSettings)
{
_emailSettings = emailSettings.Value;
}
}
Environment-specific Configuration¶
// appsettings.json
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyAppDb;Trusted_Connection=true;"
},
"Jwt": {
"Key": "your-secret-key-here",
"Issuer": "MyApp",
"Audience": "MyApp",
"ExpiryInHours": 24
},
"Email": {
"SmtpServer": "smtp.gmail.com",
"SmtpPort": 587,
"EnableSsl": true,
"FromAddress": "noreply@myapp.com",
"FromName": "MyApp"
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
}
}
}
🐳 Containerización¶
Dockerfile Multi-stage¶
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copy csproj and restore dependencies
COPY ["MyApp.WebApi/MyApp.WebApi.csproj", "MyApp.WebApi/"]
RUN dotnet restore "MyApp.WebApi/MyApp.WebApi.csproj"
# Copy everything else and build
COPY . .
WORKDIR "/src/MyApp.WebApi"
RUN dotnet build "MyApp.WebApi.csproj" -c Release -o /app/build
# Publish stage
FROM build AS publish
RUN dotnet publish "MyApp.WebApi.csproj" -c Release -o /app/publish /p:UseAppHost=false
# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
# Create non-root user
RUN adduser --disabled-password --gecos '' appuser && chown -R appuser /app
USER appuser
COPY --from=publish /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.WebApi.dll"]
Docker Compose¶
version: '3.8'
services:
api:
build: .
ports:
- "8080:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__DefaultConnection=Server=db;Database=MyAppDb;User=sa;Password=YourPassword123!;TrustServerCertificate=true
depends_on:
- db
networks:
- app-network
db:
image: mcr.microsoft.com/mssql/server:2022-latest
environment:
- ACCEPT_EULA=Y
- SA_PASSWORD=YourPassword123!
ports:
- "1433:1433"
volumes:
- sqlserver_data:/var/opt/mssql
networks:
- app-network
volumes:
sqlserver_data:
networks:
app-network:
driver: bridge
🎯 Mejores Prácticas Específicas de .NET¶
Async/Await Best Practices¶
// ✅ Correcto
public async Task<User> GetUserAsync(int id)
{
return await _repository.GetByIdAsync(id);
}
// ✅ Correcto - ConfigureAwait(false) en bibliotecas
public async Task<User> GetUserAsync(int id)
{
return await _repository.GetByIdAsync(id).ConfigureAwait(false);
}
// ❌ Incorrecto - Bloqueo síncrono
public User GetUser(int id)
{
return _repository.GetByIdAsync(id).Result; // Puede causar deadlock
}
// ✅ Correcto - Usar CancellationToken
public async Task<IEnumerable<User>> GetUsersAsync(CancellationToken cancellationToken = default)
{
return await _repository.GetAllAsync(cancellationToken);
}
Memory Management¶
// ✅ Usar using statements para IDisposable
public async Task ProcessFileAsync(string filePath)
{
using var fileStream = new FileStream(filePath, FileMode.Open);
using var reader = new StreamReader(fileStream);
var content = await reader.ReadToEndAsync();
// Process content
}
// ✅ Usar ArrayPool para arrays grandes
public void ProcessLargeArray()
{
var pool = ArrayPool<byte>.Shared;
var buffer = pool.Rent(1024);
try
{
// Use buffer
}
finally
{
pool.Return(buffer);
}
}
Validation with FluentValidation¶
public class CreateUserRequestValidator : AbstractValidator<CreateUserRequest>
{
public CreateUserRequestValidator()
{
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required")
.EmailAddress().WithMessage("Invalid email format")
.MaximumLength(256).WithMessage("Email must not exceed 256 characters");
RuleFor(x => x.FirstName)
.NotEmpty().WithMessage("First name is required")
.MaximumLength(100).WithMessage("First name must not exceed 100 characters");
RuleFor(x => x.LastName)
.NotEmpty().WithMessage("Last name is required")
.MaximumLength(100).WithMessage("Last name must not exceed 100 characters");
RuleFor(x => x.Age)
.GreaterThan(0).WithMessage("Age must be greater than 0")
.LessThan(150).WithMessage("Age must be less than 150")
.When(x => x.Age.HasValue);
}
}
💡 Consejo: Mantén siempre la consistencia usando EditorConfig, StyleCop y Roslyn Analyzers. Configura CI/CD pipelines para automatizar la verificación de calidad de código y tests.