Generate a complete CRUD service for a new domain entity including Entity, DTOs, Interface, Service, DI registration, and DbSet. Use when creating a new domain feature from scratch.
Create a new CRUD service for the domain: $ARGUMENTS
Generate all 4 files for a new domain CRUD feature following the exact patterns in this project.
src/FairwayFinder.Data/Entities/{EntityName}.csFollow the exact entity pattern:
namespace FairwayFinder.Data.Entities;
public partial class {EntityName}
{
public long {EntityName}Id { get; set; }
// Domain properties...
public string CreatedBy { get; set; } = null!;
public DateOnly CreatedOn { get; set; }
public string UpdatedBy { get; set; } = null!;
public DateOnly UpdatedOn { get; set; }
public bool IsDeleted { get; set; }
}
src/FairwayFinder.Features/Data/{EntityName}Dtos.csInclude these DTO types:
{EntityName}ListItem — Row DTO for data grids (lightweight, only display fields){EntityName}DetailResponse — Full detail response with all fields and nested objectsSave{EntityName}Request — Shared create/update request. Nullable ID (long?) means create when null, update when setnamespace FairwayFinder.Features.Data;
public class {EntityName}ListItem
{
public long {EntityName}Id { get; set; }
// Display fields only
}
public class {EntityName}DetailResponse
{
public long {EntityName}Id { get; set; }
// All fields + nested collections
}
public class Save{EntityName}Request
{
public long? {EntityName}Id { get; set; }
// Editable fields only
}
src/FairwayFinder.Features/Services/Interfaces/I{EntityName}Service.csusing FairwayFinder.Features.Data;
namespace FairwayFinder.Features.Services.Interfaces;
public interface I{EntityName}Service
{
Task<List<{EntityName}ListItem>> GetAll{EntityName}sAsync();
Task<{EntityName}DetailResponse?> Get{EntityName}DetailAsync(long id);
Task<long> Create{EntityName}Async(Save{EntityName}Request request, string userId);
Task<bool> Update{EntityName}Async(Save{EntityName}Request request, string userId);
Task<bool> Delete{EntityName}Async(long id, string userId);
}
src/FairwayFinder.Features/Services/{EntityName}Service.csFollow these exact patterns:
IDbContextFactory<ApplicationDbContext> (NOT DbContext directly)await using var dbContext = await _dbContextFactory.CreateDbContextAsync();!c.IsDeletedCreatedBy, CreatedOn, UpdatedBy, UpdatedOn, IsDeleted = false, return new IDUpdatedBy/UpdatedOn, return boolIsDeleted = true), cascade to children, return boolDateOnly.FromDateTime(DateTime.UtcNow)using FairwayFinder.Data;
using FairwayFinder.Data.Entities;
using FairwayFinder.Features.Data;
using FairwayFinder.Features.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace FairwayFinder.Features.Services;
public class {EntityName}Service : I{EntityName}Service
{
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
public {EntityName}Service(IDbContextFactory<ApplicationDbContext> dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
// Implement all interface methods...
}
src/FairwayFinder.Features/ServiceRegistration.csAdd under the // Domain services comment:
services.AddTransient<I{EntityName}Service, {EntityName}Service>();
src/FairwayFinder.Data/ApplicationDbContext.csAdd the DbSet property to ApplicationDbContext:
public DbSet<{EntityName}> {EntityName}s { get; set; }
Default behavior: update existing records in place. Never delete-and-reinsert.
When updating a parent entity that has child collections (e.g. a Round has Scores, Scores have Shots), follow this pattern:
UpdatedBy/UpdatedOn// ✅ CORRECT — upsert in place
var existingChildren = await dbContext.Children
.Where(c => c.ParentId == parentId && !c.IsDeleted)
.OrderBy(c => c.SortOrder)
.ToListAsync();
for (int i = 0; i < incomingChildren.Count; i++)
{
if (i < existingChildren.Count)
{
// Update existing
existingChildren[i].SomeField = incomingChildren[i].SomeField;
existingChildren[i].UpdatedBy = userId;
existingChildren[i].UpdatedOn = today;
}
else
{
// Insert new
dbContext.Children.Add(new Child { /* ... */ });
}
}
// Soft-delete extras that are no longer needed
for (int i = incomingChildren.Count; i < existingChildren.Count; i++)
{
existingChildren[i].IsDeleted = true;
existingChildren[i].UpdatedBy = userId;
existingChildren[i].UpdatedOn = today;
}
// ❌ WRONG — delete-and-reinsert
foreach (var existing in existingChildren)
{
existing.IsDeleted = true; // deleting everything
}
foreach (var incoming in incomingChildren)
{
dbContext.Children.Add(new Child { /* ... */ }); // reinserting everything
}
When soft-delete IS appropriate:
DeleteAsync — the user explicitly deletes a parent; cascade soft-delete to all children.Select() projections in queries, don't load full entities for list operations