Instruction file imported from DaveSoftDev/Linkubator (
.github/instructions/csharp.instructions.md). Copyright stays with the author.
Nomenclatura específica (no expresable en .editorconfig)
Qué debes tener en cuenta cuando propongas código
Todo el código propuesto para esta solución debe ser como si lo hiciera un desarrollador senior con experiencia en entornos seguros, escalables y de alta demanda.
Organización de clases
La regla canónica de organización del código está en AGENTS.md bajo “Convención de estructura del código”.
Este archivo no duplica la política general: solo recuerda aplicarla en C# y mantener la ruta real del proyecto alineada con el namespace. En la práctica:
- cada tipo vive en la carpeta que refleja su responsabilidad;
- no se dejan tipos en la raíz salvo puntos de entrada o composición;
Contracts/queda para interfaces y contratos;Results/para resultados/errores;Enums/para enumeraciones;Requests/para DTOs de entrada;Models/oDtos/Models/para salidas y modelos;Constants/para literales, mensajes y códigos;UseCases/,Services/yRepositories/para la implementación concreta.
Si aparece una duda, la fuente de decisión es AGENTS.md.
Async
- Los métodos propios de
LinkubatorNO DEBEN terminar con el sufijoAsync— el paradigma del proyecto es que todos los métodos son asíncronos por defecto. - Sólo pueden terminar en
Asynclos propios métodos del framework de .NET.
Evitar async void — usar siempre Task o Task<T>
❌ Incorrecto:
public async void GetCollections(long userId) { }
✅ Correcto:
public async Task GetCollections(long userId) { }
public async Task<Collection> GetCollection(long id) { }
Sufijos por tipo de archivo/clase
- Razor Page models → sufijo
Model(IndexModel, DisplayUrlsModel) - OptionsPattern → sufijo
Options(AppOptions) - Extension methods → sufijo
Extensions(WebRedirectorExtensions) - DTOs de entrada → sufijo
Request(LinkRequest) - DTOs de salida → sufijo
Response(LinkResponse) - Otros DTOs → sufijo
DTO
Métodos de repositorio
- Obtención: Get, GetAll, GetById, GetBy{Column}
- Creación: Insert, Add
- Actualización: Update
- Eliminación: Delete, Remove
Namespaces
NO usar usings generales a namespaces de la solución Usar alias de using para los namespaces internos
❌ Incorrecto:
using Linkubator.Domain.Entities;
✅ Correcto — alias en el using:
using Entities = Linkubator.Domain.Entities;
O bien, usar el nombre completo directamente en el código sin using.
Inyección de Dependencias
- Todas las dependencias deben inyectarse mediante el constructor
- Todos los servicios deben implementar interfaces
✅ Ejemplo correcto:
public class LinkubatorApplicationService : ILinkubatorApplicationService
{
private readonly IUnitOfWorkFactory _unitOfWorkFactory;
private readonly ILogger<LinkubatorApplicationService> _logger;
public LinkubatorApplicationService(IUnitOfWorkFactory unitOfWorkFactory, ILogger<LinkubatorApplicationService> logger)
{
_unitOfWorkFactory = unitOfWorkFactory;
_logger = logger;
}
}
Recursos Descartables
IUnitOfWork implementa IAsyncDisposable — usar await using
✅ Correcto:
await using (IUnitOfWork unitOfWork = _unitOfWorkFactory.Get(Linkubator.Application.Constants.UnitOfWorkContexts.Linkubator))
{
ILinkubatorRepository repository = unitOfWork.GetRepository<ILinkubatorRepository>();
await repository.Insert(entity);
await unitOfWork.Commit();
}
Para recursos síncronos (IDisposable), usar using convencional.
Manejo de Errores
En aplicaciones web (Razor Pages): el GlobalExceptionHandler centraliza el logging y la redirección a /Errors/500 para cualquier excepción no controlada. No añadir try-catch genérico en los Page Model handlers — es código duplicado. Solo usar try-catch cuando haya que manejar un error de negocio específico (por ejemplo, mostrar un mensaje de validación).
✅ En Razor Pages — solo para manejo específico de negocio:
public async Task<IActionResult> OnPostAsync()
{
try
{
await _service.Link(Request);
return RedirectToPage();
}
catch (UrlSchemeNotAllowedException)
{
ModelState.AddModelError(string.Empty, "El esquema de la URL no está permitido.");
return Page();
}
// Las excepciones no controladas las gestiona GlobalExceptionHandler
}
Configuración y Options Pattern
Options Pattern para Configuración
Usar Options Pattern para toda la configuración de la aplicación
✅ Estructura:
// En Program.cs o Bootstrap
builder.Services.Configure<AppOptions>(
builder.Configuration.GetSection("AppOptions"));
// En el servicio
public class SomeService
{
private readonly AppOptions _appOptions;
public SomeService(IOptions<AppOptions> appOptions)
{
_appOptions = appOptions.Value;
}
}
Ubicación de Options
Linkubator.Web/Options/— opciones específicas de la Web.Linkubator.Application/Options/— opciones compartidas entre capas.Linkubator.Infrastructure/Options/— opciones específicas de la infraestructura.- Inyectar usando
IOptions<T>,IOptionsSnapshot<T>oIOptionsMonitor<T>.
Uso de Records
Records para Datos Inmutables
Usar record para DTOs inmutables, errores y mensajes
- Ubicación:
Linkubator.Application/Records/ - Usar para errores y trazas de log
- Usar para respuestas de API inmutables
✅ Ejemplos:
// En Linkubator.Application/Records/Errors.cs
public record ErrorRecord(string Code, string Message, DateTime Timestamp);
// En Linkubator.Application/Records/Traces.cs
public record TraceRecord(string EventId, string Description);
Métodos de Extensión
Estructura y Nomenclatura
Crear métodos de extensión para funcionalidad reutilizable
Ubicación por Capa
Linkubator.Domain/Extensions/ # Extensiones de dominio
Linkubator.Application/Extensions/ # Extensiones de aplicación
Linkubator.Infrastructure.Repositories/Extensions/ # Extensiones de repositorios
Linkubator.Web/Extensions/ # Extensiones de presentación web
Nomenclatura de Clases de Extensión
- Nombrar como:
[Tipo]Extensions - Ejemplos:
ServiceCollectionExtensions,WebApplicationBuilderExtensions
Convenciones para IServiceCollection
- Métodos para registrar servicios: comenzar con
Add - Métodos para configurar opciones: comenzar con
Configure
✅ Ejemplo:
// En Linkubator.Infrastructure.Repositories/Extensions/ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddRepositories(this IServiceCollection services)
{
services.AddScoped<ILinkubatorRepository, LinkubatorRepository>();
return services;
}
}
Convenciones para IApplicationBuilder / WebApplicationBuilder
- Métodos deben comenzar con
UseUseCustomMiddleware()UseErrorHandling()
Logging y Trazabilidad
Inyección y Uso de Logger
Siempre inyectar ILogger<T> donde T es la clase actual
✅ Ejemplo:
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
}
Niveles de Log
-
LogError: Excepciones y errores críticos
_logger.LogError(ex, "Error"); -
LogWarning: Situaciones anómalas no críticas
_logger.LogWarning("Warning"); -
LogInformation: Eventos importantes del flujo
_logger.LogInformation("Information"); -
LogDebug: Información de depuración
_logger.LogDebug("Debug");
Entidades y Estructura de Domain
Entidades Base
Todas las entidades deben heredar de BaseEntity
BaseEntitycontiene propiedades comunes:Id,CreatedAt, etc.- Ubicación:
Linkubator.Domain/Entities/
✅ Ejemplo:
public class Collection : BaseEntity
{
public string Name { get; private set; }
public string Description { get; private set; }
}
Organización del proyecto Domain
Todo vive en Linkubator.Domain (no existe un proyecto Domain.Entities separado):
Linkubator.Domain/
├── Constants/ # Constantes del dominio
├── DTOs/
│ ├── Requests/ # DTOs de entrada (sufijo Request)
│ └── Responses/ # DTOs de salida (sufijo Response)
├── Entities/ # Entidades del dominio (heredan de BaseEntity)
│ └── Bases/ # BaseEntity
├── Enums/ # ScrapingStatus, Purpose
├── Exceptions/ # UrlSchemeNotAllowedException, etc.
└── POCOs/ # ViewModels y otros objetos planos
Razor Pages
Convenciones de PageModel
Nomenclatura
- El modelo de la página debe terminar con
Model:IndexModel,DashboardModel - Los ViewModels se ubican en el proyecto con sufijo
Model
Propiedades Enlazadas
Usar [BindProperty] para propiedades que se enlazan desde formularios
✅ Ejemplo:
public class IndexModel : PageModel
{
[BindProperty]
public Entities.Link Link { get; set; } = new();
public CollectionsModel CollectionsModel { get; set; } = new();
}
Métodos Handler
OnGetAsync(): Para peticiones GETOnPostAsync(): Para peticiones POSTOnPost[Action]Async(): Para handlers específicos (ej:OnPostDeleteAsync())
Uso de Constants
Evitar Strings y Números Mágicos
Nunca usar strings o números mágicos en el código
❌ Incorrecto:
IUnitOfWork uow = _uowFactory.Get("Linkubator");
✅ Correcto:
IUnitOfWork uow = _uowFactory.Get(Linkubator.Application.Constants.UnitOfWorkContexts.Linkubator);
Organización de Constants
- Ubicación:
Linkubator.Domain/Constants/ - Crear clases estáticas agrupadas por contexto
✅ Ejemplo:
// En Linkubator.Domain/Constants/LengthLimitsConstants.cs
public static class LengthLimitsConstants
{
public const int UserName = 100;
}
Gestión de Unit of Work
Uso de Factory Pattern
IUnitOfWork es IAsyncDisposable — usar await using
✅ Patrón correcto:
await using (IUnitOfWork uow = _unitOfWorkFactory.Get(Linkubator.Application.Constants.UnitOfWorkContexts.Linkubator))
{
ILinkRepository repository = uow.GetRepository<ILinkRepository>();
Entities.Link link = await repository.GetById(id);
entity.Description = "Updated";
await repository.Update(entity);
await uow.Commit();
}
Múltiples Contextos
Usar bloques await using separados para diferentes contextos
✅ Ejemplo:
await using (IUnitOfWork uowLinkubator = _unitOfWorkFactory.Get(Linkubator.Application.Constants.UnitOfWorkContexts.Linkubator))
{
IEnumerable<Entities.Link> links = await uowLinkubator.GetRepository<ILinkRepository>().GetAll();
}
await using (IUnitOfWork uowOther = _unitOfWorkFactory.Get(Linkubator.Application.Constants.UnitOfWorkContexts.OtherContext))
{
IEnumerable<Entities.OtherEntity> otherEntities = await uowOther.GetRepository<IOtherEntityRepository>().GetAll();
}
Patrones de Diseño
Repository Pattern
- Las interfaces base genéricas viven en
Linkubator.Infrastructure.Repositories.Base/Contracts/ - Repositorios específicos por entidad en
Linkubator.Infrastructure.Repositories - Repository Pattern:
I*Repository→*Repository - Los repositorios reciben POCOs/DTOs de entrada y retornan entidades de dominio
- Ubicación de contratos:
Linkubator.Application/Contracts/Infrastructure/Repositories/
Unit of Work Pattern
IUnitOfWorkimplementaIAsyncDisposable- Usar
await using(nousingsimple) para garantizar el dispose asíncrono - Factory para crear instancias:
IUnitOfWorkFactory - Implementaciones:
LinkubatorUnitOfWork,CommonsUnitOfWork
Factory Pattern
IConnectionFactorypara crear conexiones a base de datosIUnitOfWorkFactorypara obtener instancias de UnitOfWork según contexto- Factory Pattern:
I*Factory→*Factory - No crear conexiones directas en repositorios o servicios
Resumen de Mejores Prácticas
✅ Hacer Siempre
- Usar tipos explícitos (no
var). - Usar
await usingparaIUnitOfWork(IAsyncDisposable);usingparaIDisposable. - Inyectar dependencias por constructor.
- Todos los servicios implementan interfaces.
- Usar
try-catchen endpoints de API; en Razor Pages solo para errores de negocio específicos. - Usar alias de using para namespaces internos.
- Heredar entidades de
BaseEntity. - Usar Factory Pattern para UnitOfWork y conexiones.
- Usar Options Pattern para configuración.
- Inyectar
ILogger<T>donde T es la clase actual. - Usar constants en lugar de strings mágicos o números mágicos.
- Sufijos
Request/Responsepara DTOs. - Sufijo
Modelpara modelos de Razor Pages. - Usa siempre el estilo Allman (Allman brace style) al escribir instrucciones de control de flujo (*control flow statements) aunque sólo afecte a una línea de código.
❌ Evitar Siempre
async void.varpara declarar variables.- Crear conexiones directas sin Factory.
- Strings o números mágicos en el código.
usinggenerales a namespaces de la solución (usar alias).- Múltiples responsabilidades en una clase.
- Dependencias sin interfaces.
try-catchgenérico en Page Model handlers.
