Imported from awingrove/VitaTrack (
VitaTrack.Core/AGENTS.md). Install upstream withnpx skills add awingrove/VitaTrack --skill VitaTrack.Core. Copyright stays with the author.
VitaTrack.Core – Data & Service Layer
Responsibilities
- Persist data using Dapper over SQLite.
- Define repository interfaces (
IFamilyRepository,ISupplementRepository,ISupplementNutrientRepository,IPrescribedDoseRepository). - Implement repositories with async CRUD methods.
- Provide access to external services (LLM) via
ILlmService. - Contain models used across layers; every slice owns its models under
Features/<Slice>/(Supplement,FamilyMember,SupplementNutrient,PrescribedDose,LlmResult, report records) —VitaTrack.Core/Modelsand the flatVitaTrack.Core/Servicesnamespace are retired. - No direct HTTP or UI concerns; keep pure C#.
Conventions
- Interfaces: prefix
I, co-located with their implementation — feature slices own theirs underVitaTrack.Core/Features/<Slice>/(ADR-0006); shared infrastructure ones live inVitaTrack.Core.DataorVitaTrack.Core.Services. - Implementations: suffix
RepositoryorService, same namespace. - Models: plain POCOs with public get/set; default string values
string.Empty. Feature-owned models live in their slice folder, notVitaTrack.Core/Models. - Constructor injection: receive
IDbConnection(repositories) orHttpClient+IConfiguration(LLM service). - All I/O methods are
asyncand returnTask<T>orTask<IReadOnlyList<T>>. - Use
await _db.QueryAsync<T>(sql)for reads. - Use
await _db.ExecuteAsync(sql, param)for writes. - For inserts returning identity, execute
INSERTthenSELECT last_insert_rowid()as two separate calls (SQLite limitation).
Foreign Key Delete Order
SQLite enforces foreign keys. When implementing DeleteAsync for a parent table, always delete child rows first. The current dependency chain is:
SupplementNutrients → Supplements
PrescribedDoses → Supplements
PrescribedDoses → FamilyMembers
When deleting a Supplement, delete in this order:
DELETE FROM SupplementNutrients WHERE SupplementId = @IdDELETE FROM PrescribedDoses WHERE SupplementId = @IdDELETE FROM Supplements WHERE Id = @Id
Cross-slice deletes are routed via the owning slice's repository (ADR-0006 cross-slice invariant). SupplementRepository.DeleteAsync (both overloads) no longer issues that raw SQL itself — it calls ISupplementNutrientRepository.DeleteBySupplementIdsAsync and IPrescribedDoseRepository.DeleteBySupplementIdsAsync, which implement the same order against their own tables. When a delete cascade crosses a slice boundary, add a bulk-delete method to the owning slice's repository and call it — never write SQL against another slice's table. Repo-to-repo constructor injection is the accepted pragmatic pattern for this.
When deleting a FamilyMember, delete in this order:
DELETE FROM PrescribedDoses WHERE FamilyMemberId = @IdDELETE FROM FamilyMembers WHERE Id = @Id
FamilyRepository.DeleteAsync routes step 1 through IPrescribedDoseRepository.DeleteByFamilyMemberIdsAsync instead of issuing that SQL itself (same cross-slice rule as SupplementRepository below).
Bulk deletes (DeleteAsync(IEnumerable<int> ids)) must follow the same order using WHERE Id IN @Ids.
When deleting a SupplementNutrient that is a blend parent, delete its children first:
DELETE FROM SupplementNutrients WHERE ParentNutrientId = @IdDELETE FROM SupplementNutrients WHERE Id = @Id
SupplementNutrients.ParentNutrientId is a self-reference without a DB constraint (added via ALTER TABLE in DbInit); the cascade above is app-enforced and is the only thing preventing orphaned child rows.
When adding new tables (or FK-like columns, via CREATE TABLE or ALTER TABLE migration in DbInit) with foreign keys, update the relevant DeleteAsync methods in the same change — a migration without its cascade update silently orphans or blocks deletes at runtime. Add cascade-delete unit tests alongside (Delete_Parent_AlsoDeletesChildren pattern).
Transaction Handling
- Currently each repository method opens/closes the connection via Dapper (connection is scoped from Web).
- If multiple operations need a transaction, open a connection and use
IDbTransaction(future work).
Dependencies
- Packages:
Dapper,Microsoft.Data.Sqlite,Microsoft.Extensions.Configuration.Abstractions,Microsoft.Extensions.Http. - No reference to
VitaTrack.Web; only depends on .NET primitives and NuGet.
Testing
- Tests live in
VitaTrack.Tests. - Unit tests must pass before considering a feature complete; the aim of unit testing is to verify that a piece of functionality is defect‑free under the tested conditions.
- Use in‑memory SQLite (
Microsoft.Data.Sqlite) with connection stringData Source=:memory:. - Base class
SqliteTestBasehandles connection creation and schema initialization. - Mock
HttpClient(with Moq) forOpenRouterLlmServicetests.
Adding New Features
- Add model (if needed) to the owning feature slice under
VitaTrack.Core/Features/<Slice>/. - Extend repository interface (if new entity) and implement.
- Register new interface/implementation in
VitaTrack.Web/Program.csviabuilder.Services.AddScoped<...>(). - If external service, add to
VitaTrack.Core.Servicesand register viaAddHttpClient<TInterface, TImplementation>()(you may also need to registerHttpClientseparately if not already). - Write unit tests in
VitaTrack.Testsbefore or after implementation (TDD encouraged).
Build
dotnet build VitaTrack.Core.csproj(or via solution).- No executable produced; it's a class library.
