242 lines
6.3 KiB
Markdown
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).
|