Skip to content

fix(ef): SaveOrUpdate via EntityState + per-package versioning + release workflows#10

Merged
cloviscoli merged 10 commits into
masterfrom
claude/fix-saveupdate-entity-ids-piGHS
May 12, 2026
Merged

fix(ef): SaveOrUpdate via EntityState + per-package versioning + release workflows#10
cloviscoli merged 10 commits into
masterfrom
claude/fix-saveupdate-entity-ids-piGHS

Conversation

@cloviscoli

Copy link
Copy Markdown
Contributor

Resumo

Direct push to master foi rejeitado pelo branch protection, então abro como PR. 10 commits agrupados em três temas independentes mas relacionados.

1. Correção do EFRepository.SaveOrUpdate (Codout.Framework.EF6.3.0)

SaveOrUpdate decidia insert vs. update via IEntity.IsTransient(), o que quebrava qualquer entidade com Id pré-atribuído no construtor (Id = Guid.NewGuid()): a entidade era marcada como Modified e o SaveChanges falhava ao tentar UPDATE numa linha inexistente.

  • Agora a decisão usa Context.Entry(entity).State. Para entidades Detached com chave atribuída, consulta o banco via Find / FindAsync pra decidir insert ou update.
  • SaveOrUpdateAsync passou a usar FindAsync e respeitar o CancellationToken recebido — antes bloqueava a thread no Find síncrono.
  • Quando precisa atualizar uma entidade existente, copia valores via Entry(existing).CurrentValues.SetValues(entity), preservando OriginalValues (e portanto concurrency tokens [Timestamp]/RowVersion).

Mudança de comportamento observável: SaveOrUpdate em entidade Unchanged já rastreada não força mais transição pra Modified (confia no change tracker); e em Detached com chave não-default agora há um roundtrip ao banco. Hot paths devem usar Save(entity) ou Update(entity) quando a intenção é conhecida.

2. Versão per-pacote

<Version> foi removido do Directory.Build.props e movido pra cada .csproj publicável. Antes, qualquer bump regerava todos os 28 pacotes; agora cada pacote evolui no próprio ritmo.

  • 27 pacotes congelados em 6.2.2 (último valor central estável).
  • Codout.Framework.EF6.3.0 (carrega o fix).
  • Codout.Framework.Mcp mantém seu próprio ciclo (workflow dedicado).
  • Mapeamento short-name → caminho.csproj em .github/release-packages.json.

3. Encoding + workflows de release

  • Repo todo convertido pra UTF-8 (14 arquivos saíram de Windows-1252/ISO-8859-1). .editorconfig e .gitattributes enforçam UTF-8 + LF daqui pra frente.
  • CLAUDE.md novo no root com a premissa de encoding e o playbook completo de release (CLAUDE e outros agentes seguem a partir dele).
  • .github/workflows/release.yml: workflow tag-driven, um pacote por tag (<short>-v<X.Y.Z>), build + test da solution + master-ancestry gate + push pro NuGet.org via secrets.NUGET_API_KEY. Tag mcp-v* continua indo pro mcp-release.yml existente.
  • .github/workflows/mass-release.yml: workflow_dispatch only, exige confirm: PUBLISH, default dry_run: true, opt-in pro Mcp. Idempotente via --skip-duplicate. Pensado pra publicar o baseline 6.2.2 dos 26 outros pacotes de uma vez, se/quando desejado.

Próximo passo após merge

Após este PR entrar em master, o release do EF é:

git fetch origin master
git tag ef-v6.3.0 origin/master   # confirmar SHA antes
git push origin ef-v6.3.0

A tag dispara release.yml que publica Codout.Framework.EF 6.3.0 no NuGet.org. Os demais 26 pacotes seguem em 6.2.2 (não publicados) — publicar o baseline depende de dispatch manual do mass-release.yml.

Commits

SHA Subject
ebfa8e9 fix: SaveOrUpdate inspects EntityState instead of IsTransient
af71cdc fix(ef): make SaveOrUpdateAsync truly async and preserve concurrency tokens
a204af6 chore: bump 6.3.0 and document EFRepository.SaveOrUpdate fix
58234d9 chore: individualize per-package <Version> in each csproj
8b78bff chore: normalize all source/doc files to UTF-8
592e93e chore: enforce UTF-8 + LF via .editorconfig and .gitattributes
e397fce docs: add CLAUDE.md with UTF-8 directive for AI agents
8772ce1 ci: add generic per-package release workflow
9b3d549 ci: harden release.yml and add mass-release.yml
ea5a9b6 docs: prescribe the release flow for AI agents in CLAUDE.md

Test plan

  • Revisar o diff do EFRepository.SaveOrUpdate — confirmar que a semântica nova (state-based + Find lookup) atende ao caso de uso original
  • Confirmar Directory.Build.props sem <Version> e cada .csproj publicável com <Version> próprio (28 csproj tocados)
  • Conferir .github/release-packages.json — mapeamento dos 27 short-names → csproj
  • Validar workflows release.yml e mass-release.yml no preview/lint do GitHub
  • Após merge, confirmar que NUGET_API_KEY está configurado em Settings → Secrets

https://claude.ai/code/session_015jxScBEvdKkRwGvJTgK5Py


Generated by Claude Code

claude added 10 commits May 12, 2026 15:45
IEntity.IsTransient() returns false whenever the Id is non-default, which
breaks SaveOrUpdate for any entity whose constructor pre-assigns the key
(e.g. Id = Guid.NewGuid()): the entity is marked Modified and SaveChanges
attempts to UPDATE a row that does not exist.

Now SaveOrUpdate looks at Context.Entry(entity).State first - if already
tracked, leave it alone - and for detached entities decides insert vs.
update from the primary key values and a Find() lookup, so pre-assigned
Ids on brand-new entities are inserted correctly.

https://claude.ai/code/session_015jxScBEvdKkRwGvJTgK5Py
…tokens

Follow-up to the previous SaveOrUpdate fix:

- SaveOrUpdateAsync no longer calls the sync path. It uses DbSet.FindAsync
  with the supplied CancellationToken instead of blocking on Find().
- When Find/FindAsync returns an existing tracked instance for a detached
  entity, copy values via Entry(existing).CurrentValues.SetValues(entity)
  instead of detaching `existing` and forcing Modified on the caller's
  instance. This preserves OriginalValues (concurrency tokens like
  [Timestamp]/RowVersion) so EF generates the correct WHERE clause.
- Extract the shared decision logic into TryResolveTrackedOrAdd /
  ApplySaveOrUpdate so sync and async paths stay in lockstep.

https://claude.ai/code/session_015jxScBEvdKkRwGvJTgK5Py
- Bump Version to 6.3.0 (centralized in Directory.Build.props).
- Convert CHANGELOG.md from Windows-1252 to UTF-8 (continuation of
  fcb1ad1's encoding cleanup) and add 6.3.0 entry covering the
  SaveOrUpdate / SaveOrUpdateAsync fixes and the resulting behavior
  changes for already-tracked and detached-with-key entities.

https://claude.ai/code/session_015jxScBEvdKkRwGvJTgK5Py
Previously <Version> was centralized in Directory.Build.props, so any
bump shipped a new build of every package even when only one had real
changes. Each .csproj now owns its own <Version> and pacakges can move
independently.

- Removed <Version> from Directory.Build.props.
- Added <Version>6.2.2</Version> to every packable .csproj as the
  baseline (the last centralized stable version before the EF fix).
- Codout.Framework.EF goes to 6.3.0 to ship the SaveOrUpdate fix.
- Test projects, .shproj shared projects, and the legacy NetFull
  non-SDK projects were left untouched.
- CHANGELOG entry reorganized by package/version instead of a single
  framework-wide header.

https://claude.ai/code/session_015jxScBEvdKkRwGvJTgK5Py
Converted 14 files from Windows-1252/ISO-8859-1 to UTF-8 so that the
repository has a single, consistent encoding. The change is encoding-
only (same line counts); diacritics that previously rendered as mojibake
(e.g. "m�ltiplos", "tempor�rio") now decode correctly as "múltiplos",
"temporário", "abstração", etc.

Files: a mix of .cs, .csproj, and README/guide .md across
Codout.Framework.Data, .EF, .Mongo, .NH, .Storage, Codout.Mailer, and
Codout.Mailer.Razor.

Note: a few '?' placeholders that remained in Storage/README.md were
already present before this conversion (emojis lost in a prior
encoding round-trip) and are out of scope here.

https://claude.ai/code/session_015jxScBEvdKkRwGvJTgK5Py
Prevent regressing back to Windows-1252/ISO-8859-1:

- New .editorconfig with charset = utf-8 and end_of_line = lf so
  Visual Studio, VS Code, Rider, and other EditorConfig-aware editors
  save new and edited files as UTF-8 with LF endings by default.
  (.cmd/.bat files keep CRLF.)
- .gitattributes default rule changed from "* text=auto" to
  "* text=auto eol=lf" so Git normalizes line endings on commit.
  Note: .gitattributes does not enforce charset on its own
  (working-tree-encoding=UTF-8 would be a no-op); the .editorconfig
  is the real lever.

Follow-up option: a CI step running "file -i" against staged text
files to fail the build on non-UTF-8 content.

https://claude.ai/code/session_015jxScBEvdKkRwGvJTgK5Py
Single-rule instruction file at the repo root telling Claude (and other
AI assistants that pick up CLAUDE.md / AGENTS.md style files) to always
create and edit files as UTF-8 without BOM. Mirrors the .editorconfig
convention so the policy is discoverable by both humans and AIs.

https://claude.ai/code/session_015jxScBEvdKkRwGvJTgK5Py
Tag-driven NuGet release that pairs with the per-package <Version>
introduced in 58234d9. Pushing a tag in the form "<short>-v<X.Y.Z>"
(e.g. "ef-v6.3.0", "mongo-v6.2.3") builds, packs, and publishes only
that one package to NuGet.org. Short-name to .csproj mapping lives in
.github/release-packages.json (27 packages).

Behavior:
- push tag "<short>-v*.*.*": build + pack + push to NuGet.org with
  --skip-duplicate using secrets.NUGET_API_KEY.
- workflow_dispatch: build + pack only (no publish), useful as a smoke
  test before tagging.
- "mcp-v*" tags are excluded so they keep flowing through the existing
  mcp-release.yml that has the Mcp-specific --validate step.
- An explicit "-p:Version=" override is honored when supplied; otherwise
  the version baked into the .csproj wins.

Optional version-from-tag override mirrors the pattern already used by
mcp-release.yml.

https://claude.ai/code/session_015jxScBEvdKkRwGvJTgK5Py
Applied the three follow-ups suggested in the previous turn:

release.yml
- Build + test the solution before pack, as a safety net for cross-
  package regressions. dotnet test today is effectively a no-op (the
  legacy NetCore tests are not in the solution and need SQL Server +
  MongoDB, so are not CI-friendly), but the hook is in place for when
  hermetic tests are added.
- Gate releases to commits that are ancestors of origin/master, so a
  tag accidentally cut from a feature branch fails fast.
- checkout uses fetch-depth: 0 to make the ancestry check possible.
- Dropped --no-build from the Pack step so out-of-solution projects
  (Cosmos, DocumentDB, etc.) can still be packed.

mass-release.yml (new)
- workflow_dispatch-only, requires typing PUBLISH to confirm.
- Defaults to dry_run = true; flip to false to actually push.
- Includes the same master-ancestry gate.
- Iterates over .github/release-packages.json (27 packages) and packs
  each at its currently pinned <Version>. Codout.Framework.Mcp is
  excluded by default; opt-in via include_mcp = true.
- --skip-duplicate makes the push idempotent and re-runnable after a
  partial failure.
- Writes a step summary listing artifacts.

Designed for the one-shot baseline release of the per-package versions
(all packages at 6.2.2 except EF at 6.3.0); afterwards every package
moves on its own tag-driven schedule via release.yml.

https://claude.ai/code/session_015jxScBEvdKkRwGvJTgK5Py
Extend CLAUDE.md so that "gera nova versão e publica o pacote X" is
self-contained for any agent picking up the repo. Documents:

- Where <Version> lives now (per-csproj, not Directory.Build.props) and
  why not to reintroduce it centrally.
- The seven-step release flow: resolve package + bump, edit only the
  target csproj, update CHANGELOG under today's date in the per-package
  format, conventional-commits commit, push, master-ancestry check,
  tag + push tag (the actual publish trigger).
- Mcp uses its own workflow / tag prefix; mass-release is opt-in only
  via the Actions UI.
- Explicit "do not" list covering the common slip-ups: reintroducing
  central Version, bumping unrelated packages, tagging without an
  explicit publish intent, dispatching mass-release autonomously,
  guessing bump level on ambiguous changes.

https://claude.ai/code/session_015jxScBEvdKkRwGvJTgK5Py
@cloviscoli cloviscoli merged commit c90afc5 into master May 12, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants