AI-Enabled RMA, part 2: Eight projects, one dependency rule
Part 2from the AI-Enabled RMA series · 6 parts in all
AIEnabledRMA ships as one solution
file, AIEnabledRma.slnx, with eight projects and a single dependency rule:
domain code never references I/O, and every host depends on the same registrations.
This article is the map — what each project is for, and why the boundaries are where they are.
The projects
src/AIEnabledRma.Domain— entities, theRmaWorkflowstate machine, the policy evaluator, the triage pipeline and scope guard, the fuzzy matcher, and the abstractions everything else implements. No I/O, no database, no HTTP. It is the only project with business rules in it.src/AIEnabledRma.Data— PostgreSQL through EF Core 10:RmaDbContext, repositories, migrations, schema bootstrap, the RMA number sequence, and the demo seeder. Entities are mapped with snake_case names so the schema matches PostgreSQL convention and the raw-SQL trigram indexes are readable without quoting.src/AIEnabledRma.Ai— provider selection and startup validation, plus the bundled fake payment gateway. One switch,Ai:Provider, decides whichIChatModelthe process talks to.src/AIEnabledRma.Rag— an in-process knowledge index over Markdown articles, with BM25-style lexical ranking. No embedding model, deliberately.src/AIEnabledRma.Mcp— a stdio MCP server exposing the same policy and lookups as agent tools.src/AIEnabledRma.Web— the customer-facing return wizard, ASP.NET Core MVC.tools/DbAdmin— a console app that canmigrate,seed,reset --force, list migrations and printstats.tests/AIEnabledRma.Tests— the suite: policy, workflow, fuzzy matching, retrieval, knowledge-base loading, the AI provider switch, the MCP tools, the number sequence, and an end-to-end wizard run against a real database.
The rule that matters most
The three hosts — web, MCP and the RAG service — call the same registration methods.
AddRmaDbContext plus AddRmaData give every host the same repositories
and, critically, the same fuzzy-match thresholds. AddRag gives them the same
retriever over the same corpus. AddAiProvider gives them the same model switch.
The comment in the README is the actual design goal: the fuzzy-match thresholds a customer-facing wizard uses and the ones an agent's lookup tool uses cannot drift apart. A drift there would be uniquely nasty, because an agent would confidently tell a customer that a serial resolves to nothing while the wizard happily found it a second earlier.
Configuration as the extension point
Almost everything a deployment would want to change is configuration, not C#. The
Policy section carries the return window and grace period, the repair price table,
the warranty tiers, the excluded causes with their match modes, the fraud-indicator prefixes,
the human-review-only regions, and the advisory-only lockdown switch. Adding a tier, a return
window or an excluded cause never requires editing code — which is the property that keeps the
policy testable as data rather than re-testing as a build.
Package versions are pinned centrally in
Directory.Packages.props
with ManagePackageVersionsCentrally, and the shared target framework is
net10.0 from Directory.Build.props. The Microsoft.Extensions.*
pins are held at the same servicing band as the EF Core packages, because a lower pin trips
NU1109 downgrade detection — a small detail that saves an afternoon the first time it happens.
Getting it running in five minutes
docker compose up -d # PostgreSQL 16 on :5432
export ConnectionStrings__Rma='Host=localhost;Port=5432;Database=rmadev;Username=rmauser;Password=rmapassword'
dotnet run --project tools/DbAdmin -- reset --force # drop schema, migrate, seed
dotnet run --project src/AIEnabledRma.Web # the wizard on http://localhost:5xxx
The checked-in connection string is a placeholder and nothing that touches the database works until a real one is supplied through the environment variable. The seeder gives you a device for every policy outcome — one in warranty, one expired but inside the window that walks the paid repair path for $69.00, one shipped 1,200 days ago that is refused, one with an open return that routes to human review, and one serial that does not exist. Being able to demo every branch of a decision engine in one screen is worth more than any amount of documentation.
Two containers ship as well, for the web app and the MCP server, so the deployable shape is not a configuration exercise left to the reader.
Why this layout, and not something cleverer
The alternative structure — one project, or a project per feature — was rejected for a
specific reason. The whole point of this sample is that the policy layer is provably
separate from the model. A folder boundary would not have demonstrated that; a project boundary
with no dependency from Domain to Ai does, and it fails at compile time
rather than at review time when someone tries to shortcut it.
Next: inside the evaluator — nine ordered rules, a rule trace, and how money is priced.
Repository: github.com/bobhuang1/AIEnabledRMA