1. The model and the database

Create the projects

Create the class libraries and their test projects next to the Workspaces ones, and add them to back/Blouppy.slnx:

ProjectReferences
Blouppy.Projects.ModelsBlouppy.SharedKernel
Blouppy.Projects.AbstractionsBlouppy.Projects.Models
Blouppy.ProjectsBlouppy.Projects.Abstractions, Blouppy.Workspaces.PublicData, Blouppy.Infrastructure.Database.Abstractions, Blouppy.Infrastructure.Messaging.Abstractions, Blouppy.Events
Blouppy.Projects.Models.UnitTestsBlouppy.Projects.Models
Blouppy.Projects.UnitTestsBlouppy.Projects

Blouppy.Workspaces.csproj is the template: it also lists the FluentValidation packages and gives its test project access to its internal types (InternalsVisibleTo). Package versions are central, in back/Directory.Packages.props: write <PackageReference Include="FluentValidation" /> without a Version.

Blouppy.Infrastructure.Database.Abstractions and Blouppy.ApiQuery.Models also reference Blouppy.Projects.Models, since they type their ports with its entity.

The strongly-typed id

Ids are records, so a ProjectId can never be passed where a WorkspaceId is expected. In Blouppy.SharedKernel/Types/:

public sealed record ProjectId(int Value) : IntStrongType(Value);

No converter to write: the database adapter maps every strong type it finds.

The entity

In Blouppy.Projects.Models/. Plain C#, no EF attribute; the entity guards its own invariants and records what happened:

public enum ProjectStatus
{
    Planned,
    Active,
    Done
}

public sealed class Project(string name, WorkspaceId workspaceId) : AuditableEntityBase<ProjectId>
{
    public const int MaxNameLength = 100;

    public string Name { get; private set; } = Exceptions.ThrowIfNullOrWhiteSpace(name);
    public ProjectStatus Status { get; private set; } = ProjectStatus.Planned;
    public WorkspaceId WorkspaceId { get; private set; } = Exceptions.ThrowIfNullOrNegativeOrZero(workspaceId);

    public void MarkAsCreated()
        => AddDomainEvent(new ProjectCreatedDomainEvent(Exceptions.ThrowIfNullOrNegativeOrZero(Id), WorkspaceId));

    public void ChangeStatus(ProjectStatus status)
        => Status = status;
}

AuditableEntityBase<TId> adds CreatedAt, UpdatedAt, CreatedBy and UpdatedBy, stamped by an interceptor on save. Add a private parameterless constructor for EF Core.

The EF Core configuration

In Blouppy.Infrastructure.Database/Projects/ProjectConfiguration.cs, discovered automatically:

internal sealed class ProjectConfiguration : AuditableEntityTypeConfiguration<Project>, IModelConfiguration
{
    protected override void ConfigureEntity(EntityTypeBuilder<Project> builder)
    {
        builder.ToTable("projects");

        builder.Property(x => x.Name).HasMaxLength(Project.MaxNameLength).IsRequired();

        // A real foreign key, never a bare id column: deleting the workspace deletes its projects.
        builder
            .HasOne<Workspace>()
            .WithMany()
            .HasForeignKey(x => x.WorkspaceId);
    }

    // Nothing else assigns an int key: without a sequence every insert would carry 0.
    public void ConfigureModel(ModelBuilder modelBuilder)
        => modelBuilder.UseSequence<Project, ProjectId>(increment: 5);
}

The database adapter may name both entities; the Projects domain never names Workspace. Then add DbSet<Project> to BlouppyDbContext.

The repository

The port, in Blouppy.Infrastructure.Database.Abstractions/Projects/IProjectsRepository.cs:

public interface IProjectsRepository
{
    Task AddAsync(Project project, CancellationToken cancellationToken = default);
    Task<SearchResult<Project>> TryGetAsync(ProjectId id, CancellationToken cancellationToken = default);
}

The adapter, internal sealed class ProjectsRepository(BlouppyDbContext dbContext), in Blouppy.Infrastructure.Database/Projects/, modelled on TeamsRepository. A repository never commits — the unit of work does. Register it as scoped in the database adapter's ServiceCollectionExtensions.cs.

The migration

Blouppy.Web hosts several contexts, so name the one you mean:

dotnet ef migrations add Add_Project -s back/Blouppy.Web -p back/Blouppy.Infrastructure.Database \
  -c BlouppyDbContext -o Migrations

Read the generated file: the sequence, the foreign key and its onDelete, and nothing unrelated. You never apply it by hand in development — the migration runner does on the next start. CI fails if a model change has no migration.