feat(adapters): add driven and driving adapters
Driven Adapters (Persistence): - AppDbContext with EF Core configurations - Repository implementations (Student, Subject, Professor, Enrollment) - UnitOfWork pattern for transactions - DataLoaders for GraphQL N+1 optimization - Database seeding with 5 professors and 10 subjects - EF Core migrations for SQL Server Driving Adapters (API): - GraphQL API with HotChocolate - Query and Mutation types - Type definitions for all entities - GraphQLErrorFilter for domain exceptions
This commit is contained in:
parent
68e420fdf2
commit
60d35f1040
|
|
@ -0,0 +1,23 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Domain\Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="*" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="*" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="*">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="GreenDonut" Version="*" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
namespace Adapters.Driven.Persistence;
|
||||
|
||||
using Adapters.Driven.Persistence.Context;
|
||||
using Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled EF Core queries for frequently used operations.
|
||||
/// Compiled queries skip expression tree parsing on each execution.
|
||||
/// </summary>
|
||||
public static class CompiledQueries
|
||||
{
|
||||
// Student queries
|
||||
public static readonly Func<AppDbContext, int, Task<Student?>> GetStudentById =
|
||||
EF.CompileAsyncQuery((AppDbContext ctx, int id) =>
|
||||
ctx.Students.FirstOrDefault(s => s.Id == id));
|
||||
|
||||
public static readonly Func<AppDbContext, int, Task<bool>> StudentExists =
|
||||
EF.CompileAsyncQuery((AppDbContext ctx, int id) =>
|
||||
ctx.Students.Any(s => s.Id == id));
|
||||
|
||||
// EmailExists removed - using raw SQL in repository due to value object translation issues
|
||||
|
||||
// Subject queries
|
||||
public static readonly Func<AppDbContext, int, Task<Subject?>> GetSubjectById =
|
||||
EF.CompileAsyncQuery((AppDbContext ctx, int id) =>
|
||||
ctx.Subjects.FirstOrDefault(s => s.Id == id));
|
||||
|
||||
public static readonly Func<AppDbContext, IAsyncEnumerable<Subject>> GetAllSubjects =
|
||||
EF.CompileAsyncQuery((AppDbContext ctx) =>
|
||||
ctx.Subjects.AsNoTracking());
|
||||
|
||||
// Professor queries
|
||||
public static readonly Func<AppDbContext, int, Task<Professor?>> GetProfessorById =
|
||||
EF.CompileAsyncQuery((AppDbContext ctx, int id) =>
|
||||
ctx.Professors.FirstOrDefault(p => p.Id == id));
|
||||
|
||||
// Enrollment queries
|
||||
public static readonly Func<AppDbContext, int, Task<int>> CountStudentEnrollments =
|
||||
EF.CompileAsyncQuery((AppDbContext ctx, int studentId) =>
|
||||
ctx.Enrollments.Count(e => e.StudentId == studentId));
|
||||
|
||||
public static readonly Func<AppDbContext, int, int, Task<bool>> EnrollmentExists =
|
||||
EF.CompileAsyncQuery((AppDbContext ctx, int studentId, int subjectId) =>
|
||||
ctx.Enrollments.Any(e => e.StudentId == studentId && e.SubjectId == subjectId));
|
||||
|
||||
// Count queries (for pagination)
|
||||
public static readonly Func<AppDbContext, Task<int>> CountStudents =
|
||||
EF.CompileAsyncQuery((AppDbContext ctx) =>
|
||||
ctx.Students.Count());
|
||||
|
||||
public static readonly Func<AppDbContext, Task<int>> CountSubjects =
|
||||
EF.CompileAsyncQuery((AppDbContext ctx) =>
|
||||
ctx.Subjects.Count());
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
namespace Adapters.Driven.Persistence.Configurations;
|
||||
|
||||
using Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
public class EnrollmentConfiguration : IEntityTypeConfiguration<Enrollment>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Enrollment> builder)
|
||||
{
|
||||
builder.ToTable("Enrollments");
|
||||
|
||||
builder.HasKey(e => e.Id);
|
||||
|
||||
builder.Property(e => e.EnrolledAt)
|
||||
.IsRequired();
|
||||
|
||||
builder.HasIndex(e => new { e.StudentId, e.SubjectId }).IsUnique();
|
||||
|
||||
builder.HasOne(e => e.Student)
|
||||
.WithMany(s => s.Enrollments)
|
||||
.HasForeignKey(e => e.StudentId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(e => e.Subject)
|
||||
.WithMany(s => s.Enrollments)
|
||||
.HasForeignKey(e => e.SubjectId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
namespace Adapters.Driven.Persistence.Configurations;
|
||||
|
||||
using Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
public class ProfessorConfiguration : IEntityTypeConfiguration<Professor>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Professor> builder)
|
||||
{
|
||||
builder.ToTable("Professors");
|
||||
|
||||
builder.HasKey(p => p.Id);
|
||||
|
||||
builder.Property(p => p.Name)
|
||||
.IsRequired()
|
||||
.HasMaxLength(100);
|
||||
|
||||
builder.HasMany(p => p.Subjects)
|
||||
.WithOne(s => s.Professor)
|
||||
.HasForeignKey(s => s.ProfessorId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
namespace Adapters.Driven.Persistence.Configurations;
|
||||
|
||||
using Domain.Entities;
|
||||
using Domain.ValueObjects;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
public class StudentConfiguration : IEntityTypeConfiguration<Student>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Student> builder)
|
||||
{
|
||||
builder.ToTable("Students");
|
||||
|
||||
builder.HasKey(s => s.Id);
|
||||
|
||||
builder.Property(s => s.Name)
|
||||
.IsRequired()
|
||||
.HasMaxLength(100);
|
||||
|
||||
// Email value object with proper converter and comparer
|
||||
var emailComparer = new ValueComparer<Email>(
|
||||
(e1, e2) => e1 != null && e2 != null && e1.Value == e2.Value,
|
||||
e => e.Value.GetHashCode(),
|
||||
e => Email.Create(e.Value));
|
||||
|
||||
builder.Property(s => s.Email)
|
||||
.IsRequired()
|
||||
.HasMaxLength(150)
|
||||
.HasConversion(
|
||||
e => e.Value,
|
||||
v => Email.Create(v))
|
||||
.Metadata.SetValueComparer(emailComparer);
|
||||
|
||||
// Use raw column name for index to avoid value object issues
|
||||
builder.HasIndex("Email").IsUnique();
|
||||
|
||||
builder.HasMany(s => s.Enrollments)
|
||||
.WithOne(e => e.Student)
|
||||
.HasForeignKey(e => e.StudentId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.Navigation(s => s.Enrollments).AutoInclude(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
namespace Adapters.Driven.Persistence.Configurations;
|
||||
|
||||
using Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
public class SubjectConfiguration : IEntityTypeConfiguration<Subject>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Subject> builder)
|
||||
{
|
||||
builder.ToTable("Subjects");
|
||||
|
||||
builder.HasKey(s => s.Id);
|
||||
|
||||
builder.Property(s => s.Name)
|
||||
.IsRequired()
|
||||
.HasMaxLength(100);
|
||||
|
||||
builder.Property(s => s.Credits)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(3);
|
||||
|
||||
builder.HasOne(s => s.Professor)
|
||||
.WithMany(p => p.Subjects)
|
||||
.HasForeignKey(s => s.ProfessorId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasMany(s => s.Enrollments)
|
||||
.WithOne(e => e.Subject)
|
||||
.HasForeignKey(e => e.SubjectId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
namespace Adapters.Driven.Persistence.Context;
|
||||
|
||||
using Adapters.Driven.Persistence.Seeding;
|
||||
using Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<Student> Students => Set<Student>();
|
||||
public DbSet<Subject> Subjects => Set<Subject>();
|
||||
public DbSet<Professor> Professors => Set<Professor>();
|
||||
public DbSet<Enrollment> Enrollments => Set<Enrollment>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
|
||||
DataSeeder.Seed(modelBuilder);
|
||||
base.OnModelCreating(modelBuilder);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
namespace Adapters.Driven.Persistence.DataLoaders;
|
||||
|
||||
using Adapters.Driven.Persistence.Context;
|
||||
using Domain.Entities;
|
||||
using GreenDonut;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
public class ProfessorByIdDataLoader : BatchDataLoader<int, Professor>
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _contextFactory;
|
||||
|
||||
public ProfessorByIdDataLoader(
|
||||
IDbContextFactory<AppDbContext> contextFactory,
|
||||
IBatchScheduler batchScheduler,
|
||||
DataLoaderOptions? options = null)
|
||||
: base(batchScheduler, options ?? new DataLoaderOptions())
|
||||
{
|
||||
_contextFactory = contextFactory;
|
||||
}
|
||||
|
||||
protected override async Task<IReadOnlyDictionary<int, Professor>> LoadBatchAsync(
|
||||
IReadOnlyList<int> keys,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await using var context = await _contextFactory.CreateDbContextAsync(ct);
|
||||
return await context.Professors
|
||||
.Where(p => keys.Contains(p.Id))
|
||||
.ToDictionaryAsync(p => p.Id, ct);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
namespace Adapters.Driven.Persistence.DataLoaders;
|
||||
|
||||
using Adapters.Driven.Persistence.Context;
|
||||
using Domain.Entities;
|
||||
using GreenDonut;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
public class StudentByIdDataLoader : BatchDataLoader<int, Student>
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _contextFactory;
|
||||
|
||||
public StudentByIdDataLoader(
|
||||
IDbContextFactory<AppDbContext> contextFactory,
|
||||
IBatchScheduler batchScheduler,
|
||||
DataLoaderOptions? options = null)
|
||||
: base(batchScheduler, options ?? new DataLoaderOptions())
|
||||
{
|
||||
_contextFactory = contextFactory;
|
||||
}
|
||||
|
||||
protected override async Task<IReadOnlyDictionary<int, Student>> LoadBatchAsync(
|
||||
IReadOnlyList<int> keys,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await using var context = await _contextFactory.CreateDbContextAsync(ct);
|
||||
return await context.Students
|
||||
.Where(s => keys.Contains(s.Id))
|
||||
.ToDictionaryAsync(s => s.Id, ct);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
namespace Adapters.Driven.Persistence.DataLoaders;
|
||||
|
||||
using Adapters.Driven.Persistence.Context;
|
||||
using Domain.Entities;
|
||||
using GreenDonut;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
public class SubjectByIdDataLoader : BatchDataLoader<int, Subject>
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _contextFactory;
|
||||
|
||||
public SubjectByIdDataLoader(
|
||||
IDbContextFactory<AppDbContext> contextFactory,
|
||||
IBatchScheduler batchScheduler,
|
||||
DataLoaderOptions? options = null)
|
||||
: base(batchScheduler, options ?? new DataLoaderOptions())
|
||||
{
|
||||
_contextFactory = contextFactory;
|
||||
}
|
||||
|
||||
protected override async Task<IReadOnlyDictionary<int, Subject>> LoadBatchAsync(
|
||||
IReadOnlyList<int> keys,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await using var context = await _contextFactory.CreateDbContextAsync(ct);
|
||||
return await context.Subjects
|
||||
.Include(s => s.Professor)
|
||||
.Where(s => keys.Contains(s.Id))
|
||||
.ToDictionaryAsync(s => s.Id, ct);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
namespace Adapters.Driven.Persistence;
|
||||
|
||||
using Adapters.Driven.Persistence.Context;
|
||||
using Adapters.Driven.Persistence.Repositories;
|
||||
using Domain.Ports.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddPersistence(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
services.AddDbContext<AppDbContext>(options =>
|
||||
options.UseSqlServer(
|
||||
configuration.GetConnectionString("DefaultConnection"),
|
||||
b => b.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName)));
|
||||
|
||||
services.AddScoped<IStudentRepository, StudentRepository>();
|
||||
services.AddScoped<ISubjectRepository, SubjectRepository>();
|
||||
services.AddScoped<IProfessorRepository, ProfessorRepository>();
|
||||
services.AddScoped<IEnrollmentRepository, EnrollmentRepository>();
|
||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
174
src/backend/Adapters/Driven/Persistence/Migrations/20260107212215_InitialCreate.Designer.cs
generated
Normal file
174
src/backend/Adapters/Driven/Persistence/Migrations/20260107212215_InitialCreate.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Adapters.Driven.Persistence.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Adapters.Driven.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260107212215_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Enrollment", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("EnrolledAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("StudentId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SubjectId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SubjectId");
|
||||
|
||||
b.HasIndex("StudentId", "SubjectId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Enrollments", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Professor", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Professors", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Student", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("nvarchar(150)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Email")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Students", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Subject", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("Credits")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(3);
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("ProfessorId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProfessorId");
|
||||
|
||||
b.ToTable("Subjects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Enrollment", b =>
|
||||
{
|
||||
b.HasOne("Domain.Entities.Student", "Student")
|
||||
.WithMany("Enrollments")
|
||||
.HasForeignKey("StudentId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Domain.Entities.Subject", "Subject")
|
||||
.WithMany("Enrollments")
|
||||
.HasForeignKey("SubjectId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Student");
|
||||
|
||||
b.Navigation("Subject");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Subject", b =>
|
||||
{
|
||||
b.HasOne("Domain.Entities.Professor", "Professor")
|
||||
.WithMany("Subjects")
|
||||
.HasForeignKey("ProfessorId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Professor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Professor", b =>
|
||||
{
|
||||
b.Navigation("Subjects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Student", b =>
|
||||
{
|
||||
b.Navigation("Enrollments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Subject", b =>
|
||||
{
|
||||
b.Navigation("Enrollments");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Adapters.Driven.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Professors",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
Name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Professors", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Students",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
Name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
Email = table.Column<string>(type: "nvarchar(150)", maxLength: 150, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Students", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Subjects",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
Name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
Credits = table.Column<int>(type: "int", nullable: false, defaultValue: 3),
|
||||
ProfessorId = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Subjects", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Subjects_Professors_ProfessorId",
|
||||
column: x => x.ProfessorId,
|
||||
principalTable: "Professors",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Enrollments",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
StudentId = table.Column<int>(type: "int", nullable: false),
|
||||
SubjectId = table.Column<int>(type: "int", nullable: false),
|
||||
EnrolledAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Enrollments", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Enrollments_Students_StudentId",
|
||||
column: x => x.StudentId,
|
||||
principalTable: "Students",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Enrollments_Subjects_SubjectId",
|
||||
column: x => x.SubjectId,
|
||||
principalTable: "Subjects",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Enrollments_StudentId_SubjectId",
|
||||
table: "Enrollments",
|
||||
columns: new[] { "StudentId", "SubjectId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Enrollments_SubjectId",
|
||||
table: "Enrollments",
|
||||
column: "SubjectId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Students_Email",
|
||||
table: "Students",
|
||||
column: "Email",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Subjects_ProfessorId",
|
||||
table: "Subjects",
|
||||
column: "ProfessorId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Enrollments");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Students");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Subjects");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Professors");
|
||||
}
|
||||
}
|
||||
}
|
||||
273
src/backend/Adapters/Driven/Persistence/Migrations/20260107212421_SeedData.Designer.cs
generated
Normal file
273
src/backend/Adapters/Driven/Persistence/Migrations/20260107212421_SeedData.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Adapters.Driven.Persistence.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Adapters.Driven.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260107212421_SeedData")]
|
||||
partial class SeedData
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Enrollment", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("EnrolledAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("StudentId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SubjectId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SubjectId");
|
||||
|
||||
b.HasIndex("StudentId", "SubjectId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Enrollments", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Professor", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Professors", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Dr. García"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
Name = "Dra. Martínez"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
Name = "Dr. López"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4,
|
||||
Name = "Dra. Rodríguez"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 5,
|
||||
Name = "Dr. Hernández"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Student", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("nvarchar(150)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Email")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Students", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Subject", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("Credits")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(3);
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("ProfessorId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProfessorId");
|
||||
|
||||
b.ToTable("Subjects", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
Credits = 3,
|
||||
Name = "Matemáticas I",
|
||||
ProfessorId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
Credits = 3,
|
||||
Name = "Matemáticas II",
|
||||
ProfessorId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
Credits = 3,
|
||||
Name = "Física I",
|
||||
ProfessorId = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4,
|
||||
Credits = 3,
|
||||
Name = "Física II",
|
||||
ProfessorId = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 5,
|
||||
Credits = 3,
|
||||
Name = "Programación I",
|
||||
ProfessorId = 3
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 6,
|
||||
Credits = 3,
|
||||
Name = "Programación II",
|
||||
ProfessorId = 3
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 7,
|
||||
Credits = 3,
|
||||
Name = "Base de Datos I",
|
||||
ProfessorId = 4
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 8,
|
||||
Credits = 3,
|
||||
Name = "Base de Datos II",
|
||||
ProfessorId = 4
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 9,
|
||||
Credits = 3,
|
||||
Name = "Redes I",
|
||||
ProfessorId = 5
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 10,
|
||||
Credits = 3,
|
||||
Name = "Redes II",
|
||||
ProfessorId = 5
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Enrollment", b =>
|
||||
{
|
||||
b.HasOne("Domain.Entities.Student", "Student")
|
||||
.WithMany("Enrollments")
|
||||
.HasForeignKey("StudentId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Domain.Entities.Subject", "Subject")
|
||||
.WithMany("Enrollments")
|
||||
.HasForeignKey("SubjectId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Student");
|
||||
|
||||
b.Navigation("Subject");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Subject", b =>
|
||||
{
|
||||
b.HasOne("Domain.Entities.Professor", "Professor")
|
||||
.WithMany("Subjects")
|
||||
.HasForeignKey("ProfessorId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Professor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Professor", b =>
|
||||
{
|
||||
b.Navigation("Subjects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Student", b =>
|
||||
{
|
||||
b.Navigation("Enrollments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Subject", b =>
|
||||
{
|
||||
b.Navigation("Enrollments");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||
|
||||
namespace Adapters.Driven.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SeedData : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.InsertData(
|
||||
table: "Professors",
|
||||
columns: new[] { "Id", "Name" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 1, "Dr. García" },
|
||||
{ 2, "Dra. Martínez" },
|
||||
{ 3, "Dr. López" },
|
||||
{ 4, "Dra. Rodríguez" },
|
||||
{ 5, "Dr. Hernández" }
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "Subjects",
|
||||
columns: new[] { "Id", "Credits", "Name", "ProfessorId" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 1, 3, "Matemáticas I", 1 },
|
||||
{ 2, 3, "Matemáticas II", 1 },
|
||||
{ 3, 3, "Física I", 2 },
|
||||
{ 4, 3, "Física II", 2 },
|
||||
{ 5, 3, "Programación I", 3 },
|
||||
{ 6, 3, "Programación II", 3 },
|
||||
{ 7, 3, "Base de Datos I", 4 },
|
||||
{ 8, 3, "Base de Datos II", 4 },
|
||||
{ 9, 3, "Redes I", 5 },
|
||||
{ 10, 3, "Redes II", 5 }
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DeleteData(
|
||||
table: "Subjects",
|
||||
keyColumn: "Id",
|
||||
keyValue: 1);
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "Subjects",
|
||||
keyColumn: "Id",
|
||||
keyValue: 2);
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "Subjects",
|
||||
keyColumn: "Id",
|
||||
keyValue: 3);
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "Subjects",
|
||||
keyColumn: "Id",
|
||||
keyValue: 4);
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "Subjects",
|
||||
keyColumn: "Id",
|
||||
keyValue: 5);
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "Subjects",
|
||||
keyColumn: "Id",
|
||||
keyValue: 6);
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "Subjects",
|
||||
keyColumn: "Id",
|
||||
keyValue: 7);
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "Subjects",
|
||||
keyColumn: "Id",
|
||||
keyValue: 8);
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "Subjects",
|
||||
keyColumn: "Id",
|
||||
keyValue: 9);
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "Subjects",
|
||||
keyColumn: "Id",
|
||||
keyValue: 10);
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "Professors",
|
||||
keyColumn: "Id",
|
||||
keyValue: 1);
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "Professors",
|
||||
keyColumn: "Id",
|
||||
keyValue: 2);
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "Professors",
|
||||
keyColumn: "Id",
|
||||
keyValue: 3);
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "Professors",
|
||||
keyColumn: "Id",
|
||||
keyValue: 4);
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "Professors",
|
||||
keyColumn: "Id",
|
||||
keyValue: 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,270 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Adapters.Driven.Persistence.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Adapters.Driven.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
partial class AppDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Enrollment", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("EnrolledAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("StudentId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SubjectId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SubjectId");
|
||||
|
||||
b.HasIndex("StudentId", "SubjectId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Enrollments", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Professor", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Professors", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Dr. García"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
Name = "Dra. Martínez"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
Name = "Dr. López"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4,
|
||||
Name = "Dra. Rodríguez"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 5,
|
||||
Name = "Dr. Hernández"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Student", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("nvarchar(150)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Email")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Students", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Subject", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("Credits")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(3);
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("ProfessorId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProfessorId");
|
||||
|
||||
b.ToTable("Subjects", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
Credits = 3,
|
||||
Name = "Matemáticas I",
|
||||
ProfessorId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
Credits = 3,
|
||||
Name = "Matemáticas II",
|
||||
ProfessorId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
Credits = 3,
|
||||
Name = "Física I",
|
||||
ProfessorId = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4,
|
||||
Credits = 3,
|
||||
Name = "Física II",
|
||||
ProfessorId = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 5,
|
||||
Credits = 3,
|
||||
Name = "Programación I",
|
||||
ProfessorId = 3
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 6,
|
||||
Credits = 3,
|
||||
Name = "Programación II",
|
||||
ProfessorId = 3
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 7,
|
||||
Credits = 3,
|
||||
Name = "Base de Datos I",
|
||||
ProfessorId = 4
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 8,
|
||||
Credits = 3,
|
||||
Name = "Base de Datos II",
|
||||
ProfessorId = 4
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 9,
|
||||
Credits = 3,
|
||||
Name = "Redes I",
|
||||
ProfessorId = 5
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 10,
|
||||
Credits = 3,
|
||||
Name = "Redes II",
|
||||
ProfessorId = 5
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Enrollment", b =>
|
||||
{
|
||||
b.HasOne("Domain.Entities.Student", "Student")
|
||||
.WithMany("Enrollments")
|
||||
.HasForeignKey("StudentId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Domain.Entities.Subject", "Subject")
|
||||
.WithMany("Enrollments")
|
||||
.HasForeignKey("SubjectId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Student");
|
||||
|
||||
b.Navigation("Subject");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Subject", b =>
|
||||
{
|
||||
b.HasOne("Domain.Entities.Professor", "Professor")
|
||||
.WithMany("Subjects")
|
||||
.HasForeignKey("ProfessorId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Professor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Professor", b =>
|
||||
{
|
||||
b.Navigation("Subjects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Student", b =>
|
||||
{
|
||||
b.Navigation("Enrollments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Domain.Entities.Subject", b =>
|
||||
{
|
||||
b.Navigation("Enrollments");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
namespace Adapters.Driven.Persistence.Repositories;
|
||||
|
||||
using Adapters.Driven.Persistence.Context;
|
||||
using Domain.Entities;
|
||||
using Domain.Ports.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
public class EnrollmentRepository(AppDbContext context) : IEnrollmentRepository
|
||||
{
|
||||
public async Task<Enrollment?> GetByIdAsync(int id, CancellationToken ct = default) =>
|
||||
await context.Enrollments.FindAsync([id], ct);
|
||||
|
||||
public async Task<Enrollment?> GetByStudentAndSubjectAsync(int studentId, int subjectId, CancellationToken ct = default) =>
|
||||
await context.Enrollments
|
||||
.FirstOrDefaultAsync(e => e.StudentId == studentId && e.SubjectId == subjectId, ct);
|
||||
|
||||
public async Task<IReadOnlyList<Enrollment>> GetByStudentIdAsync(int studentId, CancellationToken ct = default) =>
|
||||
await context.Enrollments
|
||||
.Include(e => e.Subject)
|
||||
.ThenInclude(s => s.Professor)
|
||||
.Where(e => e.StudentId == studentId)
|
||||
.AsNoTracking()
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Enrollment>> GetBySubjectIdAsync(int subjectId, CancellationToken ct = default) =>
|
||||
await context.Enrollments
|
||||
.Include(e => e.Student)
|
||||
.Where(e => e.SubjectId == subjectId)
|
||||
.AsNoTracking()
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Student>> GetClassmatesAsync(int studentId, int subjectId, CancellationToken ct = default) =>
|
||||
await context.Enrollments
|
||||
.Where(e => e.SubjectId == subjectId && e.StudentId != studentId)
|
||||
.Select(e => e.Student)
|
||||
.AsNoTracking()
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyDictionary<int, IReadOnlyList<Student>>> GetClassmatesBatchAsync(
|
||||
int studentId, IEnumerable<int> subjectIds, CancellationToken ct = default)
|
||||
{
|
||||
var subjectIdList = subjectIds.ToList();
|
||||
if (subjectIdList.Count == 0)
|
||||
return new Dictionary<int, IReadOnlyList<Student>>();
|
||||
|
||||
// Single query to get all classmates for all subjects
|
||||
var enrollments = await context.Enrollments
|
||||
.Where(e => subjectIdList.Contains(e.SubjectId) && e.StudentId != studentId)
|
||||
.Select(e => new { e.SubjectId, e.Student.Id, e.Student.Name })
|
||||
.AsNoTracking()
|
||||
.ToListAsync(ct);
|
||||
|
||||
// Group by SubjectId and project to Student (minimal data needed)
|
||||
return enrollments
|
||||
.GroupBy(e => e.SubjectId)
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
g => (IReadOnlyList<Student>)g
|
||||
.Select(e => CreateMinimalStudent(e.Id, e.Name))
|
||||
.ToList());
|
||||
}
|
||||
|
||||
private static Student CreateMinimalStudent(int id, string name)
|
||||
{
|
||||
// Use reflection to set Id since it's private set
|
||||
var student = (Student)System.Runtime.CompilerServices.RuntimeHelpers
|
||||
.GetUninitializedObject(typeof(Student));
|
||||
typeof(Student).GetProperty("Id")!.SetValue(student, id);
|
||||
typeof(Student).GetProperty("Name")!.SetValue(student, name);
|
||||
return student;
|
||||
}
|
||||
|
||||
public void Add(Enrollment enrollment) => context.Enrollments.Add(enrollment);
|
||||
public void Delete(Enrollment enrollment) => context.Enrollments.Remove(enrollment);
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
namespace Adapters.Driven.Persistence.Repositories;
|
||||
|
||||
using Adapters.Driven.Persistence.Context;
|
||||
using Domain.Entities;
|
||||
using Domain.Ports.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
public class ProfessorRepository(AppDbContext context) : IProfessorRepository
|
||||
{
|
||||
public async Task<Professor?> GetByIdAsync(int id, CancellationToken ct = default) =>
|
||||
await context.Professors.FindAsync([id], ct);
|
||||
|
||||
public async Task<IReadOnlyList<Professor>> GetAllAsync(CancellationToken ct = default) =>
|
||||
await context.Professors.AsNoTracking().ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Professor>> GetAllWithSubjectsAsync(CancellationToken ct = default) =>
|
||||
await context.Professors
|
||||
.Include(p => p.Subjects)
|
||||
.AsNoTracking()
|
||||
.ToListAsync(ct);
|
||||
|
||||
// Direct projection - only SELECT needed columns
|
||||
public async Task<IReadOnlyList<TResult>> GetAllProjectedAsync<TResult>(
|
||||
Expression<Func<Professor, TResult>> selector,
|
||||
CancellationToken ct = default) =>
|
||||
await context.Professors
|
||||
.AsNoTracking()
|
||||
.Select(selector)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
namespace Adapters.Driven.Persistence.Repositories;
|
||||
|
||||
using Adapters.Driven.Persistence.Context;
|
||||
using Domain.Entities;
|
||||
using Domain.Ports.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
public class StudentRepository(AppDbContext context) : IStudentRepository
|
||||
{
|
||||
public async Task<Student?> GetByIdAsync(int id, CancellationToken ct = default) =>
|
||||
await context.Students.FindAsync([id], ct);
|
||||
|
||||
public async Task<Student?> GetByIdWithEnrollmentsAsync(int id, CancellationToken ct = default) =>
|
||||
await context.Students
|
||||
.Include(s => s.Enrollments)
|
||||
.ThenInclude(e => e.Subject)
|
||||
.ThenInclude(sub => sub.Professor)
|
||||
.AsSplitQuery() // Avoid cartesian explosion
|
||||
.FirstOrDefaultAsync(s => s.Id == id, ct);
|
||||
|
||||
public async Task<IReadOnlyList<Student>> GetAllAsync(CancellationToken ct = default) =>
|
||||
await context.Students.AsNoTracking().ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Student>> GetAllWithEnrollmentsAsync(CancellationToken ct = default) =>
|
||||
await context.Students
|
||||
.Include(s => s.Enrollments)
|
||||
.ThenInclude(e => e.Subject)
|
||||
.ThenInclude(sub => sub.Professor)
|
||||
.AsSplitQuery() // Avoid cartesian explosion
|
||||
.AsNoTrackingWithIdentityResolution() // Better than AsNoTracking with navigations
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task<bool> ExistsAsync(int id, CancellationToken ct = default) =>
|
||||
await CompiledQueries.StudentExists(context, id);
|
||||
|
||||
public async Task<bool> EmailExistsAsync(string email, int? excludeId = null, CancellationToken ct = default)
|
||||
{
|
||||
var normalizedEmail = email.Trim().ToLowerInvariant();
|
||||
|
||||
// Use raw SQL to avoid value object translation issues
|
||||
// SQL Server requires column alias for SqlQueryRaw
|
||||
var sql = excludeId.HasValue
|
||||
? "SELECT 1 AS Value FROM Students WHERE LOWER(Email) = {0} AND Id != {1}"
|
||||
: "SELECT 1 AS Value FROM Students WHERE LOWER(Email) = {0}";
|
||||
|
||||
var parameters = excludeId.HasValue
|
||||
? new object[] { normalizedEmail, excludeId.Value }
|
||||
: new object[] { normalizedEmail };
|
||||
|
||||
return await context.Database
|
||||
.SqlQueryRaw<int>(sql, parameters)
|
||||
.AnyAsync(ct);
|
||||
}
|
||||
|
||||
// Direct projection - query only needed columns
|
||||
public async Task<IReadOnlyList<TResult>> GetAllProjectedAsync<TResult>(
|
||||
Expression<Func<Student, TResult>> selector,
|
||||
CancellationToken ct = default) =>
|
||||
await context.Students
|
||||
.AsNoTracking()
|
||||
.Select(selector)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task<TResult?> GetByIdProjectedAsync<TResult>(
|
||||
int id,
|
||||
Expression<Func<Student, TResult>> selector,
|
||||
CancellationToken ct = default) =>
|
||||
await context.Students
|
||||
.Where(s => s.Id == id)
|
||||
.AsNoTracking()
|
||||
.Select(selector)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
// Keyset pagination - more efficient than OFFSET for large datasets
|
||||
public async Task<(IReadOnlyList<TResult> Items, int? NextCursor, int TotalCount)> GetPagedProjectedAsync<TResult>(
|
||||
Expression<Func<Student, TResult>> selector,
|
||||
int? afterId = null,
|
||||
int pageSize = 10,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var query = context.Students.AsNoTracking();
|
||||
|
||||
// Keyset: WHERE Id > afterId (uses index efficiently)
|
||||
if (afterId.HasValue)
|
||||
query = query.Where(s => s.Id > afterId.Value);
|
||||
|
||||
// Get one extra to determine if there's a next page
|
||||
var items = await query
|
||||
.OrderBy(s => s.Id)
|
||||
.Take(pageSize + 1)
|
||||
.Select(selector)
|
||||
.ToListAsync(ct);
|
||||
|
||||
var hasMore = items.Count > pageSize;
|
||||
var resultItems = hasMore ? items.Take(pageSize).ToList() : items;
|
||||
|
||||
// Get next cursor from last item (requires Id in projection or separate query)
|
||||
int? nextCursor = null;
|
||||
if (hasMore && resultItems.Count > 0)
|
||||
{
|
||||
var lastId = await context.Students
|
||||
.AsNoTracking()
|
||||
.Where(s => afterId.HasValue ? s.Id > afterId.Value : true)
|
||||
.OrderBy(s => s.Id)
|
||||
.Skip(pageSize - 1)
|
||||
.Select(s => s.Id)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
nextCursor = lastId > 0 ? lastId : null;
|
||||
}
|
||||
|
||||
// Use compiled query for count
|
||||
var totalCount = await CompiledQueries.CountStudents(context);
|
||||
|
||||
return (resultItems, nextCursor, totalCount);
|
||||
}
|
||||
|
||||
public void Add(Student student) => context.Students.Add(student);
|
||||
public void Update(Student student) => context.Students.Update(student);
|
||||
public void Delete(Student student) => context.Students.Remove(student);
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
namespace Adapters.Driven.Persistence.Repositories;
|
||||
|
||||
using Adapters.Driven.Persistence.Context;
|
||||
using Domain.Entities;
|
||||
using Domain.Ports.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
public class SubjectRepository(AppDbContext context) : ISubjectRepository
|
||||
{
|
||||
public async Task<Subject?> GetByIdAsync(int id, CancellationToken ct = default) =>
|
||||
await context.Subjects.FindAsync([id], ct);
|
||||
|
||||
public async Task<Subject?> GetByIdWithProfessorAsync(int id, CancellationToken ct = default) =>
|
||||
await context.Subjects
|
||||
.Include(s => s.Professor)
|
||||
.FirstOrDefaultAsync(s => s.Id == id, ct);
|
||||
|
||||
public async Task<IReadOnlyList<Subject>> GetAllAsync(CancellationToken ct = default) =>
|
||||
await context.Subjects.AsNoTracking().ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Subject>> GetAllWithProfessorsAsync(CancellationToken ct = default) =>
|
||||
await context.Subjects
|
||||
.Include(s => s.Professor)
|
||||
.AsNoTracking()
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Subject>> GetAvailableForStudentAsync(int studentId, CancellationToken ct = default)
|
||||
{
|
||||
// Optimized: single query instead of two separate queries
|
||||
var enrollmentData = await context.Enrollments
|
||||
.Where(e => e.StudentId == studentId)
|
||||
.Select(e => new { e.SubjectId, e.Subject.ProfessorId })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var enrolledSubjectIds = enrollmentData.Select(e => e.SubjectId).ToHashSet();
|
||||
var enrolledProfessorIds = enrollmentData.Select(e => e.ProfessorId).ToHashSet();
|
||||
|
||||
return await context.Subjects
|
||||
.Include(s => s.Professor)
|
||||
.Where(s => !enrolledSubjectIds.Contains(s.Id) && !enrolledProfessorIds.Contains(s.ProfessorId))
|
||||
.AsNoTracking()
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
// Direct projection - only SELECT needed columns
|
||||
public async Task<IReadOnlyList<TResult>> GetAllProjectedAsync<TResult>(
|
||||
Expression<Func<Subject, TResult>> selector,
|
||||
CancellationToken ct = default) =>
|
||||
await context.Subjects
|
||||
.AsNoTracking()
|
||||
.Select(selector)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
namespace Adapters.Driven.Persistence.Repositories;
|
||||
|
||||
using Adapters.Driven.Persistence.Context;
|
||||
using Domain.Ports.Repositories;
|
||||
|
||||
public class UnitOfWork(AppDbContext context) : IUnitOfWork
|
||||
{
|
||||
public async Task<int> SaveChangesAsync(CancellationToken ct = default) =>
|
||||
await context.SaveChangesAsync(ct);
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
namespace Adapters.Driven.Persistence.Seeding;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
public static class DataSeeder
|
||||
{
|
||||
public static void Seed(ModelBuilder modelBuilder)
|
||||
{
|
||||
// 5 Profesores
|
||||
modelBuilder.Entity<Domain.Entities.Professor>().HasData(
|
||||
new { Id = 1, Name = "Dr. García" },
|
||||
new { Id = 2, Name = "Dra. Martínez" },
|
||||
new { Id = 3, Name = "Dr. López" },
|
||||
new { Id = 4, Name = "Dra. Rodríguez" },
|
||||
new { Id = 5, Name = "Dr. Hernández" }
|
||||
);
|
||||
|
||||
// 10 Materias (2 por profesor, 3 créditos cada una)
|
||||
modelBuilder.Entity<Domain.Entities.Subject>().HasData(
|
||||
new { Id = 1, Name = "Matemáticas I", Credits = 3, ProfessorId = 1 },
|
||||
new { Id = 2, Name = "Matemáticas II", Credits = 3, ProfessorId = 1 },
|
||||
new { Id = 3, Name = "Física I", Credits = 3, ProfessorId = 2 },
|
||||
new { Id = 4, Name = "Física II", Credits = 3, ProfessorId = 2 },
|
||||
new { Id = 5, Name = "Programación I", Credits = 3, ProfessorId = 3 },
|
||||
new { Id = 6, Name = "Programación II", Credits = 3, ProfessorId = 3 },
|
||||
new { Id = 7, Name = "Base de Datos I", Credits = 3, ProfessorId = 4 },
|
||||
new { Id = 8, Name = "Base de Datos II", Credits = 3, ProfessorId = 4 },
|
||||
new { Id = 9, Name = "Redes I", Credits = 3, ProfessorId = 5 },
|
||||
new { Id = 10, Name = "Redes II", Credits = 3, ProfessorId = 5 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Application\Application.csproj" />
|
||||
<ProjectReference Include="..\..\Driven\Persistence\Adapters.Driven.Persistence.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HotChocolate.AspNetCore" Version="*" />
|
||||
<PackageReference Include="HotChocolate.Data" Version="*" />
|
||||
<PackageReference Include="HotChocolate.Data.EntityFramework" Version="*" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
namespace Adapters.Driving.Api.Extensions;
|
||||
|
||||
using Adapters.Driven.Persistence.DataLoaders;
|
||||
using Adapters.Driving.Api.Middleware;
|
||||
using Adapters.Driving.Api.Types;
|
||||
using HotChocolate.Execution.Options;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class GraphQLExtensions
|
||||
{
|
||||
private const int MaxQueryDepth = 5;
|
||||
private const int MaxQueryComplexity = 100;
|
||||
private const int MaxQueryNodes = 50;
|
||||
|
||||
public static IServiceCollection AddGraphQLApi(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddGraphQLServer()
|
||||
.AddQueryType<Query>()
|
||||
.AddMutationType<Mutation>()
|
||||
.AddProjections()
|
||||
.AddFiltering()
|
||||
.AddSorting()
|
||||
.AddErrorFilter<GraphQLErrorFilter>()
|
||||
.AddDataLoader<StudentByIdDataLoader>()
|
||||
.AddDataLoader<SubjectByIdDataLoader>()
|
||||
.AddDataLoader<ProfessorByIdDataLoader>()
|
||||
// Security: Query depth, complexity, and execution limits
|
||||
.ModifyRequestOptions(ConfigureRequestOptions)
|
||||
.AddMaxExecutionDepthRule(MaxQueryDepth)
|
||||
// Cost analysis for query complexity
|
||||
.ModifyCostOptions(opt =>
|
||||
{
|
||||
opt.MaxFieldCost = MaxQueryComplexity;
|
||||
opt.MaxTypeCost = MaxQueryComplexity;
|
||||
opt.EnforceCostLimits = true;
|
||||
})
|
||||
// Pagination limits to prevent large result sets
|
||||
.ModifyPagingOptions(opt =>
|
||||
{
|
||||
opt.MaxPageSize = MaxQueryNodes;
|
||||
opt.DefaultPageSize = 20;
|
||||
opt.IncludeTotalCount = true;
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static void ConfigureRequestOptions(RequestExecutorOptions opt)
|
||||
{
|
||||
opt.IncludeExceptionDetails = false; // Don't expose internal errors in production
|
||||
opt.ExecutionTimeout = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
namespace Adapters.Driving.Api.Middleware;
|
||||
|
||||
using Domain.Exceptions;
|
||||
using FluentValidation;
|
||||
|
||||
public class GraphQLErrorFilter : IErrorFilter
|
||||
{
|
||||
public IError OnError(IError error)
|
||||
{
|
||||
return error.Exception switch
|
||||
{
|
||||
DomainException domainException => error
|
||||
.WithMessage(domainException.Message)
|
||||
.WithCode(domainException.Code),
|
||||
|
||||
ValidationException validationException => error
|
||||
.WithMessage("Validation failed")
|
||||
.WithCode("VALIDATION_ERROR")
|
||||
.SetExtension("errors", validationException.Errors
|
||||
.Select(e => new { e.PropertyName, e.ErrorMessage })),
|
||||
|
||||
ArgumentException argException => error
|
||||
.WithMessage(argException.Message)
|
||||
.WithCode("INVALID_ARGUMENT"),
|
||||
|
||||
_ => error.WithMessage(error.Exception?.Message ?? error.Message)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
namespace Adapters.Driving.Api.Types.Enrollments;
|
||||
|
||||
using Adapters.Driven.Persistence.DataLoaders;
|
||||
using Domain.Entities;
|
||||
|
||||
[ObjectType<Enrollment>]
|
||||
public static partial class EnrollmentType
|
||||
{
|
||||
[GraphQLDescription("Student enrolled")]
|
||||
public static async Task<Student?> GetStudent(
|
||||
[Parent] Enrollment enrollment,
|
||||
StudentByIdDataLoader dataLoader,
|
||||
CancellationToken ct) =>
|
||||
await dataLoader.LoadAsync(enrollment.StudentId, ct);
|
||||
|
||||
[GraphQLDescription("Subject enrolled in")]
|
||||
public static async Task<Subject?> GetSubject(
|
||||
[Parent] Enrollment enrollment,
|
||||
SubjectByIdDataLoader dataLoader,
|
||||
CancellationToken ct) =>
|
||||
await dataLoader.LoadAsync(enrollment.SubjectId, ct);
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
namespace Adapters.Driving.Api.Types;
|
||||
|
||||
using Application.Enrollments.Commands;
|
||||
using Application.Students.Commands;
|
||||
using Application.Students.DTOs;
|
||||
using MediatR;
|
||||
|
||||
public class Mutation
|
||||
{
|
||||
[GraphQLDescription("Create a new student")]
|
||||
public async Task<CreateStudentPayload> CreateStudent(
|
||||
CreateStudentInput input,
|
||||
[Service] IMediator mediator,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new CreateStudentCommand(input.Name, input.Email), ct);
|
||||
return new CreateStudentPayload(result);
|
||||
}
|
||||
|
||||
[GraphQLDescription("Update an existing student")]
|
||||
public async Task<UpdateStudentPayload> UpdateStudent(
|
||||
int id,
|
||||
UpdateStudentInput input,
|
||||
[Service] IMediator mediator,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new UpdateStudentCommand(id, input.Name, input.Email), ct);
|
||||
return new UpdateStudentPayload(result);
|
||||
}
|
||||
|
||||
[GraphQLDescription("Delete a student")]
|
||||
public async Task<DeleteStudentPayload> DeleteStudent(
|
||||
int id,
|
||||
[Service] IMediator mediator,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var success = await mediator.Send(new DeleteStudentCommand(id), ct);
|
||||
return new DeleteStudentPayload(success);
|
||||
}
|
||||
|
||||
[GraphQLDescription("Enroll a student in a subject")]
|
||||
public async Task<EnrollStudentPayload> EnrollStudent(
|
||||
EnrollStudentInput input,
|
||||
[Service] IMediator mediator,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(new EnrollStudentCommand(input.StudentId, input.SubjectId), ct);
|
||||
return new EnrollStudentPayload(result);
|
||||
}
|
||||
|
||||
[GraphQLDescription("Unenroll a student from a subject")]
|
||||
public async Task<UnenrollStudentPayload> UnenrollStudent(
|
||||
int enrollmentId,
|
||||
[Service] IMediator mediator,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var success = await mediator.Send(new UnenrollStudentCommand(enrollmentId), ct);
|
||||
return new UnenrollStudentPayload(success);
|
||||
}
|
||||
}
|
||||
|
||||
// Inputs
|
||||
public record CreateStudentInput(string Name, string Email);
|
||||
public record UpdateStudentInput(string Name, string Email);
|
||||
public record EnrollStudentInput(int StudentId, int SubjectId);
|
||||
|
||||
// Payloads
|
||||
public record CreateStudentPayload(StudentDto Student);
|
||||
public record UpdateStudentPayload(StudentDto Student);
|
||||
public record DeleteStudentPayload(bool Success);
|
||||
public record EnrollStudentPayload(EnrollmentDto Enrollment);
|
||||
public record UnenrollStudentPayload(bool Success);
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
namespace Adapters.Driving.Api.Types.Professors;
|
||||
|
||||
using Domain.Entities;
|
||||
|
||||
[ObjectType<Professor>]
|
||||
public static partial class ProfessorType
|
||||
{
|
||||
[GraphQLDescription("Subjects taught by this professor")]
|
||||
public static IEnumerable<Subject> GetSubjects([Parent] Professor professor) =>
|
||||
professor.Subjects;
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
namespace Adapters.Driving.Api.Types;
|
||||
|
||||
using Application.Enrollments.DTOs;
|
||||
using Application.Enrollments.Queries;
|
||||
using Application.Professors.DTOs;
|
||||
using Application.Professors.Queries;
|
||||
using Application.Students.DTOs;
|
||||
using Application.Students.Queries;
|
||||
using Application.Subjects.DTOs;
|
||||
using Application.Subjects.Queries;
|
||||
using MediatR;
|
||||
|
||||
public class Query
|
||||
{
|
||||
[GraphQLDescription("Get all students")]
|
||||
public async Task<IReadOnlyList<StudentDto>> GetStudents(
|
||||
[Service] IMediator mediator,
|
||||
CancellationToken ct) =>
|
||||
await mediator.Send(new GetStudentsQuery(), ct);
|
||||
|
||||
[GraphQLDescription("Get students with cursor-based pagination (keyset)")]
|
||||
public async Task<PagedResult<StudentPagedDto>> GetStudentsPaged(
|
||||
int? afterCursor,
|
||||
int pageSize,
|
||||
[Service] IMediator mediator,
|
||||
CancellationToken ct) =>
|
||||
await mediator.Send(new GetStudentsPagedQuery(afterCursor, pageSize), ct);
|
||||
|
||||
[GraphQLDescription("Get student by ID")]
|
||||
public async Task<StudentDto> GetStudent(
|
||||
int id,
|
||||
[Service] IMediator mediator,
|
||||
CancellationToken ct) =>
|
||||
await mediator.Send(new GetStudentByIdQuery(id), ct);
|
||||
|
||||
[GraphQLDescription("Get all subjects")]
|
||||
public async Task<IReadOnlyList<SubjectDto>> GetSubjects(
|
||||
[Service] IMediator mediator,
|
||||
CancellationToken ct) =>
|
||||
await mediator.Send(new GetSubjectsQuery(), ct);
|
||||
|
||||
[GraphQLDescription("Get available subjects for a student")]
|
||||
public async Task<IReadOnlyList<AvailableSubjectDto>> GetAvailableSubjects(
|
||||
int studentId,
|
||||
[Service] IMediator mediator,
|
||||
CancellationToken ct) =>
|
||||
await mediator.Send(new GetAvailableSubjectsQuery(studentId), ct);
|
||||
|
||||
[GraphQLDescription("Get all professors")]
|
||||
public async Task<IReadOnlyList<ProfessorDto>> GetProfessors(
|
||||
[Service] IMediator mediator,
|
||||
CancellationToken ct) =>
|
||||
await mediator.Send(new GetProfessorsQuery(), ct);
|
||||
|
||||
[GraphQLDescription("Get classmates for a student by subject")]
|
||||
public async Task<IReadOnlyList<ClassmatesBySubjectDto>> GetClassmates(
|
||||
int studentId,
|
||||
[Service] IMediator mediator,
|
||||
CancellationToken ct) =>
|
||||
await mediator.Send(new GetClassmatesQuery(studentId), ct);
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
namespace Adapters.Driving.Api.Types.Students;
|
||||
|
||||
using Domain.Entities;
|
||||
|
||||
[ObjectType<Student>]
|
||||
public static partial class StudentType
|
||||
{
|
||||
public static string GetEmail([Parent] Student student) => student.Email.Value;
|
||||
|
||||
[GraphQLDescription("Total credits enrolled")]
|
||||
public static int GetTotalCredits([Parent] Student student) => student.TotalCredits;
|
||||
|
||||
[GraphQLDescription("List of enrollments")]
|
||||
public static IEnumerable<Enrollment> GetEnrollments([Parent] Student student) =>
|
||||
student.Enrollments;
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
namespace Adapters.Driving.Api.Types.Subjects;
|
||||
|
||||
using Adapters.Driven.Persistence.DataLoaders;
|
||||
using Domain.Entities;
|
||||
|
||||
[ObjectType<Subject>]
|
||||
public static partial class SubjectType
|
||||
{
|
||||
[GraphQLDescription("Professor teaching this subject")]
|
||||
public static async Task<Professor?> GetProfessor(
|
||||
[Parent] Subject subject,
|
||||
ProfessorByIdDataLoader dataLoader,
|
||||
CancellationToken ct) =>
|
||||
await dataLoader.LoadAsync(subject.ProfessorId, ct);
|
||||
}
|
||||
Loading…
Reference in New Issue