# efcore shared reference

Load this file whenever you invoke any `efcore` agent. It captures the
invariants that don't change between operations.

## 1. Dual DbContext model

Generated SmartStack apps ship with two contexts:

| Context | Schema | Migration project | Use |
|---------|--------|-------------------|-----|
| `CoreDbContext` | `core` | `{App}.Infrastructure` | SmartStack platform tables |
| `ExtensionsDbContext` | `extensions` | `{App}.Infrastructure` | Client / tenant tables |

When both contexts change, apply Core **before** Extensions (Extensions FKs
reference Core rows).

The Studio itself has a single context (`StudioDbContext`) compiled
against three migration assemblies, one per provider:

| Assembly | Provider env var |
|----------|------------------|
| `StudioDataService.Migrations.Sqlite` | `STUDIO_DESIGN_PROVIDER=Sqlite` |
| `StudioDataService.Migrations.Postgres` | `STUDIO_DESIGN_PROVIDER=Postgres` |
| `StudioDataService.Migrations.SqlServer` | `STUDIO_DESIGN_PROVIDER=SqlServer` |

For the Studio, every `create` / `squash` / `rebase-snapshot` operation
runs three times — one per assembly — with the env var set correctly. The
CLI handles this automatically when `allProviders: true` (default).

## 2. Naming convention — no MCP

```
{contextPrefix}_v{version_underscore}_{seq3}_{PascalCaseDescription}
```

- `contextPrefix` — `CoreDbContext` → `core`, `ExtensionsDbContext` →
  `ext`, `StudioDbContext` → `studio`.
- `version_underscore` — project version with `.` → `_`. Resolution mirrors
  MSBuild inheritance: `<Version>` / `<VersionPrefix>` / `<AssemblyVersion>` /
  `<FileVersion>` from the csproj, then the NEAREST `Directory.Build.props`
  walked up from the project dir (central pipeline-managed versioning — e.g.
  SmartStack.app — keeps the version there, not in each csproj). Pre-release
  suffixes and build metadata are stripped: `0.1.0-dev.3` becomes `0_1_0`.
  Defaults to `0_1_0` if no version tag is found anywhere. The `create` spec
  accepts an optional `version` override for repos whose working line diverges
  from the props value (align with the previous migration's version).
- `seq3` — number of existing migrations in the assembly's migrations
  directory + 1, zero-padded to three digits.
- `PascalCaseDescription` — description stripped to `[A-Za-z0-9]`, split on
  whitespace / `-` / `_`, words capitalized and joined.

Examples:

```
core_v0_1_0_007_AddEmployeeAvatar
ext_v1_0_0_003_CreateOrders
studio_v1_0_0_005_AddBugWorkflow
```

## 3. Environment preflight

### dotnet-ef on PATH (Git Bash on Windows)

The `dotnet-ef` global tool lives under `$USERPROFILE\.dotnet\tools`
(Windows) or `$HOME/.dotnet/tools` (POSIX). Git Bash does not pick these
up by default. The CLI `ef-runner.ts` prepends both paths to `PATH`
automatically. If you're invoking `dotnet ef` manually from a terminal,
run:

```bash
for TOOLS_DIR in "$USERPROFILE/.dotnet/tools" "$HOME/.dotnet/tools"; do
  [ -d "$TOOLS_DIR" ] && export PATH="$TOOLS_DIR:$PATH"
done
```

### WSL git path repair

Worktrees on Windows sometimes store `.git` with a Windows path inside WSL
(`gitdir: C:\...`). When git commands run from WSL this breaks. Before any
`git` command, if the worktree has a `.git` file (not directory) and it
starts with `gitdir: ` + a drive letter, rewrite it:

```bash
if [ -f .git ] && grep -q '^gitdir: [A-Za-z]:' .git; then
  p=$(sed 's/^gitdir: //' .git | tr -d '\r\n')
  printf 'gitdir: %s\n' "$(realpath -m --relative-to="$(pwd)" "/mnt/$(echo ${p:0:1} | tr '[:upper:]' '[:lower:]')${p:2//"\\"/"/}")" > .git
fi
```

The Studio runs on Windows bash, so this rarely matters in practice, but
keep the snippet handy if you hit "not a git repository" errors.

## 4. Install `dotnet-ef`

If `dotnet-ef` is missing:

```bash
dotnet tool install --global dotnet-ef --version 10.0.*
```

EF Core 10 is required to match the Studio sidecar and the apps generated
by SmartStack. The `list` / `status` CLIs both emit a warning when the
tool isn't found; `create` / `squash` / `rebase-snapshot` refuse to run.

## 5. Backups

`cli/squash/` and `cli/rebase-snapshot/` write a backup before deleting
anything. The backup directory is:

```
{cwd}/.efcore-squash-backup/<ISO-timestamp>/<AssemblyName>/<relativePath>
```

If a squash fails, restore by copying files back:

```bash
cp -r .efcore-squash-backup/<timestamp>/<AssemblyName>/* .
```

The `.efcore-squash-backup/` directory is **not** gitignored by default —
add it to `.gitignore` if you want to keep it out of commits.

## 6. Dry-run everything

Every mutating CLI accepts `"dryRun": true` in its spec. The output lists
what WOULD happen without touching the worktree. Use this from the
Studio's MigrationsTab before firing the real command, and inside agent
prompts when you want to preview a destructive step.

## 7. Workflow summary

```
feature branch created
    ↓
agents/create.md  → core_v1.8.0_001_AddUserRoles.cs
    ↓ (commit, push, review)
    ↓ (rebase onto develop — snapshot conflict)
agents/rebase-snapshot.md  → reset snapshot, regenerate one migration
    ↓ (ready to merge)
agents/squash.md  → collapse N branch migrations into 1
    ↓ (merge to develop)
develop
    ↓
agents/db-update.md  → apply pending migrations via cli/apply (3-tier policy: 🟢 local)
```

## 8. SQL objects (functions / views / stored procedures)

A project may keep raw SQL objects — programmable objects that EF Core does
**not** track in its model — as `.sql` files under
`…/Persistence/SqlObjects/**` (e.g. `SqlObjects/Functions/fn_GetUserGroupHierarchy.sql`).
They use `CREATE OR ALTER` so they are idempotent.

`dotnet ef migrations add` never emits them (the model differ doesn't see raw
SQL). To guarantee that a plain `dotnet ef database update` deploys them — in CI,
integration tests, and any pipeline that runs migrations **without** booting the
app — `create` and `squash` **freeze each new/changed `.sql` into the generated
migration** as a `migrationBuilder.Sql(@"…")` literal in `Up()` plus a
`DROP … IF EXISTS` in `Down()`. Detection is by glob under the assembly's own
project dir — **no project/assembly name is hardcoded**, so a client renaming the
Infrastructure project still works.

Why a frozen literal and **not** a runtime `SqlObjectHelper.ApplyAll(migrationBuilder)`
call: a migration that reads the embedded `.sql` at run time is non-deterministic
and breaks ordering (an early migration would apply *today's* SQL, which may
reference a table a *later* migration creates → `database update` fails with
"Invalid object name"). A literal frozen at creation time is deterministic and
correctly ordered.

Each `create`/`squash` assembly result carries `sqlObjectsInlined: string[]`
(the `schema.name` objects frozen into that migration). **Surface it to the
user.** Only new/changed objects are inlined (idempotent — an unchanged object
stays in the migration that already carries it). The `.sql` files remain the
single source of truth; the app's startup re-apply, if any, stays as the runtime
safety net while the migration is the deployment path.
