academia/docs/entregables/02-diseno/arquitectura/DI-008-manejo-errores.md

242 lines
6.3 KiB
Markdown

# DI-008: Estrategia de Manejo de Errores
**Proyecto:** Sistema de Registro de Estudiantes
**Fecha:** 2026-01-07
---
## 1. Clasificación de Errores
| Tipo | Origen | Manejo | HTTP Code (equiv) |
|------|--------|--------|-------------------|
| **Validación** | FluentValidation | Payload.errors | 400 |
| **Dominio** | Domain Exceptions | Payload.errors | 422 |
| **Not Found** | Repository | Payload.errors | 404 |
| **Conflicto** | Concurrencia | Payload.errors | 409 |
| **Sistema** | Excepciones no manejadas | Error GraphQL | 500 |
---
## 2. Excepciones de Dominio
```csharp
// Base
public abstract class DomainException : Exception
{
public string Code { get; }
protected DomainException(string code, string message) : base(message)
=> Code = code;
}
// Específicas
public class MaxEnrollmentsExceededException : DomainException
{
public MaxEnrollmentsExceededException()
: base("MAX_ENROLLMENTS", "Máximo 3 materias permitidas") { }
}
public class SameProfessorConstraintException : DomainException
{
public SameProfessorConstraintException(string professorName)
: base("SAME_PROFESSOR", $"Ya tienes una materia con {professorName}") { }
}
public class DuplicateEmailException : DomainException
{
public DuplicateEmailException()
: base("DUPLICATE_EMAIL", "Este email ya está registrado") { }
}
public class StudentNotFoundException : DomainException
{
public StudentNotFoundException(int id)
: base("NOT_FOUND", $"Estudiante {id} no encontrado") { }
}
```
---
## 3. Patrón Result
```csharp
public class Result
{
public bool IsSuccess { get; }
public IEnumerable<string> Errors { get; }
protected Result(bool success, IEnumerable<string>? errors = null)
{
IsSuccess = success;
Errors = errors ?? Array.Empty<string>();
}
public static Result Success() => new(true);
public static Result Failure(params string[] errors) => new(false, errors);
}
public class Result<T> : Result
{
public T? Value { get; }
private Result(T value) : base(true) => Value = value;
private Result(IEnumerable<string> errors) : base(false, errors) { }
public static Result<T> Success(T value) => new(value);
public static new Result<T> Failure(params string[] errors) => new(errors);
}
```
---
## 4. Handler con Manejo de Errores
```csharp
public class EnrollStudentHandler
{
public async Task<EnrollmentPayload> Handle(EnrollInput input)
{
try
{
var student = await _studentRepo.GetByIdWithEnrollmentsAsync(input.StudentId);
if (student is null)
return new EnrollmentPayload(null, ["Estudiante no encontrado"]);
var subject = await _subjectRepo.GetByIdAsync(input.SubjectId);
if (subject is null)
return new EnrollmentPayload(null, ["Materia no encontrada"]);
// Validación de dominio
student.Enroll(subject, _enrollmentPolicy);
await _unitOfWork.SaveChangesAsync();
var dto = student.Enrollments.Last().Adapt<EnrollmentDto>();
return new EnrollmentPayload(dto, null);
}
catch (DomainException ex)
{
return new EnrollmentPayload(null, [ex.Message]);
}
}
}
```
---
## 5. Error Filter GraphQL (HotChocolate)
```csharp
public class ErrorFilter : IErrorFilter
{
public IError OnError(IError error)
{
return error.Exception switch
{
DomainException ex => error
.WithMessage(ex.Message)
.WithCode(ex.Code),
ValidationException ex => error
.WithMessage("Errores de validación")
.WithCode("VALIDATION_ERROR")
.SetExtension("errors", ex.Errors.Select(e => e.ErrorMessage)),
DbUpdateConcurrencyException => error
.WithMessage("Los datos fueron modificados por otro usuario")
.WithCode("CONCURRENCY_ERROR"),
_ => error
.WithMessage("Error interno del servidor")
.WithCode("INTERNAL_ERROR")
};
}
}
```
---
## 6. Manejo en Frontend
### Service
```typescript
@Injectable({ providedIn: 'root' })
export class ErrorHandlerService {
private snackBar = inject(MatSnackBar);
handleGraphQLErrors(errors: string[] | undefined): void {
if (errors?.length) {
this.snackBar.open(errors[0], 'Cerrar', {
duration: 5000,
panelClass: 'error-snackbar'
});
}
}
handleApolloError(error: ApolloError): void {
const message = error.graphQLErrors[0]?.message ?? 'Error de conexión';
this.snackBar.open(message, 'Cerrar', { duration: 5000 });
}
}
```
### Componente
```typescript
enrollStudent(subjectId: number) {
this.enrollmentService.enroll({
studentId: this.studentId(),
subjectId
}).subscribe({
next: ({ data }) => {
if (data?.enrollStudent.errors?.length) {
this.errorHandler.handleGraphQLErrors(data.enrollStudent.errors);
} else {
this.snackBar.open('Inscripción exitosa', 'OK');
}
},
error: (err) => this.errorHandler.handleApolloError(err)
});
}
```
---
## 7. Códigos de Error Estándar
| Código | Mensaje | Acción UI |
|--------|---------|-----------|
| `VALIDATION_ERROR` | Errores de validación | Mostrar en campos |
| `MAX_ENROLLMENTS` | Máximo 3 materias | Toast + deshabilitar |
| `SAME_PROFESSOR` | Ya tienes materia con X | Toast + deshabilitar |
| `DUPLICATE_EMAIL` | Email ya registrado | Error en campo |
| `NOT_FOUND` | Recurso no encontrado | Redirect + toast |
| `CONCURRENCY_ERROR` | Datos modificados | Refetch + toast |
| `INTERNAL_ERROR` | Error del servidor | Toast genérico |
---
## 8. Logging de Errores
```csharp
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public async Task<TResponse> Handle(TRequest request, ...)
{
try
{
return await next();
}
catch (Exception ex) when (ex is not DomainException)
{
_logger.LogError(ex, "Error procesando {Request}", typeof(TRequest).Name);
throw;
}
}
}
```
**Regla:** NO loguear datos sensibles (email, contraseñas).