fix(ef): SaveOrUpdate via EntityState + per-package versioning + release workflows#10
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resumo
Direct push to
masterfoi 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.EF→6.3.0)SaveOrUpdatedecidia insert vs. update viaIEntity.IsTransient(), o que quebrava qualquer entidade comIdpré-atribuído no construtor (Id = Guid.NewGuid()): a entidade era marcada comoModifiede oSaveChangesfalhava ao tentarUPDATEnuma linha inexistente.Context.Entry(entity).State. Para entidadesDetachedcom chave atribuída, consulta o banco viaFind/FindAsyncpra decidir insert ou update.SaveOrUpdateAsyncpassou a usarFindAsynce respeitar oCancellationTokenrecebido — antes bloqueava a thread noFindsíncrono.Entry(existing).CurrentValues.SetValues(entity), preservandoOriginalValues(e portanto concurrency tokens[Timestamp]/RowVersion).Mudança de comportamento observável:
SaveOrUpdateem entidadeUnchangedjá rastreada não força mais transição praModified(confia no change tracker); e emDetachedcom chave não-default agora há um roundtrip ao banco. Hot paths devem usarSave(entity)ouUpdate(entity)quando a intenção é conhecida.2. Versão per-pacote
<Version>foi removido doDirectory.Build.propse movido pra cada.csprojpublicável. Antes, qualquer bump regerava todos os 28 pacotes; agora cada pacote evolui no próprio ritmo.6.2.2(último valor central estável).Codout.Framework.EF→6.3.0(carrega o fix).Codout.Framework.Mcpmantém seu próprio ciclo (workflow dedicado).short-name → caminho.csprojem.github/release-packages.json.3. Encoding + workflows de release
.editorconfige.gitattributesenforçam UTF-8 + LF daqui pra frente.CLAUDE.mdnovo 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 viasecrets.NUGET_API_KEY. Tagmcp-v*continua indo promcp-release.ymlexistente..github/workflows/mass-release.yml:workflow_dispatchonly, exigeconfirm: PUBLISH, defaultdry_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.0A tag dispara
release.ymlque publicaCodout.Framework.EF 6.3.0no NuGet.org. Os demais 26 pacotes seguem em6.2.2(não publicados) — publicar o baseline depende de dispatch manual domass-release.yml.Commits
ebfa8e9af71cdca204af658234d9<Version>in each csproj8b78bff592e93ee397fce8772ce19b3d549ea5a9b6Test plan
EFRepository.SaveOrUpdate— confirmar que a semântica nova (state-based + Find lookup) atende ao caso de uso originalDirectory.Build.propssem<Version>e cada.csprojpublicável com<Version>próprio (28 csproj tocados).github/release-packages.json— mapeamento dos 27 short-names → csprojrelease.ymlemass-release.ymlno preview/lint do GitHubNUGET_API_KEYestá configurado em Settings → Secretshttps://claude.ai/code/session_015jxScBEvdKkRwGvJTgK5Py
Generated by Claude Code