Files
GerbilManager/GerbilManagerWebAPI/Endpoints/LitterEndpoints.cs
Gulum 180d53b203 DATA-2: new schema entities + Minimal API endpoints + Scalar + DAL teardown
- Entities: Breeder->Contact, +Enclosure/ColorVariety/GerbilPhoto/HealthRecord/WeightRecord;
  Gerbil expanded (single Genotype text col, Status/Gender enums-as-string, FK ids,
  ImportSource/ExternalRef provenance); Litter Strength->TotalBorn +ExpectedGoHomeDate/Notes.
  ColorVariety HasData seed = 18 from GEN-1 catalog. Gerbil<->Litter cycle handled
  (SetNull/Restrict). Enums stored as strings.
- Minimal API (no controllers): Endpoints/*.cs MapGroup+TypedResults for gerbils, litters,
  contacts, enclosures, color-varieties, health/weight-records, inbreeding (converted from
  controller, same routes/shapes), photos (Oscar contract: GET array/POST multipart/DELETE,
  url /photos/files/{fileName}). Gridify paged {items,totalCount,page,pageSize}, camelCase,
  409 conflict-deletes, flat FK ids, litter parent-gender validation (400 {code,...}).
- Scalar replaces Swashbuckle (AddOpenApi/MapOpenApi + MapScalarApiReference at /scalar);
  launchUrl swagger->scalar. GenericRepository/UnitOfWork/Converters deleted; DbContext direct.
- InbreedingService reads real FK props now; pure calculator + 8 tests untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 00:53:49 +02:00

92 lines
4.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using GerbilManagerWebAPI.Common;
using GerbilManagerWebAPI.Dtos;
using GerbilManagerWebAPI.Models;
using Gridify;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
namespace GerbilManagerWebAPI.Endpoints
{
public static class LitterEndpoints
{
public static IEndpointRouteBuilder MapLitterEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/litters").WithTags("Litters");
// GET /litters (Gridify: date range + orderBy=date supported on the DateOnly column)
group.MapGet("/", async ([AsParameters] GridifyQuery query, ApplicationContext db) =>
TypedResults.Ok(await db.Litters.AsNoTracking().ToPagedResultAsync(query, ToDto)));
group.MapGet("/{id:guid}", async Task<Results<Ok<LitterDto>, NotFound>> (Guid id, ApplicationContext db) =>
{
var l = await db.Litters.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id);
return l is null ? TypedResults.NotFound() : TypedResults.Ok(ToDto(l));
});
group.MapPost("/", async Task<Results<Created<LitterDto>, BadRequest<ParentGenderError>>> (LitterInput input, ApplicationContext db) =>
{
var err = await ValidateParents(db, input.FatherId, input.MotherId);
if (err is not null) return TypedResults.BadRequest(err);
var l = new Litter { Id = Guid.NewGuid(), Name = input.Name, Date = input.Date };
Apply(l, input);
db.Litters.Add(l);
await db.SaveChangesAsync();
return TypedResults.Created($"/litters/{l.Id}", ToDto(l));
});
group.MapPut("/{id:guid}", async Task<Results<NoContent, NotFound, BadRequest<ParentGenderError>>> (Guid id, LitterInput input, ApplicationContext db) =>
{
var l = await db.Litters.FirstOrDefaultAsync(x => x.Id == id);
if (l is null) return TypedResults.NotFound();
var err = await ValidateParents(db, input.FatherId, input.MotherId);
if (err is not null) return TypedResults.BadRequest(err);
l.Name = input.Name;
l.Date = input.Date;
Apply(l, input);
await db.SaveChangesAsync();
return TypedResults.NoContent();
});
group.MapDelete("/{id:guid}", async Task<Results<NoContent, NotFound>> (Guid id, ApplicationContext db) =>
{
var l = await db.Litters.FirstOrDefaultAsync(x => x.Id == id);
if (l is null) return TypedResults.NotFound();
db.Litters.Remove(l);
await db.SaveChangesAsync();
return TypedResults.NoContent();
});
return app;
}
// father must not be female; mother must not be male (unknown is allowed).
private static async Task<ParentGenderError?> ValidateParents(ApplicationContext db, Guid? fatherId, Guid? motherId)
{
var father = fatherId is Guid f ? await db.Gerbils.AsNoTracking().FirstOrDefaultAsync(x => x.Id == f) : null;
var mother = motherId is Guid m ? await db.Gerbils.AsNoTracking().FirstOrDefaultAsync(x => x.Id == m) : null;
if (father?.Gender == Gender.female || mother?.Gender == Gender.male)
{
return new ParentGenderError("InvalidParentGender",
father?.Gender ?? Gender.unknown, mother?.Gender ?? Gender.unknown);
}
return null;
}
private static void Apply(Litter l, LitterInput i)
{
l.TotalBorn = i.TotalBorn;
l.FatherId = i.FatherId;
l.MotherId = i.MotherId;
l.ExpectedGoHomeDate = i.ExpectedGoHomeDate;
l.Notes = i.Notes;
}
private static LitterDto ToDto(Litter l) => new(
l.Id, l.Name, l.Date, l.TotalBorn, l.FatherId, l.MotherId, l.ExpectedGoHomeDate, l.Notes);
}
/// <summary>400 body for a father×mother gender mismatch; frontend localises by Code.</summary>
public record ParentGenderError(string Code, Gender FatherGender, Gender MotherGender);
}