Compare commits
3 Commits
c6de10b98b
...
f313b34d86
| Author | SHA1 | Date | |
|---|---|---|---|
| f313b34d86 | |||
|
|
3493038e55 | ||
|
|
e92e8db399 |
576
.claude/agents/project-init.md
Normal file
576
.claude/agents/project-init.md
Normal file
@ -0,0 +1,576 @@
|
|||||||
|
---
|
||||||
|
model: sonnet
|
||||||
|
description: Scaffolds a complete new Svrnty.CQRS project from a natural language description. Creates solution, web project, DAL with PostgreSQL, entities, Program.cs, first feature, and .editorconfig.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Project Init Agent
|
||||||
|
|
||||||
|
You scaffold new Svrnty.CQRS projects from natural language descriptions.
|
||||||
|
|
||||||
|
Given a description like "create an order management API with orders and customers", you produce a compilable .NET solution with CQRS wiring, a DAL, entity models, and a working first feature.
|
||||||
|
|
||||||
|
## Step 0 — Parse the Request
|
||||||
|
|
||||||
|
Extract from the user's description:
|
||||||
|
|
||||||
|
1. **Project name** — e.g. `OrderManagement`, `Billing`. If unclear, ask.
|
||||||
|
2. **Domain entities** — e.g. Order, Customer, Product. Infer from context.
|
||||||
|
3. **Protocol choice** — default is **gRPC only**. If the user explicitly asks for HTTP/REST, add MinimalApi. If they say "both", add both. Only ask if the description is ambiguous.
|
||||||
|
|
||||||
|
Infer without asking:
|
||||||
|
- Entity properties (reasonable defaults for the domain)
|
||||||
|
- Relationships between entities
|
||||||
|
- Feature folder organization
|
||||||
|
|
||||||
|
## Step 1 — Create the Solution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create solution directory (sibling to current workspace or in user-specified location)
|
||||||
|
mkdir -p {ProjectName}
|
||||||
|
cd {ProjectName}
|
||||||
|
|
||||||
|
# Create solution
|
||||||
|
dotnet new sln -n {ProjectName}
|
||||||
|
|
||||||
|
# Create web project (API host)
|
||||||
|
dotnet new web -n {ProjectName} --no-https
|
||||||
|
dotnet sln add {ProjectName}/{ProjectName}.csproj
|
||||||
|
|
||||||
|
# Create DAL classlib
|
||||||
|
dotnet new classlib -n {ProjectName}.Dal
|
||||||
|
dotnet sln add {ProjectName}.Dal/{ProjectName}.Dal.csproj
|
||||||
|
|
||||||
|
# Add project reference: API depends on DAL
|
||||||
|
dotnet add {ProjectName}/{ProjectName}.csproj reference {ProjectName}.Dal/{ProjectName}.Dal.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2 — Add NuGet Packages
|
||||||
|
|
||||||
|
### DAL Project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd {ProjectName}.Dal
|
||||||
|
dotnet add package Microsoft.EntityFrameworkCore -v 10.0.0-*
|
||||||
|
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL -v 10.0.0-*
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web Project — Always
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd {ProjectName}
|
||||||
|
dotnet add package Svrnty.CQRS
|
||||||
|
dotnet add package Svrnty.CQRS.Abstractions
|
||||||
|
dotnet add package Svrnty.CQRS.DynamicQuery
|
||||||
|
dotnet add package Svrnty.CQRS.DynamicQuery.Abstractions
|
||||||
|
dotnet add package Svrnty.CQRS.FluentValidation
|
||||||
|
dotnet add package FluentValidation
|
||||||
|
dotnet add package PoweredSoft.DynamicQuery
|
||||||
|
dotnet add package PoweredSoft.Data.Core
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web Project — gRPC (default)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet add package Svrnty.CQRS.Grpc
|
||||||
|
dotnet add package Svrnty.CQRS.Grpc.Abstractions
|
||||||
|
dotnet add package Svrnty.CQRS.Grpc.Generators
|
||||||
|
dotnet add package Grpc.AspNetCore
|
||||||
|
dotnet add package Grpc.AspNetCore.Server.Reflection
|
||||||
|
dotnet add package Grpc.Tools
|
||||||
|
dotnet add package Grpc.StatusProto
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web Project — MinimalApi (only if user requests HTTP)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet add package Svrnty.CQRS.MinimalApi
|
||||||
|
dotnet add package Svrnty.CQRS.DynamicQuery.MinimalApi
|
||||||
|
dotnet add package Swashbuckle.AspNetCore
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3 — Patch the Web .csproj
|
||||||
|
|
||||||
|
After adding packages, edit `{ProjectName}/{ProjectName}.csproj` to add source generator and proto settings:
|
||||||
|
|
||||||
|
### gRPC (default)
|
||||||
|
|
||||||
|
Add these elements inside `<Project>`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<PropertyGroup>
|
||||||
|
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
|
||||||
|
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Protobuf Include="Protos\*.proto" GrpcServices="Server" />
|
||||||
|
</ItemGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
Also ensure the `Svrnty.CQRS.Grpc.Generators` package reference has the analyzer attributes:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<PackageReference Include="Svrnty.CQRS.Grpc.Generators" Version="..." OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4 — Create the DAL
|
||||||
|
|
||||||
|
### Entity Models
|
||||||
|
|
||||||
|
Create `{ProjectName}.Dal/Entities/{EntityName}.cs` for each entity:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
namespace {ProjectName}.Dal.Entities;
|
||||||
|
|
||||||
|
public record {EntityName}
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
// Properties inferred from description
|
||||||
|
// Strings default to string.Empty, collections to []
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Guidelines for entity properties:
|
||||||
|
- Always include `Id` (int), `CreatedAt`, `UpdatedAt`
|
||||||
|
- Use `string.Empty` as default for string properties
|
||||||
|
- Use `[]` as default for collection properties
|
||||||
|
- Add foreign keys for relationships (e.g. `public int CustomerId { get; set; }`)
|
||||||
|
|
||||||
|
### DbContext
|
||||||
|
|
||||||
|
Create `{ProjectName}.Dal/{ProjectName}DbContext.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using {ProjectName}.Dal.Entities;
|
||||||
|
|
||||||
|
namespace {ProjectName}.Dal;
|
||||||
|
|
||||||
|
public class {ProjectName}DbContext : DbContext
|
||||||
|
{
|
||||||
|
public {ProjectName}DbContext(DbContextOptions<{ProjectName}DbContext> options) : base(options) { }
|
||||||
|
|
||||||
|
// One DbSet per entity
|
||||||
|
public DbSet<Order> Orders { get; set; } = null!;
|
||||||
|
public DbSet<Customer> Customers { get; set; } = null!;
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
// Configure relationships, indexes, etc.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 5 — Create SimpleAsyncQueryableService
|
||||||
|
|
||||||
|
This boilerplate is required for dynamic queries. Create `{ProjectName}/SimpleAsyncQueryableService.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using PoweredSoft.Data.Core;
|
||||||
|
|
||||||
|
namespace {ProjectName};
|
||||||
|
|
||||||
|
public class SimpleAsyncQueryableService : IAsyncQueryableService
|
||||||
|
{
|
||||||
|
public IEnumerable<IAsyncQueryableHandlerService> Handlers { get; } = Array.Empty<IAsyncQueryableHandlerService>();
|
||||||
|
|
||||||
|
public Task<List<TSource>> ToListAsync<TSource>(IQueryable<TSource> queryable, CancellationToken cancellationToken = default)
|
||||||
|
=> Task.FromResult(queryable.ToList());
|
||||||
|
|
||||||
|
public Task<TSource?> FirstOrDefaultAsync<TSource>(IQueryable<TSource> queryable, CancellationToken cancellationToken = default)
|
||||||
|
=> Task.FromResult(queryable.FirstOrDefault());
|
||||||
|
|
||||||
|
public Task<TSource?> FirstOrDefaultAsync<TSource>(IQueryable<TSource> queryable, Expression<Func<TSource, bool>> predicate, CancellationToken cancellationToken = default)
|
||||||
|
=> Task.FromResult(queryable.FirstOrDefault(predicate));
|
||||||
|
|
||||||
|
public Task<TSource?> LastOrDefaultAsync<TSource>(IQueryable<TSource> queryable, CancellationToken cancellationToken = default)
|
||||||
|
=> Task.FromResult(queryable.LastOrDefault());
|
||||||
|
|
||||||
|
public Task<TSource?> LastOrDefaultAsync<TSource>(IQueryable<TSource> queryable, Expression<Func<TSource, bool>> predicate, CancellationToken cancellationToken = default)
|
||||||
|
=> Task.FromResult(queryable.LastOrDefault(predicate));
|
||||||
|
|
||||||
|
public Task<bool> AnyAsync<TSource>(IQueryable<TSource> queryable, Expression<Func<TSource, bool>> predicate, CancellationToken cancellationToken = default)
|
||||||
|
=> Task.FromResult(queryable.Any(predicate));
|
||||||
|
|
||||||
|
public Task<bool> AllAsync<TSource>(IQueryable<TSource> queryable, Expression<Func<TSource, bool>> predicate, CancellationToken cancellationToken = default)
|
||||||
|
=> Task.FromResult(queryable.All(predicate));
|
||||||
|
|
||||||
|
public Task<int> CountAsync<TSource>(IQueryable<TSource> queryable, CancellationToken cancellationToken = default)
|
||||||
|
=> Task.FromResult(queryable.Count());
|
||||||
|
|
||||||
|
public Task<long> LongCountAsync<TSource>(IQueryable<TSource> queryable, CancellationToken cancellationToken = default)
|
||||||
|
=> Task.FromResult(queryable.LongCount());
|
||||||
|
|
||||||
|
public Task<TSource?> SingleOrDefaultAsync<TSource>(IQueryable<TSource> queryable, Expression<Func<TSource, bool>> predicate, CancellationToken cancellationToken = default)
|
||||||
|
=> Task.FromResult(queryable.SingleOrDefault(predicate));
|
||||||
|
|
||||||
|
public Task<bool> AnyAsync<TSource>(IQueryable<TSource> queryable, CancellationToken cancellationToken = default)
|
||||||
|
=> Task.FromResult(queryable.Any());
|
||||||
|
|
||||||
|
public IAsyncQueryableHandlerService? GetAsyncQueryableHandler<TSource>(IQueryable<TSource> queryable)
|
||||||
|
=> null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** When the project uses EF Core with a real database, replace this with a proper EF Core async queryable service that delegates to `EntityFrameworkQueryableExtensions`. This in-memory version is for initial scaffolding only.
|
||||||
|
|
||||||
|
## Step 6 — Create First Feature
|
||||||
|
|
||||||
|
Scaffold one command and one dynamic query as working examples.
|
||||||
|
|
||||||
|
### Command
|
||||||
|
|
||||||
|
Create `{ProjectName}/Features/{DomainArea}/{CommandName}Command.cs`:
|
||||||
|
|
||||||
|
The command name must express **domain intent**, not CRUD:
|
||||||
|
- "create an order" -> `PlaceOrderCommand`
|
||||||
|
- "add a customer" -> `RegisterCustomerCommand`
|
||||||
|
- "create an invoice" -> `IssueInvoiceCommand`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using FluentValidation;
|
||||||
|
using Svrnty.CQRS.Abstractions;
|
||||||
|
|
||||||
|
namespace {ProjectName}.Features.{DomainArea};
|
||||||
|
|
||||||
|
public record {CommandName}Command
|
||||||
|
{
|
||||||
|
// Properties based on entity, excluding Id and timestamps
|
||||||
|
// Strings default to string.Empty, collections to []
|
||||||
|
}
|
||||||
|
|
||||||
|
public class {CommandName}CommandValidator : AbstractValidator<{CommandName}Command>
|
||||||
|
{
|
||||||
|
public {CommandName}CommandValidator()
|
||||||
|
{
|
||||||
|
// Validation rules — always include .WithMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class {CommandName}CommandHandler : ICommandHandler<{CommandName}Command, int>
|
||||||
|
{
|
||||||
|
private readonly {DbContextType} _db;
|
||||||
|
|
||||||
|
public {CommandName}CommandHandler({DbContextType} db) => _db = db;
|
||||||
|
|
||||||
|
public async Task<int> HandleAsync({CommandName}Command command, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var entity = new {Entity}
|
||||||
|
{
|
||||||
|
// Map from command properties
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
_db.{EntityPlural}.Add(entity);
|
||||||
|
await _db.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return entity.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic Query Provider
|
||||||
|
|
||||||
|
Create `{ProjectName}/Features/{DomainArea}/{Entity}QueryableProvider.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Svrnty.CQRS.DynamicQuery.Abstractions;
|
||||||
|
using {ProjectName}.Dal;
|
||||||
|
using {ProjectName}.Dal.Entities;
|
||||||
|
|
||||||
|
namespace {ProjectName}.Features.{DomainArea};
|
||||||
|
|
||||||
|
public class {Entity}QueryableProvider : IQueryableProviderOverride<{Entity}>
|
||||||
|
{
|
||||||
|
private readonly {DbContextType} _db;
|
||||||
|
|
||||||
|
public {Entity}QueryableProvider({DbContextType} db) => _db = db;
|
||||||
|
|
||||||
|
public Task<IQueryable<{Entity}>> GetQueryableAsync(object query, CancellationToken cancellationToken = default)
|
||||||
|
=> Task.FromResult(_db.{EntityPlural}.AsQueryable());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 7 — Create Program.cs
|
||||||
|
|
||||||
|
### gRPC Only (default)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Svrnty.CQRS;
|
||||||
|
using Svrnty.CQRS.DynamicQuery;
|
||||||
|
using Svrnty.CQRS.FluentValidation;
|
||||||
|
using Svrnty.CQRS.Grpc;
|
||||||
|
using {ProjectName};
|
||||||
|
using {ProjectName}.Dal;
|
||||||
|
using {ProjectName}.Dal.Entities;
|
||||||
|
using {ProjectName}.Features.{DomainArea};
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Configure Kestrel for HTTP/2 (gRPC)
|
||||||
|
builder.WebHost.ConfigureKestrel(options =>
|
||||||
|
{
|
||||||
|
options.ListenLocalhost(5000, o => o.Protocols = HttpProtocols.Http2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Database
|
||||||
|
builder.Services.AddDbContext<{ProjectName}DbContext>(options =>
|
||||||
|
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||||
|
|
||||||
|
// Dynamic query dependencies (must be registered BEFORE AddSvrntyCqrs)
|
||||||
|
builder.Services.AddTransient<PoweredSoft.Data.Core.IAsyncQueryableService, SimpleAsyncQueryableService>();
|
||||||
|
builder.Services.AddTransient<PoweredSoft.DynamicQuery.Core.IQueryHandlerAsync, PoweredSoft.DynamicQuery.QueryHandlerAsync>();
|
||||||
|
|
||||||
|
// Register commands and queries
|
||||||
|
builder.Services.AddCommand<{CommandName}Command, int, {CommandName}CommandHandler, {CommandName}CommandValidator>();
|
||||||
|
builder.Services.AddDynamicQueryWithProvider<{Entity}, {Entity}QueryableProvider>();
|
||||||
|
|
||||||
|
// Configure CQRS
|
||||||
|
builder.Services.AddSvrntyCqrs(cqrs =>
|
||||||
|
{
|
||||||
|
cqrs.AddGrpc(grpc => grpc.EnableReflection());
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseSvrntyCqrs();
|
||||||
|
|
||||||
|
Console.WriteLine("gRPC (HTTP/2): http://localhost:5000");
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
```
|
||||||
|
|
||||||
|
### gRPC + MinimalApi (if user requests both)
|
||||||
|
|
||||||
|
Add to the above:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Kestrel — dual port
|
||||||
|
builder.WebHost.ConfigureKestrel(options =>
|
||||||
|
{
|
||||||
|
options.ListenLocalhost(5000, o => o.Protocols = HttpProtocols.Http2); // gRPC
|
||||||
|
options.ListenLocalhost(5001, o => o.Protocols = HttpProtocols.Http1); // HTTP
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add MinimalApi + Swagger
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
|
builder.Services.AddSvrntyCqrs(cqrs =>
|
||||||
|
{
|
||||||
|
cqrs.AddGrpc(grpc => grpc.EnableReflection());
|
||||||
|
cqrs.AddMinimalApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
// After app.UseSvrntyCqrs():
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
```
|
||||||
|
|
||||||
|
### MinimalApi Only (if user explicitly requests HTTP without gRPC)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Svrnty.CQRS;
|
||||||
|
using Svrnty.CQRS.DynamicQuery;
|
||||||
|
using Svrnty.CQRS.FluentValidation;
|
||||||
|
using Svrnty.CQRS.MinimalApi;
|
||||||
|
using {ProjectName};
|
||||||
|
using {ProjectName}.Dal;
|
||||||
|
using {ProjectName}.Dal.Entities;
|
||||||
|
using {ProjectName}.Features.{DomainArea};
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Database
|
||||||
|
builder.Services.AddDbContext<{ProjectName}DbContext>(options =>
|
||||||
|
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||||
|
|
||||||
|
// Dynamic query dependencies
|
||||||
|
builder.Services.AddTransient<PoweredSoft.Data.Core.IAsyncQueryableService, SimpleAsyncQueryableService>();
|
||||||
|
builder.Services.AddTransient<PoweredSoft.DynamicQuery.Core.IQueryHandlerAsync, PoweredSoft.DynamicQuery.QueryHandlerAsync>();
|
||||||
|
|
||||||
|
// Register commands and queries
|
||||||
|
builder.Services.AddCommand<{CommandName}Command, int, {CommandName}CommandHandler, {CommandName}CommandValidator>();
|
||||||
|
builder.Services.AddDynamicQueryWithProvider<{Entity}, {Entity}QueryableProvider>();
|
||||||
|
|
||||||
|
// Configure CQRS
|
||||||
|
builder.Services.AddSvrntyCqrs(cqrs =>
|
||||||
|
{
|
||||||
|
cqrs.AddMinimalApi();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseSvrntyCqrs();
|
||||||
|
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
|
||||||
|
Console.WriteLine("HTTP API: http://localhost:5000/api/command/* and /api/query/*");
|
||||||
|
Console.WriteLine("Swagger UI: http://localhost:5000/swagger");
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 8 — Create appsettings.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "Host=localhost;Database={project_name_snake};Username=postgres;Password=postgres"
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 9 — Create Proto File (gRPC projects)
|
||||||
|
|
||||||
|
Create `{ProjectName}/Protos/services.proto`:
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
option csharp_namespace = "{ProjectName}.Protos";
|
||||||
|
|
||||||
|
package {project_name_lower};
|
||||||
|
|
||||||
|
// Command Service
|
||||||
|
service CommandService {
|
||||||
|
rpc {CommandNameWithoutSuffix} ({CommandName}Request) returns ({CommandName}Response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic Query Service
|
||||||
|
service DynamicQueryService {
|
||||||
|
rpc Query{EntityPlural} (DynamicQuery{EntityPlural}Request) returns (DynamicQuery{EntityPlural}Response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Command Messages ===
|
||||||
|
|
||||||
|
message {CommandName}Request {
|
||||||
|
// fields matching command properties, snake_case, numbered sequentially
|
||||||
|
}
|
||||||
|
|
||||||
|
message {CommandName}Response {
|
||||||
|
int32 result = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Entity Messages ===
|
||||||
|
|
||||||
|
message {Entity} {
|
||||||
|
int32 id = 1;
|
||||||
|
// fields matching entity properties
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Dynamic Query Messages ===
|
||||||
|
|
||||||
|
message DynamicQuery{EntityPlural}Request {
|
||||||
|
int32 page = 1;
|
||||||
|
int32 page_size = 2;
|
||||||
|
repeated DynamicQueryFilter filters = 3;
|
||||||
|
repeated DynamicQuerySort sorts = 4;
|
||||||
|
repeated DynamicQueryGroup groups = 5;
|
||||||
|
repeated DynamicQueryAggregate aggregates = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DynamicQuery{EntityPlural}Response {
|
||||||
|
repeated {Entity} data = 1;
|
||||||
|
int64 total_records = 2;
|
||||||
|
int32 number_of_pages = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Standard Dynamic Query Types (reuse across entities) ===
|
||||||
|
|
||||||
|
message DynamicQueryFilter {
|
||||||
|
string path = 1;
|
||||||
|
int32 type = 2;
|
||||||
|
string value = 3;
|
||||||
|
repeated DynamicQueryFilter and = 4;
|
||||||
|
repeated DynamicQueryFilter or = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DynamicQuerySort {
|
||||||
|
string path = 1;
|
||||||
|
bool ascending = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DynamicQueryGroup {
|
||||||
|
string path = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DynamicQueryAggregate {
|
||||||
|
string path = 1;
|
||||||
|
int32 type = 2;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 10 — Copy .editorconfig
|
||||||
|
|
||||||
|
Copy the standard .editorconfig to the solution root. Read it from the Svrnty.CQRS repo at `.editorconfig` (in the workspace where this agent runs) and write it to the new project's root directory.
|
||||||
|
|
||||||
|
If you cannot read the source .editorconfig, create one with these essentials:
|
||||||
|
- `root = true`
|
||||||
|
- 4-space indent for `*.cs`, 2-space for `*.csproj`, `*.json`, `*.proto`
|
||||||
|
- File-scoped namespaces (warning)
|
||||||
|
- Allman brace style
|
||||||
|
- `_camelCase` for private fields
|
||||||
|
- `var` when type is apparent
|
||||||
|
|
||||||
|
## Step 11 — Build Verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd {SolutionRoot}
|
||||||
|
dotnet build
|
||||||
|
```
|
||||||
|
|
||||||
|
If the build fails:
|
||||||
|
1. Read the error output carefully
|
||||||
|
2. Fix the issues (missing usings, type mismatches, package version conflicts)
|
||||||
|
3. Re-run `dotnet build`
|
||||||
|
4. Repeat until the build succeeds
|
||||||
|
|
||||||
|
Do NOT consider the scaffolding complete until `dotnet build` succeeds with 0 errors.
|
||||||
|
|
||||||
|
## Step 12 — Summary
|
||||||
|
|
||||||
|
After everything compiles, print a summary:
|
||||||
|
|
||||||
|
```
|
||||||
|
Created {ProjectName} with:
|
||||||
|
- Solution: {ProjectName}.sln
|
||||||
|
- Web project: {ProjectName}/ (gRPC on port 5000)
|
||||||
|
- DAL project: {ProjectName}.Dal/ (PostgreSQL + EF Core)
|
||||||
|
- Entities: {list}
|
||||||
|
- Features: {list of commands and queries}
|
||||||
|
|
||||||
|
Next steps:
|
||||||
|
- Update the connection string in appsettings.json
|
||||||
|
- Run `dotnet ef migrations add Initial` to create the first migration
|
||||||
|
- Use /add-command to add more commands
|
||||||
|
- Use /add-dynamic-query to add more queries
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important Rules
|
||||||
|
|
||||||
|
1. **Domain intent naming** — Commands express user intent, not CRUD. "Create order" becomes `PlaceOrderCommand`, not `CreateOrderCommand`.
|
||||||
|
2. **Single file per feature** — Command, validator, and handler go in the same `.cs` file.
|
||||||
|
3. **Always use CancellationToken** — Never omit it from handler signatures.
|
||||||
|
4. **Record types** — Commands and queries are `record` types with `{ get; set; }` properties.
|
||||||
|
5. **Defaults** — Strings default to `string.Empty`, collections to `[]`.
|
||||||
|
6. **Register before AddSvrntyCqrs** — Dynamic query dependencies and all command/query registrations must come before `AddSvrntyCqrs()`.
|
||||||
|
7. **File-scoped namespaces** — Always use file-scoped namespace declarations.
|
||||||
|
8. **Proto field naming** — Use `snake_case` in proto files; the framework maps to PascalCase C# properties case-insensitively.
|
||||||
@ -26,6 +26,10 @@
|
|||||||
<None Include="..\README.md" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
|
<None Include="..\README.md" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
|
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Builder;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Svrnty.CQRS.Configuration;
|
using Svrnty.CQRS.Configuration;
|
||||||
|
|
||||||
namespace Svrnty.CQRS.MinimalApi;
|
namespace Svrnty.CQRS;
|
||||||
|
|
||||||
public static class WebApplicationExtensions
|
public static class WebApplicationExtensions
|
||||||
{
|
{
|
||||||
Loading…
Reference in New Issue
Block a user