258 lines
5.7 KiB
Markdown
258 lines
5.7 KiB
Markdown
|
|
# DI-007: Contratos y DTOs
|
||
|
|
|
||
|
|
**Proyecto:** Sistema de Registro de Estudiantes
|
||
|
|
**Fecha:** 2026-01-07
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. DTOs Backend (C#)
|
||
|
|
|
||
|
|
### Request DTOs (Inputs)
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
// Students
|
||
|
|
public record CreateStudentInput(string Name, string Email);
|
||
|
|
public record UpdateStudentInput(string? Name, string? Email);
|
||
|
|
|
||
|
|
// Enrollments
|
||
|
|
public record EnrollInput(int StudentId, int SubjectId);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Response DTOs
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
// Student
|
||
|
|
public record StudentDto(
|
||
|
|
int Id,
|
||
|
|
string Name,
|
||
|
|
string Email,
|
||
|
|
int TotalCredits,
|
||
|
|
IEnumerable<EnrollmentDto> Enrollments);
|
||
|
|
|
||
|
|
// Subject
|
||
|
|
public record SubjectDto(
|
||
|
|
int Id,
|
||
|
|
string Name,
|
||
|
|
int Credits,
|
||
|
|
ProfessorDto Professor);
|
||
|
|
|
||
|
|
// Professor
|
||
|
|
public record ProfessorDto(
|
||
|
|
int Id,
|
||
|
|
string Name);
|
||
|
|
|
||
|
|
// Enrollment
|
||
|
|
public record EnrollmentDto(
|
||
|
|
int Id,
|
||
|
|
SubjectDto Subject,
|
||
|
|
DateTime EnrolledAt);
|
||
|
|
|
||
|
|
// Available Subject (con disponibilidad)
|
||
|
|
public record AvailableSubjectDto(
|
||
|
|
SubjectDto Subject,
|
||
|
|
bool IsAvailable,
|
||
|
|
string? UnavailableReason);
|
||
|
|
|
||
|
|
// Classmates
|
||
|
|
public record ClassmateDto(
|
||
|
|
string SubjectName,
|
||
|
|
IEnumerable<string> StudentNames);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Payloads (Respuestas con errores)
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
public record StudentPayload(StudentDto? Student, IEnumerable<string>? Errors);
|
||
|
|
public record EnrollmentPayload(EnrollmentDto? Enrollment, IEnumerable<string>? Errors);
|
||
|
|
public record DeletePayload(bool Success, IEnumerable<string>? Errors);
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. DTOs Frontend (TypeScript)
|
||
|
|
|
||
|
|
### Tipos Generados (graphql-codegen)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Student
|
||
|
|
export interface Student {
|
||
|
|
id: number;
|
||
|
|
name: string;
|
||
|
|
email: string;
|
||
|
|
totalCredits: number;
|
||
|
|
enrollments: Enrollment[];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Subject
|
||
|
|
export interface Subject {
|
||
|
|
id: number;
|
||
|
|
name: string;
|
||
|
|
credits: number;
|
||
|
|
professor: Professor;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Professor
|
||
|
|
export interface Professor {
|
||
|
|
id: number;
|
||
|
|
name: string;
|
||
|
|
subjects?: Subject[];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Enrollment
|
||
|
|
export interface Enrollment {
|
||
|
|
id: number;
|
||
|
|
subject: Subject;
|
||
|
|
enrolledAt: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Available Subject
|
||
|
|
export interface AvailableSubject {
|
||
|
|
subject: Subject;
|
||
|
|
isAvailable: boolean;
|
||
|
|
unavailableReason?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Classmate
|
||
|
|
export interface Classmate {
|
||
|
|
subjectName: string;
|
||
|
|
students: string[];
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Inputs
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
export interface CreateStudentInput {
|
||
|
|
name: string;
|
||
|
|
email: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface UpdateStudentInput {
|
||
|
|
name?: string;
|
||
|
|
email?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface EnrollInput {
|
||
|
|
studentId: number;
|
||
|
|
subjectId: number;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Payloads
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
export interface StudentPayload {
|
||
|
|
student?: Student;
|
||
|
|
errors?: string[];
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface EnrollmentPayload {
|
||
|
|
enrollment?: Enrollment;
|
||
|
|
errors?: string[];
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface DeletePayload {
|
||
|
|
success: boolean;
|
||
|
|
errors?: string[];
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. Mapeos (Mapster)
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
public class MappingConfig : IRegister
|
||
|
|
{
|
||
|
|
public void Register(TypeAdapterConfig config)
|
||
|
|
{
|
||
|
|
config.NewConfig<Student, StudentDto>()
|
||
|
|
.Map(dest => dest.TotalCredits, src => src.Enrollments.Count * 3);
|
||
|
|
|
||
|
|
config.NewConfig<Subject, SubjectDto>();
|
||
|
|
config.NewConfig<Professor, ProfessorDto>();
|
||
|
|
config.NewConfig<Enrollment, EnrollmentDto>();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. Validadores (FluentValidation)
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
public class CreateStudentInputValidator : AbstractValidator<CreateStudentInput>
|
||
|
|
{
|
||
|
|
public CreateStudentInputValidator()
|
||
|
|
{
|
||
|
|
RuleFor(x => x.Name)
|
||
|
|
.NotEmpty().WithMessage("Nombre requerido")
|
||
|
|
.MaximumLength(100).WithMessage("Máximo 100 caracteres");
|
||
|
|
|
||
|
|
RuleFor(x => x.Email)
|
||
|
|
.NotEmpty().WithMessage("Email requerido")
|
||
|
|
.EmailAddress().WithMessage("Formato de email inválido")
|
||
|
|
.MaximumLength(255).WithMessage("Máximo 255 caracteres");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public class EnrollInputValidator : AbstractValidator<EnrollInput>
|
||
|
|
{
|
||
|
|
public EnrollInputValidator()
|
||
|
|
{
|
||
|
|
RuleFor(x => x.StudentId)
|
||
|
|
.GreaterThan(0).WithMessage("StudentId inválido");
|
||
|
|
|
||
|
|
RuleFor(x => x.SubjectId)
|
||
|
|
.GreaterThan(0).WithMessage("SubjectId inválido");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. Contratos de Repositorio (Ports)
|
||
|
|
|
||
|
|
```csharp
|
||
|
|
public interface IStudentRepository
|
||
|
|
{
|
||
|
|
Task<Student?> GetByIdAsync(int id, CancellationToken ct = default);
|
||
|
|
Task<Student?> GetByIdWithEnrollmentsAsync(int id, CancellationToken ct = default);
|
||
|
|
Task<IEnumerable<Student>> GetAllAsync(CancellationToken ct = default);
|
||
|
|
Task<bool> EmailExistsAsync(string email, int? excludeId = null, CancellationToken ct = default);
|
||
|
|
Task AddAsync(Student student, CancellationToken ct = default);
|
||
|
|
void Update(Student student);
|
||
|
|
void Delete(Student student);
|
||
|
|
}
|
||
|
|
|
||
|
|
public interface ISubjectRepository
|
||
|
|
{
|
||
|
|
Task<Subject?> GetByIdAsync(int id, CancellationToken ct = default);
|
||
|
|
Task<IEnumerable<Subject>> GetAllAsync(CancellationToken ct = default);
|
||
|
|
Task<IEnumerable<Subject>> GetByProfessorIdAsync(int professorId, CancellationToken ct = default);
|
||
|
|
}
|
||
|
|
|
||
|
|
public interface IEnrollmentRepository
|
||
|
|
{
|
||
|
|
Task<IEnumerable<Enrollment>> GetByStudentIdAsync(int studentId, CancellationToken ct = default);
|
||
|
|
Task<IEnumerable<Student>> GetClassmatesAsync(int studentId, int subjectId, CancellationToken ct = default);
|
||
|
|
}
|
||
|
|
|
||
|
|
public interface IUnitOfWork
|
||
|
|
{
|
||
|
|
Task<int> SaveChangesAsync(CancellationToken ct = default);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. Resumen de Contratos por Operación
|
||
|
|
|
||
|
|
| Operación | Input | Output | Errores Posibles |
|
||
|
|
|-----------|-------|--------|------------------|
|
||
|
|
| createStudent | CreateStudentInput | StudentPayload | DUPLICATE_EMAIL, VALIDATION |
|
||
|
|
| updateStudent | UpdateStudentInput | StudentPayload | NOT_FOUND, DUPLICATE_EMAIL |
|
||
|
|
| deleteStudent | id: Int | DeletePayload | NOT_FOUND |
|
||
|
|
| enrollStudent | EnrollInput | EnrollmentPayload | MAX_ENROLLMENTS, SAME_PROFESSOR |
|
||
|
|
| unenrollStudent | enrollmentId: Int | DeletePayload | NOT_FOUND |
|