Skip to content

Decouple the designer from Visual Studio and ship a headless EDMX toolchain - #3

Merged
robertmclaws merged 87 commits into
mainfrom
ai-tools
Sep 2, 2026
Merged

Decouple the designer from Visual Studio and ship a headless EDMX toolchain#3
robertmclaws merged 87 commits into
mainfrom
ai-tools

Conversation

@robertmclaws

Copy link
Copy Markdown
Collaborator

Takes the EF6 entity designer from a Visual Studio–only extension to a layered codebase whose designer runs without a shell, and ships that capability as a cross-target library and a standalone dotnet global tool.

Headless rendering and the dotnet edmx CLI

  • Renders EDMX diagrams to SVG, PNG, JPG, BMP, GIF, TIFF and Mermaid with no Visual Studio running.
  • New EasyAF.Edmx.DiagramTools package installs as a global tool: dotnet tool install --global EasyAF.Edmx.DiagramTools, invoked as dotnet edmx render.
  • Mermaid output emits erDiagram with entities, cardinality and foreign key names — a text form of the model suited to diffing, docs and AI consumption.
  • Options for diagram selection, output format and path, property data types, and transparent backgrounds.

The designer no longer needs the shell

  • Microsoft.Data.Entity.Design.Dsl has no project reference to the Visual Studio layer, and no file in it touches PackageManager, VsUtils, VSColorTheme, DiagramView or any dialog type.
  • Load path, save path, navigation and theming were inverted so only the package talks to Visual Studio; designer dialogs became typed events with no-op defaults.
  • A four-layer map (Foundation → Designer → Headless consumers → Shell) is documented in specs/layer-map.md and encoded in the solution folders, so every platform dependency now has one legitimate home.

Diagram layout

  • MSAGL layout engine added alongside the original DSL layout, selectable at runtime through a container rather than hard-wired at the call site.
  • Advanced Layout toggle in the floating toolbar, plus connector routing controls: routing mode, per-connector redraw, and opt-in grouping.
  • Layout is also exposed as its own CLI command with a pinnable algorithm choice.

Modernization

  • Projects multi-target net48 and net10.0 (plus netstandard2.0/2.1 where appropriate); every project now declares its own TargetFrameworks.
  • Windows Workflow database generation replaced with a direct pipeline.
  • ObjectContext code generation removed and VersioningFacade freed from the GAC.
  • Extensibility system overhauled without breaking existing extension authors.
  • WPF views moved out of the core XML engine, which now targets netstandard2.0 and net10.0.

Naming and structure

  • EntityDesignerDsl, XmlCoreXmlEngine, and DesignMicrosoft.VisualStudio.Data.Entity.Design, with namespaces, generated code and string references carried through.
  • Escher-era type names retired; one class per file; every project's resource class given a unique name with strings moved to their consumer.

Packages and security

  • All package vulnerability advisories cleared; dotnet list package --vulnerable --include-transitive is clean across the solution.
  • .NET runtime libraries moved to a single 10.0.10 baseline shared by the net48 and net10.0 targets, chosen to match the devenv.exe.config binding-redirect ceiling so the VSIX unifies on the assemblies Visual Studio already loads.
  • Azure.Identity moved off the deprecated line; MSTest 4, Microsoft.NET.Test.Sdk and Microsoft.SourceLink.GitHub pins removed as redundant.
  • PackageSourceMapping completed for every previously unroutable package.

Build and release

  • CI publishes to the VS Marketplace and NuGet in parallel, using NuGet Trusted Publishing (OIDC) rather than a stored API key.
  • The CLI versions independently of the extension — the VSIX major tracks the Visual Studio version, which carries no meaning for a command line tool — and its patch version auto-increments after each successful publish.
  • Workflow action versions and the .NET SDK version brought current; stale VSIX project paths corrected and VSIX discovery scoped to the build configuration output.

Fixes

  • Designer no longer crashes when reopening a solution with an EDMX already open.
  • ModelWizard and PackageResources resource lookups corrected (MissingManifestResourceException).
  • EnsureSqlClientRegistered no longer publishes its guard before completing the work.
  • 17 tests that never ran (marked static) and 8 stray [TestMethod] attributes on private helpers corrected; a Generate_returns_code flake fixed.

🤖 Generated with Claude Code

robertmclaws and others added 30 commits August 10, 2026 03:44
- Add Microsoft.Data.Entity.Design.Renderer with the exporters moved out
  of EntityDesigner, plus a headless host that builds and routes a
  diagram with no Visual Studio shell
- Add Microsoft.Data.Entity.Tools: edmx render, writing SVG and Mermaid
  and rasterising the SVG for image formats
- Load EDMX without the shell: own artifact factory that picks up a
  sibling .edmx.diagram, and a render-safety check that does not require
  the model's ADO.NET provider to be installed
- Split raster export behind IRasterExporter so Visual Studio keeps
  Diagram.CreateBitmap and other hosts supply their own
- Skip the package model manager subscription when no shell is loaded,
  via PackageManager.IsLoaded rather than a null check that asserts
- Add Microsoft.Data.Entity.Tests.Design.Renderer covering the loader,
  the exporters and headless routing

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Documentation and structure:
- Document every public type and member, with worked examples on the
  interfaces and MEF attributes an extension author implements
- One type per file, formatted to the repo's conventions, and rename two
  files that never contained the type they were named after
- Add a README covering the extension points, MEF discovery, a complete
  worked example, and the known gaps that remain
- Retarget to netstandard2.0 and net10.0, dropping the unused WPF
  dependency; the VSIX now resolves it through the ProjectReference

Fixes, none of which change a public signature:
- Throw ArgumentNullException rather than ArgumentException for null
  EntityDesignerCommand arguments
- Reject a null command name at the setter, so GetHashCode can no longer
  throw once the command is in a hash-based collection
- Require a message on ExtensionError, and clamp the -1 "no position"
  sentinel where it reaches the Visual Studio TextSpan instead of
  navigating the Error List to a negative offset
- Only an Error severity discards a model transform; warnings and
  messages are reported and the extension's work is kept
- Log to the activity log when malformed XML skips the extension
  pipeline, instead of silently running nothing
- Detect duplicate conversion extensions before running any of them, so
  a conflict fails cleanly and named rather than silently picking
  whichever MEF enumerated first

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nd net10.0

- Move 22 view files to Microsoft.VisualStudio.Data.Tools.Design.XmlCore,
  where every consumer of them already had a reference: the Explorer
  views, the WPF commands and value converters, the editable content
  controls, LoadingUI, and the UITypeEditor that was sitting among the
  view models
- Repoint the three XAML files that named the old assembly on their
  clr-namespace declarations
- Drop UseWPF and UseWindowsForms: no file in XmlCore references
  System.Windows any more
- Target netstandard2.0 and net10.0, with per-framework output so the two
  do not overwrite each other. netstandard2.0 covers the .NET Framework
  consumers and stops the layer quietly reacquiring a desktop dependency
- Let the VSIX resolve XmlCore through its ProjectReference rather than a
  hardcoded flat path, which no longer exists

What is left behind is the object model, the editing context and the
Explorer and property window view models - no UI framework of any kind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… GAC

DbContext is what people use, so the designer no longer generates
ObjectContext classes. The single file generator emits no classes for any
strategy now: DbContext generation produces a context plus a file per
entity type, which a single file generator cannot express, so it is
delivered by the EF 6.x DbContext Generator item template that already
ships here - the same way Entity Framework shipped it.

- Delete VersioningFacade/LegacyCodegen and LegacyCodeGenerationDriver,
  which wrapped System.Data.Entity.Design from the .NET Framework's in-box
  assembly, along with their tests
- Drop all four GAC references from VersioningFacade
- Keep LanguageOption, which was only filed under LegacyCodegen by
  accident, with explicit values instead of ones aliased to the GAC enum
- Point the provider services probe at Entity Framework 6's
  DbProviderServices rather than the pre-EF6 type of the same name. This
  changes which providers it recognises, which is the intent
- Target net48, netstandard2.1 and net10.0. netstandard2.x cannot stand
  alone: EF 6.5 ships no netstandard2.0 asset, and .NET Framework has
  never been able to consume netstandard2.1
- Move CopyPasteUtils to the package. Wrapping the Windows clipboard, it
  was the only file in Model with a UI dependency
- Raise System.Text.Json to 9.0.0, which is what let the provider
  packages resolve for the non net48 targets

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Target net48, netstandard2.1 and net10.0, and drop UseWindowsForms.
  The clipboard helper that needed it moved to the package earlier, so
  nothing in Model referenced a UI framework any more
- Delete HostContext, the mutable static that let Model call up into the
  package to log an Update Model from Database warning. The callback is
  now passed to UpdateModelFromDatabaseCommand and through to
  UpdateConceptualAndMappingModelsCommand, so the dependency is visible
  to the compiler at every hop and a host that does not supply one is
  making a choice rather than forgetting a static
- Let the VSIX resolve Model through its ProjectReference rather than a
  hardcoded flat path, which no longer exists

Behaviour is unchanged: the same ErrorListHelper method receives the same
warning and writes to the same error list. Both new parameters are
optional, so no other caller of these commands needed to change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
System.Activities has no supported .NET Core successor, so it pinned both
DatabaseGeneration and Design to net48. The workflow it bought was a two-step
Sequence deserialized from TablePerTypeStrategy.xaml - infer SSDL and MSL from
the CSDL, then render DDL from the SSDL - which a method expresses directly.

- Add DatabaseScriptGenerator, replacing the XAML activity graph, and
  DatabaseScript to carry its SSDL/MSL/DDL output.
- Replace IGenerateActivityOutput with ISchemaGenerator, dropping the
  NativeActivityContext and inputs dictionary the generators only ever used
  for parameter lookup. CsdlToSsdl and CsdlToMsl now take their inputs
  directly; both had a dead _activity field.
- Add IDdlGenerator so the VS-hosted T4 step is injected rather than
  referenced, keeping DatabaseGeneration free of any Visual Studio dependency.
  TemplateActivity and SsdlToDdlActivity become TemplateProcessor and
  SsdlToDdlGenerator in Design.
- Run generation on the thread pool in WizardPageDbGenSummary, which the
  workflow runtime used to do implicitly, and marshal results back through
  the existing SynchronizationContext.
- Retarget DatabaseGeneration to net48;netstandard2.1;net10.0.

The DatabaseGenerationWorkflow designer property and its file-list converter
are removed, since they selected a .xaml graph that no longer exists. Existing
.edmx files carrying the property still round-trip: SafeGetDesignerProperty
ignores properties nothing reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"EntityDesigner" read like the designer feature as a whole, when the project
is specifically the DSL surface: DslDefinition.dsl, the 14 files generated
from it, and the Modeling SDK Store that mirrors the EDMX for display.

Renames the project and test project folders and .csproj files. The shipping
assembly keeps AssemblyName and RootNamespace of
Microsoft.Data.Entity.Design.EntityDesigner - the DomainModel registration,
VSIX, and generated code all bind to that name. The test assembly has no such
constraint, so it is renamed to Microsoft.Data.Entity.Tests.Design.Dsl along
with its namespaces.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes the project rename by changing AssemblyName, so the shipped binary
matches what the project is.

- AssemblyName, Title, and Description on the Dsl project.
- Four InternalsVisibleTo grants naming the old assembly.
- ExportDiagramDialog.xaml, which resolves the Properties namespace with an
  explicit assembly= qualifier.
- Eight PkgDefData entries registering toolbox tabs, items, and bitmaps by
  DLL filename, plus the VSIXSourceItem flat path in the Package project.
  That path is not derived from the ProjectReference, so it would have kept
  packaging a file that no longer exists.
- The .lci and ten .lcl localization assets, which are named for the DLL.

RootNamespace stays Microsoft.Data.Entity.Design.EntityDesigner. Manifest
resource names derive from it, and the generated DSL code addresses
DomainModelResx by that name, so changing it would break resource lookup for
no benefit. Renaming the namespaces is a separate change across 148 files.

Verified by wiping bin/obj and confirming the rebuilt VSIX contains
Microsoft.Data.Entity.Design.Dsl.dll.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes the rename so namespace, assembly, and project all agree.

Replaces the fully-qualified namespace across 178 files, including the 20
Namespace attributes in DslDefinition.dsl and the 14 .tt templates. Those are
the generation inputs, so leaving them would have reverted the .cs files on
the next regeneration.

Also fixes ten relative references in the Package project, which wrote bare
EntityDesigner.ViewModel and EntityDesigner.Utils resolved against the
enclosing Microsoft.Data.Entity.Design namespace. A fully-qualified
find/replace cannot see those; the compiler caught them.

EntityDesigner.ctmenu is left alone. It is a VS menu resource name paired
with an entry in the pkgdef, not a namespace.

Verified beyond the compiler, which cannot catch a broken resource lookup:
reflected over the rebuilt assembly to confirm the manifest resource names
match what the generated DisplayNameResource and DescriptionResource
attributes reference, and that every non-empty key in DomainModelResx
resolves. The 17 that return empty are empty in the .resx by design.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Puts Visual Studio at the top of the name, matching the existing
Microsoft.VisualStudio.Data.Tools.Design.XmlCore sibling. This assembly
carries 24 Visual Studio package references - more than the VSIX itself -
so the old name read like a core layer while being the most shell-coupled
assembly in the tree.

Renames the project folder, .csproj, and AssemblyName, and updates the six
ProjectReference paths, six InternalsVisibleTo grants, the pkgdef CodeBase
entry, the VSIXSourceItem flat path, ResourcesHelper's VS-install probe, and
eleven localization assets.

Namespaces are deliberately left alone; see the follow-up note in the commit
body of any future namespace change. Attempting the namespace rename produced
2268 compiler errors, because namespaces in this repo are organised by
feature while assemblies are organised by dependency layer, and the two
cross-cut on purpose. Microsoft.Data.Entity.Design.UI.Views.Explorer is
declared by three assemblies; Model, Model.Commands, Model.Validation,
Model.Integrity and others are split between Model and XmlCore. Moving one
assembly's namespaces is not a rename but a decision to abandon that scheme,
and it touches four assemblies at once.

Verified from a wiped bin/obj: the rebuilt VSIX contains
Microsoft.VisualStudio.Data.Entity.Design.dll.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The headless rendering path no longer needs to be .NET Framework only. Both
projects now produce net48 and net10.0 assemblies.

Also maps system.diagnostics.performancecounter to nuget.org. It arrives as a
transitive dependency only on the net10.0 graph, and PackageSourceMapping was
refusing to restore it.

Note that the Renderer test project still targets net48 only, so the net10.0
assemblies compile and lay down their dependencies but are not yet exercised
at runtime.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…build

The in-flight rename left the tree unbuildable. Every failure traced to one of
three causes, all fallout from moving types out of Microsoft.Data.Entity.Design.*:

- Stale using directives kept alongside their replacements (CS0234).
- Folder namespaces named after types used in the same subtree (CS0118). C#
  resolves members of an enclosing namespace, including child namespaces,
  before any using directive or alias, so such a namespace shadows the type
  for the whole subtree and its parent and no alias can fix it. Pluralized the
  colliding leaves - NavigationProperties, Associations, Functions,
  FunctionImports - plus Properties, Types and Tables, which had not fired yet
  but carry the same trap. Folders and namespaces still agree everywhere.
- Names that used to bind through the enclosing namespace and no longer do
  (CS0104/CS0103): Resources, ModelChangeEventArgs, Services.ServiceProvider,
  Design.Resources. Made those bindings explicit.

The T4 templates under CodeGeneration/Generators/GeneratedCode still imported
the pre-rename namespace. Their import directives are what become the usings in
the generated output, so the .tt files and their .cs are updated together and a
regeneration will not undo the fix.

Also fixes two pre-existing runtime failures, both found by running the tool
rather than by building it:

- edmx render was dead on net48. With no PlatformTarget, an AnyCPU exe defaults
  to Prefer32Bit, launches 32 bit, and cannot load the GraphObject layout
  engine's x64 native dependency. Set PlatformTarget=x64.
- Renderer and Tools targeted net10.0 rather than net10.0-windows, leaving
  WinForms out of the framework reference set.

NuGet.config carried no package source mapping for the .NET 10 targeting and
runtime packs, so the net10.0 target could not restore an apphost.

Verified: the solution builds clean from scratch, and
`edmx render Northwind.edmx` produces byte-identical SVG - 51 rects,
36 connector paths, 152 text elements.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds specs/dsl-shell-decoupling.md: the target layering, the naming scheme,
the three mechanisms that do the decoupling, an inventory of what moves, and
a sequence where each step keeps the tree green.

The end state is a single check: Microsoft.Data.Entity.Design.Dsl.csproj holds
no ProjectReference to Microsoft.VisualStudio.Data.Entity.Design.csproj. That
edge exists today and points the wrong way.

Also corrects headless-edmx-rendering.md, which claimed the net472-only
Modeling SDK pinned the renderer and everything downstream to net48, and that
a real dotnet tool would need a net10 front end shelling out to a net48
worker. Both are false: .NET 10 references and calls .NET Framework assemblies
on Windows. Records the two things that do bite - the desktop TFM requirement
and the x64 native dependency in GraphObject - and the one genuinely open
problem, a VS SDK dependency resolving to a reference assembly under net10.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Regenerating GeneratedCode\*.cs from DslDefinition.dsl no longer requires
saving each .tt inside Visual Studio. Wires up the four things the transform
needs outside the IDE:

- the Microsoft.TextTemplating.targets import, which supplies TransformAll
- a DirectiveProcessor item naming DslDirectiveProcessor, which resolves the
  `Dsl` directive that reads DslDefinition.dsl. It is registered in Visual
  Studio's private registry hive, so a command line transform has to name the
  type and assembly explicitly
- IncludeFolders, so the nested Dsl\*.tt includes resolve
- VsIdePath, without which the targets do not add PrivateAssemblies to the
  transform's reference path and the directive processor cannot load the
  Modeling SDK

TransformOnBuild is deliberately left off. The generated code is checked in and
a plain build must not depend on the DSL SDK being present.

DslTemplatesSrc has to be a real environment variable: the template host
expands it inside the include directives, and MSBuild's property function
allowlist blocks System.Environment::SetEnvironmentVariable. A guard target
fails fast with the expected path when it is unset, because the transform task
overwrites its outputs before it discovers the includes are unresolvable.

Records in the spec that regeneration currently produces code that does not
compile - the checked-in output predates VS 18's DSL SDK, and the new templates
no longer mark InternalSaveModel virtual. Verified: the guard fires and touches
nothing, and a normal build is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
EntityDesignerDiagram was never a diagram. It is the layout, routing and
hit-testing surface; the object representing an <edmx:Designer><Diagrams>
<Diagram/> entry is Model.Designer.Diagram, which every consumer imports under
the alias ModelDiagram.

Renames the class and everything derived from it - EntityDesignerSurfaceBase,
EntityDesignerSurfaceSerializer, EntityDesignerSurfaceMoniker,
EntityDesignerSurfaceSelectionRules, EntityDesignerSurface_AddRule,
EntityDesignerSurfaceAdd, EntityDesignerSurfaceModelChange - across 49 files,
and renames the five files that carried the old name.

DslDefinition.dsl is updated in the same commit so the definition and the
generated code cannot disagree. Regenerating would otherwise silently undo
this: the code is hand-edited because the installed DSL SDK is 18.0 while the
project builds against 17.10 packages, and regeneration currently produces code
that does not compile. See specs/dsl-toolchain-alignment.md.

Three strings are deliberately left as EntityDesignerDiagram because they are
XML wire format, not type names: ElementName in DslDefinition.dsl, the
serializer's XmlTagName, and the element named in CannotMonikerizeElement.
MonikerElementName keeps its lower camel case spelling for the same reason.
Model.Designer.EntityDesignerDiagramConstant is a different concept and is
untouched.

Also retargets Microsoft.Data.Entity.Tests.Design.Renderer to net10.0-windows
to match the renderer. It was still on plain net10.0, so every test that built
a Store failed to load System.Windows.Forms - 18 failures, now 3.

Verified: solution builds clean, `edmx render Northwind.edmx` is byte-identical
to the baseline (51 rects, 36 connector paths, 152 text elements), DSL tests
pass, renderer tests pass 138/138 on net48 and 135/138 on net10.

The 3 remaining net10 failures are all one call chain - GetGlobalService
resolving Microsoft.VisualStudio.Shell to a reference assembly - reached from
the surface's theming code, which the decoupling moves out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every mutation on EntityDesignerSurface repeated the same four steps: open a
store transaction, tag its context with TransactionOriginatorDiagramId, enlist
a view model change, commit. Five call sites, and forgetting the tag is silent -
the model layer simply stops knowing which diagram a change came from.

Adds three helpers and converts all five sites:

- InDiagramTransaction(name, Action) runs work and commits.
- InDiagramTransaction(name, Func<bool>) commits only when the work reports a
  change. SetEntityShapesExpanded needs this: committing a transaction that
  changed nothing still pushes an entry onto the undo stack, so it returns
  false when no shape was expanded or collapsed.
- ApplyViewModelChange(name, change) for the three dialog driven adds.

Behaviour is unchanged; the raw BeginTransaction and the context tag now appear
exactly once in the file rather than five times.

Adds EntityDesignerSurfaceTransactionTests covering commit, conditional
rollback, the diagram id tag and the null guard. No mocks; the tests build a
real Store.

The tests are marked [DoNotParallelize]. Without it they failed intermittently
- 0, 1 and 2 failures across five runs - because constructing a Store mutates
process wide serializer state through DomainXmlSerializerDirectory
.InternalAddBehavior, so concurrent stores race. Every Store building test class
in this solution already carries that attribute; this one was missing it. Six
consecutive runs are now clean.

Mutation checked: making the wrapper commit unconditionally and drop the
context tag fails exactly the rollback and diagram id tests, with assertion
messages rather than incidental exceptions.

Verified: solution builds clean from scratch, `edmx render Northwind.edmx` is
byte-identical to the baseline, and the suite is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six WinForms wizard pages threw MissingManifestResourceException at runtime,
which surfaced as four failing tests once the solution started compiling again.

Renaming the project in 6608e52 changed its RootNamespace from
Microsoft.Data.Entity.Design to Microsoft.VisualStudio.Data.Entity.Design.
Manifest resource names are derived from RootNamespace plus folder path, so the
resx files moved to the new prefix while the form classes kept the old
namespace. ComponentResourceManager looks its resources up by the type's full
name, so the two no longer met:

  embedded   Microsoft.VisualStudio.Data.Entity.Design.VisualStudio
             .ModelWizard.gui.WizardPageDbConfig.resources
  requested  Microsoft.Data.Entity.Design.VisualStudio
             .ModelWizard.Gui.WizardPageDbConfig.resources

Wrong prefix and wrong casing, because the folders are lower case while the
namespaces were Pascal case.

Finishes that rename for the ModelWizard tree and makes folders and namespaces
agree: gui and engine are renamed to Gui and Engine, and every namespace under
them moves to the Microsoft.VisualStudio.* prefix. The embedded names now match
the type names exactly, so lookup resolves without any LogicalName overrides.

Moving those files out of Microsoft.Data.Entity.Design.* cost them their
enclosing namespace bindings, the same way the earlier CS0104 wave did, so
VSHelpers, NativeMethods, VSFileFinder and Resources are now imported or
aliased explicitly where they were previously found by walking outwards.

Verified: solution builds clean from scratch, `edmx render Northwind.edmx` is
byte-identical, and the Package suite goes from 14 failures to 10.

The remaining 10 are a different problem and predate this: they construct real
WinForms wizard pages, and VS's DpiHelper needs IVsSettingsManager from a live
shell. Fixing the resources only moved their failure later, from
InitializeComponent to DpiHelper. They need a VS host to run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The solution compiles with 0 errors, but 14 tests fail, 203 never run, and the
build emits 270 warnings including four packages with known vulnerabilities.
specs/known-issues.md records each one with its evidence, cause where known,
and a suggested order of attack.

Two findings worth calling out. 17 tests are declared static, which MSTest
silently refuses to run - they are not even reported as skipped, only as an
MSTEST0003 warning. And 186 tests are disabled with [Ignore], almost all of
them saying the same thing in thirteen different spellings: the locally built
EF6 assemblies expose different member visibility than the shipped ones. That
is 39% of the VersioningFacade suite.

Two entries are expected to close as a side effect of the DSL/shell decoupling
rather than needing their own work: the three net10 renderer failures and the
net10 edmx render tool, which share one call chain through the surface's
theming code into Microsoft.VisualStudio.Shell.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
16 advisories across 4 packages, two of them high severity. Restore and build
now emit zero NU1901, NU1902 and NU1903, and total build warnings drop from 270
to 192.

  MessagePack                2.5.187 -> 2.5.302   11 advisories, 2 high
  Npgsql                     4.1.3   -> 4.1.14     1 advisory,   1 high
  Azure.Identity             1.10.3  -> 1.16.0     2 advisories
  Microsoft.Identity.Client  4.56.0  -> 4.87.0     2 advisories

All four are transitive, so these are PackageVersion entries relying on
CentralPackageTransitivePinningEnabled, which was already on. Each is held
inside the major line its consumer binds to, so no API surface moves:

- MessagePack stays on 2.x. StreamJsonRpc and the rest of the VS SDK bind to
  2.x and 3.x is a breaking change.
- Npgsql stays on 4.1.x. EntityFramework6.Npgsql 6.4.3 depends on Npgsql 4.1.3
  and 5.x would break the EF6 provider. 4.1.14 is the last of the line.
- Azure.Identity is held at 1.16.0 rather than the latest 1.x. 1.17 and later
  pull Azure.Core 1.53, which needs System.Text.Json 10; that pin is shared
  with the VSIX, and raising it was more churn than this fix warrants. 1.16.0
  lands on Azure.Core 1.47.3, needing only System.Text.Json 8, satisfied by the
  existing 9.0.0 pin.

Verified: restore clean, build clean from scratch, `edmx render Northwind.edmx`
byte-identical, and the test suite unchanged at the same 14 failures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The type was defined twice, in Microsoft.Data.Entity.Tests.Design and again in
Microsoft.Data.Entity.Tests.Design.Package, in the same namespace, with the
projects referencing each other. The two copies were identical apart from a
BOM, using order and a trailing newline. CS0436 drops from 26 to 4.

Deleting the Package copy is all that was needed: Tests.Design.Package already
referenced Tests.Design, which already exposed its internals to it, and the
consuming test classes are in the same namespace.

Not moved to Microsoft.Data.Entity.Tests.Shared as the issue list first
suggested. It depends on MockDTE, which lives in Tests.Design, and Tests.Design
already references Tests.Shared, so moving it would have inverted that edge. It
also cannot be made public, because it returns the internal
ModelBuilderWizardForm.

The 4 remaining CS0436 are Krafs.Publicizer injecting IgnoresAccessChecksToAttribute
into more than one assembly. Left alone on purpose: blanket-suppressing CS0436
would also hide genuine duplicate type mistakes like this one.

Verified: build clean from scratch, 181 warnings down from 192, and the test
suite unchanged at the same 14 failures with the Package suite still passing 49.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AssemblyLoader_passed_WebsiteProject_can_find_correct_paths_to_DLLs threw a
NullReferenceException at the foreach over vsWebSite.References.

The production code was correct. The defect was in MockDTE.CreateVsWebsite:
foreach binds to the strongly typed AssemblyReferences.GetEnumerator(), not the
IEnumerable one, and the helper set up only the latter, so the former returned
null and the loop threw. CreateVsProject2 sets up both, which is why the
project reference test sitting next to it always passed.

Also fixes a latent second defect in the same helper. It passed
Returns(references.GetEnumerator()), evaluating the enumerator once at setup
rather than Returns(() => ...), so any second enumeration would have received
an enumerator that was already exhausted.

Microsoft.Data.Entity.Tests.Design now passes 455 of 481, stable across four
runs. Solution wide failures drop from 14 to 13.

Also records in specs/known-issues.md why the 17 static test methods are
static: they were already static at 721bc51 in 2021, inherited from the EF6
repository as xUnit [Fact] methods. xUnit runs static test methods and MSTest
does not, so the January 2026 conversion to [TestMethod] turned a legal
signature into one MSTest silently skips. They were not made static to hide
failures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Modeling SDK cannot be driven from more than one thread in a process. It
assumes a single threaded host and keeps unsynchronized process wide state in
at least two places:

- DomainXmlSerializerDirectory.InternalAddBehavior, reached from
  CoreDesignSurfaceSerializationHelperBase.InitializeSerialization during store
  construction.
- StoreDiagramMappingData.GetInstance(Store), a static Dictionary keyed by
  Store that DiagramCommittingRule.TransactionCommitting writes to on every
  transaction commit.

Locking store construction is not enough. That was tried and measured: with a
lock around new Store(...) and parallelism otherwise on, 12 runs gave 7 clean,
one with a single failure, one with two, and one torn dictionary that crashed
the test host. The commit path still raced, because the state is mutated inside
SDK rules that no care at our call sites can guard.

So the constraint is declared where it actually applies - the whole assembly. A
test project sets UsesDslStore to opt out of the blanket
[Parallelize(MethodLevel)] in Directory.Build.props and receive
[DoNotParallelize] instead. Tests.Design.Dsl and Tests.Design.Renderer set it.

This replaces [DoNotParallelize] on four individual classes. Per class only
works if every future author remembers, and these failures are intermittent
enough to pass review - which is how the hazard survived this long. Everything
that does not touch a Store keeps method level parallelism.

Verified: 12 consecutive runs of the Dsl suite and 8 of the Renderer suite, all
deterministic; build clean from scratch; Northwind SVG byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The DSL SDK assumes one thread and keeps unsynchronized process wide state.
That is not a test-only quirk, it shapes what the whole rendering path can do,
so it is written down rather than left as tribal knowledge in a commit message.

Adds specs/threading-model.md: the two known static hazards with the stack
traces that identify them, why locking store construction is not sufficient,
why per class [DoNotParallelize] is not sufficient, and how the constraint is
enforced per assembly through UsesDslStore.

Adds a threading section to src/README.md listing which projects are single
threaded and which are free threaded, plus the one instruction that matters for
anyone adding a test project: set UsesDslStore if it builds a Store. Also
documents regenerating the DSL from the command line, with a pointer to the
caveats.

Annotates the three types that carry the constraint - HeadlessDiagramStore,
EntityDesignerSurface and EntityDesignerViewModel - so it is visible at the
point of use and not only in a spec.

Note the practical consequence for the command line tool: rendering several
diagrams must be sequential within a process. Parallel rendering needs one
process per diagram.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The designer used to ask whether it was running inside Visual Studio, by
resolving SVsUIShell through Package.GetGlobalService. That is the wrong
question for a designer to ask, and under .NET 10 merely touching
Microsoft.VisualStudio.Shell throws "This is a reference assembly", so the
command line renderer died before drawing anything.

Colors are now pushed in rather than pulled out. The Dsl declares
DiagramPalette - five semantic colors with defaults - and DiagramTheme, which
holds the palette in force and re-themes registered style sets when it changes.
Microsoft.Data.Entity.Design.Package owns VsDiagramTheme, the only place that
maps Visual Studio color keys onto those slots. IsThemeServiceAvailable and
EntityDesignerSurface.SetColorTheme are deleted.

Fixes the leak in the same move. InitializeResources subscribed a closure to the
static VSColorTheme.ThemeChanged and could never unsubscribe, because a shape
class has no teardown point - it was the only unpaired += in the solution. The
subscription now lives in the package, which has a lifetime, and is removed on
dispose.

Results:

- Microsoft.Data.Entity.Tests.Design.Renderer passes 138/138 on net10, up from
  135/138. Solution wide failures drop from 13 to 10.
- `edmx render` runs on .NET 10 for the first time, and its SVG is byte
  identical to the net48 build: 51 rects, 36 connector paths, 152 text
  elements. Shipping Microsoft.Data.Entity.Tools as a real dotnet tool is now a
  packaging decision rather than a technical obstacle.
- The Dsl project no longer references VSColorTheme, EnvironmentColors or
  GetGlobalService for anything on the diagram, shape or connector paths. What
  remains is the three WPF controls - the context menu, its panel, and the
  floating zoom control - which move wholesale in work item 3.

CompartmentFill is deliberately left at its default rather than themed:
compartments are painted with the fill color declared in DslDefinition.dsl,
which is not themed, so colorizing the property icons against anything else
would make them disagree with what is drawn behind them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Records how one setting can drive both surfaces. Not implemented; for review.

The palette added when theming was inverted covers designer chrome and has no
effect on SVG output at all, because the exporter reads no StyleSet and emits
its own CSS from 14 hardcoded hex literals. Entity fill is a third system again,
stored per shape in the EDMX. Changing "the colors entities render in" today
means editing three things in three different ways.

The plan promotes DiagramPalette into the vocabulary both renderers consume and
has each translate it into its own medium: StyleSet overrides and GDI icon
colorization for the designer, CSS custom properties for SVG.

Two things get fixed on the way, both behaviour preserving and both landable
first: the WCAG luminance formula that decides black-or-white text on a fill is
currently implemented twice, in EntityTypeShape and SvgStylesheetManager; and
DiagramImageHelper calls ThemeUtils in the Visual Studio project at ten sites
for GDI bitmap work with nothing VS specific in it, which is both a theming
concern and one of the dependencies blocking the decoupling fitness function.

Leaves one question open for decision, because it is a product question rather
than a technical one: whether modernizing entity fill overrides at render time,
rewrites FillColor in the EDMX, or maps stored colors through the palette as a
hint. Nothing else in the plan depends on the answer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Records the principle governing where code goes: the core owns decisions, the
shell owns the platform. Anything the core needs is either pushed in, with a
working default so it runs headless, or requested by an event the shell answers.
Nothing is pulled, and the core never asks whether a host is present.

The point that was being missed: Visual Studio dependence and Windows
dependence are the same problem. The test is not "does this reference a Visual
Studio assembly", it is "could this run somewhere with no user interface at
all". WinForms, WPF and GDI all fail that test the same way VSColorTheme does.

Corrects specs/unified-theming.md accordingly. Work item 2 previously proposed
moving ThemeUtils - GDI bitmap colorization - into the Dsl in order to remove a
project reference. That is backwards: it trades a Visual Studio dependency for a
Windows one and makes the harder problem worse. The item now moves
DiagramImageHelper and the icon rasterization out to the package instead, which
is where the palette already went. All of it is paint time, and painting only
happens inside Visual Studio - the SVG exporter has its own vector icons.

Also records the corollary that has been violated most: decisions are functions.
Validating a path, building a connection string, choosing a default - these
belong in the core taking values and returning values, not on a UserControl. Ten
wizard tests currently fail because the logic under test can only be reached by
constructing a WinForms control, which needs a live shell. A test that needs
Visual Studio running is reporting an architecture problem, not a test problem.

States honestly what the rule cannot deliver: while the designer is built on the
Modeling SDK it cannot be fully platform free, because the SDK is WinForms and
GDI to its foundations. The achievable target is that the designer contains no
platform code of ours, leaving one known replaceable dependency instead of a
hundred scattered ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Folds the UI consolidation into step 8, because the two cannot be separated.
The UI still in the Dsl needs Dsl types - the context menu holds an
EntityDesignerSurface, CustomZoomDialog uses the Dsl's own resources - so its
destination has to be able to reference the Dsl, which cannot happen while the
Dsl references it. Moving first produces a circular reference. This was tried
and reverted.

Records the measured dependency inventory rather than an estimate. Thirteen
files, and several apparent hits are a false positive: the Resources matches are
the Dsl's own Properties.Resources behind the EntityDesignerRes alias.
EntityTypeShape turns out to need nothing at all - its using is stale.

Two findings worth having written down:

IViewDiagram is declared in the VS project but implemented by
EntityDesignerSurface, so it has to move into the Dsl. A base type cannot point
back at the shell.

The CustomZoomDialog resx needs care on the way across. Its manifest name comes
from root namespace plus folder path, while ComponentResourceManager looks it up
by the type's full name; if those disagree the dialog throws
MissingManifestResourceException at runtime with nothing failing at compile
time. That is exactly what broke the ModelWizard pages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two of the smaller items from step 8, both verified against the measured
inventory rather than assumed.

IViewDiagram was declared in the Visual Studio project but implemented by
EntityDesignerSurface in the Dsl - a base type pointing at the layer above it.
It moves to Microsoft.Data.Entity.Design.Model, which both projects already
reference, and which is where its only dependency (EFElement) lives.

Note it does not move into the Dsl, which was what the spec said. IDiagramManager
in the Visual Studio project also consumes it, and the Dsl cannot host a type
the Visual Studio project needs until the project reference is reversed. Model
is visible to both today and stays correct afterwards, so it avoids a second
move later.

EntityTypeShape's using of Microsoft.Data.Entity.Design.VisualStudio was stale.
The only apparent usage was Resources, which is the Dsl's own
Properties.Resources behind the EntityDesignerRes alias.

MicrosoftDataEntityDesignCommandSet takes a targeted alias rather than importing
Model.Designer wholesale: that namespace declares EntityTypeShape,
AssociationConnector and InheritanceConnector, which collide with the Dsl's
identically named view types. That collision is what step 6's Edmx prefix rename
exists to remove.

Verified: clean build, Northwind SVG byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The designer used to construct WPF dialogs, and three ViewModelChange classes
held a live dialog and read its controls from inside the transaction. That made
"add an entity type" impossible to express without a dialog existing, which is
the single biggest reason the designer could not run headless.

Each request is now its own event with its own EventArgs carrying what it needs
and what it wants back:

  NewEntityTypeRequested                      NewEntityTypeRequestedEventArgs
  NewAssociationRequested                     NewAssociationRequestedEventArgs
  NewInheritanceRequested                     NewInheritanceRequestedEventArgs
  UnmappedStorageEntitySetsDeletionRequested  ...DeletionRequestedEventArgs
  ReferentialConstraintRequested              ReferentialConstraintRequestedEventArgs
  CircularInheritanceDetected                 CircularInheritanceDetectedEventArgs

Cancelled starts true, so a host that answers nothing creates nothing - which is
exactly what the command line renderer wants and needs no special case.

VsDiagramRequestHandler in the package is the only place those events meet a
dialog. It is attached per document view and disposed with it, so handlers are
removed rather than accumulating.

The three *_AddFromDialog classes are replaced by EntityTypeAddFromRequest,
AssociationAddFromRequest and InheritanceAddFromRequest, which take values.

ViewUtils.SetBaseEntityType was mostly model logic wrapped around one error
dialog. The logic moves to InheritanceHelper.TrySetBaseEntityType in the model,
returning false for circular inheritance instead of telling the user about it.
Both shells then decide how to surface it: the designer raises
CircularInheritanceDetected carrying the two entity types rather than a message,
so the host words it in its own language; the properties window keeps showing
the dialog directly through a thin ViewUtils wrapper. It went to the model, not
the Dsl, because EFEntityTypeDescriptor in the Visual Studio project calls it
too.

Verified: clean build, and Northwind renders byte-identical on both net48 and
net10.0-windows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ogic

The file was two jobs sharing a name. Choosing which document view to show
is shell work; resolving an EFObject to a shape and setting the selection is
designer work. They are now DesignerNavigator (Package) and DiagramNavigator
(Dsl).

Both of the Dsl side's Visual Studio touches came from telling the mapping
details window to follow along, and both became one
MappingDetailsNavigationRequested event. The designer used to write
EntityMappingModes directly into the shell's editing context; it now reports
bool? UsesFunctionMapping instead -- a fact about the model rather than a
display mode. null preserves the existing behaviour of leaving the mode alone
on the association-set-mapping path.

Also fixes a latent NullReferenceException: the doc view loop null-checked
docView but then dereferenced singleDiagramDocView, and a null diagram was
passed into code that dereferenced it.

Verified: solution builds clean, test results unchanged (same 10 known wizard
failures), and edmx render Northwind.edmx is byte identical on net48 and
net10.0-windows at 51 rects / 36 paths / 152 text elements.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
robertmclaws and others added 28 commits August 17, 2026 15:41
Escher was Microsoft's internal codename for the designer and carries no meaning for anyone
maintaining this now. The four types take the Edmx prefix their assemblies already use:

  EscherAttributeContentValidator      -> EdmxAttributeContentValidator
  EscherModelValidator                 -> EdmxModelValidator
  EscherModelValidatorVisitor          -> EdmxModelValidatorVisitor
  EscherExtensionPointManager          -> EdmxExtensionPointManager

Files renamed to match. The codename still appears in comments and, more substantially, in the
Escher_CSDL / Escher_SSDL / Escher_MSL / Escher_UpdateModelFromDB members of ErrorClass, which
distinguish designer-level errors from the runtime's. Those are a separate decision - they are an
enum consumers switch on, not an internal type name.

Build green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Split, across six waves of parallel agents:

- 89 of 91 multi-type files now hold exactly one top-level type. The two
  survivors are generic/non-generic arity pairs (SubscribeContextCallback,
  IValueProperty), which conventionally share a file.
- git mv where the leftover type no longer matched its filename, including
  IXmlDesignePackage.cs -> IXmlDesignerPackage.cs, fixing a long-standing typo.
- Extension containers renamed to [LastNamespace]_[ClassExtending]Extensions:
  FeatureSupportedStateExtensions -> Edmx_FeatureStateExtensions,
  IExplorerViewModelExtensions -> Explorer_IExplorerViewModelExtensions.
- tools/type-catalog.cs now counts delegates. It walked only
  BaseTypeDeclarationSyntax, so delegate-only violations were invisible and
  every earlier count was a floor, not a total.

Namespaces:

- Microsoft.Data.Tools.VSXmlDesignerBase is gone. 24 declarations plus every
  consumer fold onto the folder-derived names already present alongside them.
- Fixed three manifest resource LogicalName entries still pinned to the dead
  namespace. TypeEditorHost had already been migrated, so
  new Icon(typeof(TypeEditorHost), "arrow.ico") resolved against a name nothing
  embedded - a runtime failure no build could surface. The .resources and .ico
  entries require different prefixes; both are commented so the asymmetry is
  not "corrected" later. Verified against the built assembly's manifest table.

Dead code removed:

- Both SwitchConverter/SwitchCase copies and the SwitchConverterErrorMessage
  key. Neither had a XAML consumer, and a WPF IValueConverter without one is
  unreachable.
- Seven unused .resx under UI/Views/Dialogs: six belonged to WPF DialogWindows
  and held only Name1/Color1/Bitmap1/Icon1 boilerplate, two also carrying
  orphaned WinForms designer data from before they were ported to XAML. The
  assembly drops from 22 embedded resources to 15.

Also:

- HeaderIconSet implements IDisposable; its public Dispose() was unreachable
  from a using block.
- File nesting for UI/Views/Dialogs: one wildcard plus its two real exceptions.
- Five dialog view models moved to UI/ViewModels/Dialog alongside the
  EnumTypeDialog view models they match. They are not controls.
- known-issues.md: Generate_returns_code reproduces 2-of-5 at ff6422f, so it
  predates this work. It fails far more often filtered than in a full run,
  which points at an ordering hazard rather than thread contention. Added
  Tests.Package as a fourth assembly exhibiting the flake.

Build clean; 1,732 tests pass across 14 assemblies on net48 and net10.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PackageResources.Designer.cs asks for
Microsoft.VisualStudio.Data.Entity.Package.PackageResources, but the csproj
pinned ManifestResourceName to ...Package.Resources - the pre-rename name. The
resx and its generated class were renamed in e85d22d; this override was not,
so every lookup through PackageResources threw at runtime. Opening a diagram hit
it via UpdateWindowFrameCaption -> EditorCaptionFormat.

No build could catch this: manifest names are only checked when a
ResourceManager groveling for them fails.

Dropping the element outright does NOT work here - this project's VSSDK resx
pipeline then emits an empty manifest name (".resources"), which is why all
three EmbeddedResource entries pin their name explicitly. Comment added so the
next reader does not repeat that.

Swept the rest of the tree against the built assemblies rather than assuming
this was the only one:

- 16 ResourceManager("literal") call sites across 38 project outputs: all
  resolve. Note the generator splits long names as "abc" + "def", so a
  per-line grep silently under-reads them.
- 8 type-derived lookups - 6 ComponentResourceManager(typeof(T)) WinForms
  designers plus the 2 new Icon(typeof(TypeEditorHost), ...) calls: all resolve.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ExportDiagramDialog.xaml threw XamlParseException on load:
'PackageResources.ExportImage_DialogTitle' StaticExtension value cannot be
resolved to an enumeration, static field, or static property.

Caused by e85d22d. That pass moved single-use strings to their consumer, and
ExportImage_* moved out of the public DiagramsResources into PackageResources -
which is internal. The XAML prefix was repointed with it:

  before  props -> Microsoft.Data.Entity.Design.Diagrams.Properties (Resources, public)
  after   props -> Microsoft.VisualStudio.Data.Entity.Package     (PackageResources, internal)

The move was right; the visibility was not. SDK-style WPF projects no longer
emit GeneratedInternalTypeHelper, so BAML cannot reach an internal type at all.
The two other XAML-consumed resource classes, EdmxDesignerResources and
DialogsResource, are already public for exactly this reason - PackageResources
now matches them: public class, public statics, internal ctor, and
PublicResXFileCodeGenerator so a regeneration does not revert it.

Verified against built metadata rather than the build log, since x:Static is
resolved at runtime and compiles clean either way:

- PackageResources is Public, and all 10 members the XAML names are public
  static properties.
- Swept every x:Static in our XAML: 157 targets on our types, 0 on non-public
  ones. 58 target external types and were not checked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ck.cs

Context menu parenting:

DiagramSurfaceContextMenu hosts itself in a Popup with AllowsTransparency set,
so WPF gives it its own top-level window instead of drawing inside the parent.
Nothing owned that window, so another application could be activated into the
z-order between the menu and Visual Studio, leaving the menu floating over an
unrelated window.

Show() now takes the owner hwnd and applies it on Popup.Opened via
SetWindowLongPtr(GWLP_HWNDPARENT). It has to run per-open, not once at
construction, because WPF destroys and recreates the popup's window between
opens. The service supplies IVsUIShell.GetDialogOwnerHwnd, which is what
ExportDiagramDialog already used; that duplicated lookup is now the shared
GetDialogOwnerHwnd helper. IntPtr.Zero is tolerated and simply leaves the popup
unowned, so a missing shell cannot break the menu.

SetWindowLongPtrW exists only in the 64-bit user32, so the 32-bit entry point is
used when the process is 32-bit.

tools/resource-check.cs:

Covers the three lookups the compiler cannot see, reading built assemblies
rather than the build log, and exits with the problem count so it can gate a
build:

- ResourceManager("Name") literals, joining the "abc" + "def" fragments the resx
  generator emits for long names.
- ComponentResourceManager(typeof(T)) and new Icon(typeof(T), "file"), which
  resolve against T's full name and so break silently when a type moves.
- x:Static, which needs a public type with a public static member.

Reads each project's OWN output; a stale copy in a consumer's bin would
otherwise vouch for a name that is no longer embedded. Mutation-checked in both
directions: a bad literal and a nonexistent x:Static member are each reported,
and the exit code goes 0 -> 1.

Currently: 16 literals, 8 type-derived lookups, 157 x:Static, no problems.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The built-in layout is three calls to the SDK's AutoLayoutShapeElements with
workarounds still carrying Microsoft's original bug numbers, and it produces
diagrams that ignore the grouping already recorded in the file. This designs an
opt-in MSAGL engine behind a floating-toolbar toggle, with grouping detection
that persists its guess to a new GroupName attribute so a human can correct it.

Records two things found while investigating, both of which reverse earlier
assumptions: GraphObject cannot be dropped because it is the geometry substrate
under every DSL shape, not the auto-layout engine behind one call; and the
EDMX connector-route persistence path already exists end to end, so recording
routes needs no new plumbing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rendering must not mutate what it renders. An earlier draft proposed a --layout
flag on edmx render, which would have turned a read-only command into one that
rewrites its input. Layout gets its own subcommand instead, which also makes
algorithm comparison a matter of running layout then render over copies.

Names the placement algorithms by what they do rather than by their papers, and
records that swapping them is one constructor argument, since every settings
type derives from LayoutAlgorithmSettings and CalculateLayout dispatches on it.
Tuned defaults per algorithm live on MsAglConstants.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
LayoutEngineManager holds one keyed instance of each engine for the life of the
designer. Taking the algorithm at construction would force an instance per
algorithm or a rebuild of the manager to change it. A settable property lets the
CLI set it before invoking and leaves room for a designer-side picker later.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AutoLayoutDiagram was 160 lines of Modeling SDK workarounds sitting on the
surface, which left no room for a second way to lay a diagram out. It moves
verbatim into DslLayoutEngine behind LayoutEngineBase, with LayoutEngineManager
holding the keyed set and tracking which one is current. The surface now
delegates, so all five existing callers are untouched and behaviour is
unchanged.

SaveLayoutFlags comes out of its nested private home into its own file, since
the MSAGL engine will need the same freeze-and-restore. The surface no longer
names a GraphObject type, so that using goes too.

Layout takes a weakly typed IList rather than IList<ShapeElement>, matching the
SDK's own AutoLayoutShapeElements and what every caller already holds; the spec
is corrected to match.

Verified: solution builds with 0 errors, and all 14 test assemblies pass with
0 failures under --logger trx.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The engines were constructed inline where they were needed, which is the
opposite of dependency injection. Each host now composes them once at its own
composition root and the surface is handed the result:

  - the VS package registers a LayoutEngineManager as a package service during
    Initialize, and the doc data pulls it out at the first point it holds a
    surface
  - the CLI registers LayoutEngineBase and LayoutEngineManager with the host
    builder it already had, and RenderCommand takes the manager and threads it
    into EdmxDiagramLoader.Load

Registration order is the priority order in both: LayoutEngineManager takes the
first engine as its default, and MS.DI resolves IEnumerable<T> in registration
order, so the two hosts agree without a second mechanism.

Names corrected: the surface holds a LayoutManager, and the manager's own
collection of engines is LayoutEngines. The surface starts with none and
AutoLayoutDiagram no-ops until a host supplies one, so the layout request that
arrives during deserialization is dropped rather than served by a default
nobody chose.

Verified: solution builds with 0 errors, all 14 test assemblies pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AddService with a pre-constructed LayoutEngineManager was a service locator
holding a singleton - nothing was injecting anything, and the manager's
constructor parameter was being satisfied by hand at the registration site.

The package now builds a ServiceCollection, registers the engine and the
manager as types, and bridges the result to the VS service container through
the ServiceCreatorCallback overload. Nothing is constructed until something
asks, and what comes back was built by the container. Registration order stays
the priority order, which this container guarantees for IEnumerable<T>.

MEF was considered and rejected for this. The VSIX declares only a VsPackage
asset, so VS's MEF catalog never sees these types, and [ImportMany] ordering is
undefined - determinism there needs ExportMetadata and an explicit sort, which
contradicts registration-order-is-priority. MEF becomes the right answer only
if layout engines ever become a third-party extension point, and then as a
discovery mechanism feeding these registrations.

Verified: solution builds with 0 errors, all 14 test assemblies pass, and both
Microsoft.Extensions.DependencyInjection assemblies ship inside the VSIX.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The spec said Advanced mode "does not fall back to" DSL routing, which was weak
enough to keep inviting proposals that MSAGL place the shapes while DSL draws
the lines - as a first version, or to evaluate placement on its own. It is not
an option at any stage. The DSL routing is what this work exists to replace, so
shipping on top of it would measure the new thing through the defect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Places shapes and routes connectors with MSAGL, writing both back to the
diagram. Self-contained: it calls no Modeling SDK layout or routing at any
stage, and every connector it touches comes back ManuallyRouted with explicit
EdgePoints, which the existing change rules persist to the EDMX.

Three things the API forced, each recorded where it bites:

  - Work at 96 units per inch, not in inches. SugiyamaLayoutSettings clamps
    LayerSeparation to Math.Max(10, value), so every separation under ten
    inches would silently become ten.
  - Run RectilinearEdgeRouter explicitly. For a graph with no clusters
    LayoutHelpers.CalculateLayout hands straight to the layered engine, which
    emits its own splines and never consults EdgeRoutingMode.
  - Read routes from either a Polyline or a Curve of segments. Corner fitting
    always runs, even at radius zero, where it returns plain lines.

MSAGL measures upward from the bottom left and the designer downward from the
top left, so coordinates are mirrored about the topmost node on the way back.

Also adds EntityTypeShape.ConnectedLinks, which both engines use instead of
copying FromRoleLinkShapes and ToRoleLinkShapes into an ArrayList, and maps the
Msagl package for PackageSourceMapping.

Verified: solution builds with 0 errors, all 14 test assemblies pass, and three
new tests assert placement without overlap, a route on every connector, and
that shapes actually move off the positions the EDMX supplied.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sits beside the Auto Layout button rather than with the grid toggles, because
it changes what that button does rather than what the surface looks like.
Checked selects MsAglLayoutEngine, cleared selects DslLayoutEngine.

Toggling only moves LayoutEngineManager.Current; it does not lay the diagram
out. Re-arranging everything the moment a toggle is clicked would discard
positions the user may have spent time on, so the Layout button stays the thing
that moves shapes.

If the requested engine is not registered the toggle reverts and logs, so the
button never claims a mode the designer is not in. State is synced from
whichever engine is actually current once the diagram has loaded, with the
handler unhooked while loading, matching the grid toggles.

Verified: solution builds with 0 errors. Test suite passes apart from
Generate_returns_code, the pre-existing flake recorded as known-issues 1.4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GeneratorTestBase.Model was an unguarded lazy static. Four test classes derive
from that base and MSTest runs at MethodLevel parallelism, so two threads could
both find the field null, both build a DbModel, and the second assignment would
replace the first.

The replacement is the defect, not the duplicated work. Every test there reads
the property twice - once for an entity set, once for the model passed beside
it - so a replacement between those reads hands the generator an entity set
belonging to a different DbModel. TableDiscoverer.Discover then looks that set
up in the other model's mappings, matches nothing, and First throws "Sequence
contains no matching element".

That also explains the inversion recorded in known-issues 1.4, which had looked
like an ordering hazard: a small filtered run starts all four classes at once
with the field still null, the widest possible window for the race.

Fixed with Lazy<DbModel> and LazyThreadSafetyMode.ExecutionAndPublication, so
the model is built once and never replaced. Not [DoNotParallelize], which would
have hidden a real defect behind slower tests.

Known-issues 1.4 and 1.5 are updated. The failing test was DefaultVB, not
DefaultCSharp as every prior entry claimed - all four classes have a method of
that name and the console summary never said which. 1.5's other two rows stay
open; their names were never captured.

Verified: the reproducing filter failed 3 of 3 before and passes 10 of 10 after;
the assembly passes 455/481 three runs running; the full solution passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
known-issues.md had grown to a third fixed entries, which buries what is still
open. Anything resolved now moves to fixed-bugs.md instead of staying in place
marked FIXED. The entries are kept rather than deleted because several describe
traps that are easy to walk back into.

Numbers are not reused across the two files, so the gaps in known-issues.md are
deliberate and every existing reference still resolves - rename-catalog.md and
Package.cs point at 4.2 and 6.1, both of which are still open and unmoved.

Moved: 1.1, 1.2, 1.3, 1.4, 2.3, 3.1, 3.2, 4.1. Issue 1.5 is split rather than
moved, since only one of its three rows was ever identified; the two unnamed
ones stay open and now carry the suspicion that solved 1.4.

Also records a third sighting under 1.5 - Tests.Design.EntityFramework on net10,
seen once during this work, not reproduced across three full-solution runs with
trx enabled from the start, so the name escaped again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MSTest silently never runs a static test method. All 17 were already static in
2021, inherited from the EF6 repository where they were xUnit [Fact] methods -
xUnit runs static tests, MSTest does not - so the January 2026 conversion to
[TestMethod] turned a legal signature into one that is skipped without report.

Measured on Microsoft.Data.Entity.Tests.Design.EntityFramework:

  before   390 total, 295 passed,  95 skipped
  after    407 total, 304 passed, 103 skipped

+17 collected, exactly the expected count. Nine of them run and all nine pass;
the other eight carry [Ignore]. No new failures on either target.

Fixing this uncovered a second, unrelated cause of MSTEST0003 that it had been
masking: seven private helper methods carrying a stray [TestMethod, Ignore]
from the same conversion. All seven are live - called by real tests, between 1
and 11 call sites each - so the attribute is the defect, not the method. Logged
as known-issues 2.4 rather than fixed here, since it is a different bug.

The issue moves to fixed-bugs.md. Its file paths were wrong there too: the
tests are in Tests.Design.EntityFramework, not VersioningFacade.

Also makes trx logging a standing rule for 1.5 rather than a remedy. Capturing
reactively has now lost the name on four separate sightings, because the rerun
that captures is the run that passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
None of the eight could ever have been collected as a test - all are private,
take parameters, and most return a value. They picked up the attribute during
the same [Fact] to [TestMethod] conversion that caused 2.2, which was masking
them. Seven are called by live tests; CreateDbModel's call sites are commented
out along with the tests that used them, so the method stays.

The eighth only became visible once the other seven were cleared, and is in an
assembly the original 2.2 survey never covered.

MSTEST0003 is now 0 across the solution, down from 98.

Also names the 1.5 intermittent, on the first run after trx logging became
unconditional. Create_creates_valid_EntityConnection and its sibling, net10
only, and the cause is production code:
StoreSchemaConnectionFactory.EnsureSqlClientRegistered sets its guard flag
before doing the registration the flag guards, so a second thread returns
early and calls GetFactory against an unpopulated registry. Same shape as the
GeneratorTestBase defect one layer down. Logged as 2.5 rather than fixed here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The flag was set before the registration it guarded, so a second thread saw it,
returned early, and called DbProviderFactories.GetFactory against a registry
nothing had populated yet - "The specified invariant name 'System.Data.SqlClient'
wasn't found in the list of registered .NET Data Providers". net10 only, since
.NET Framework resolves the provider from machine.config and never enters this
path. This was the named cause of one row of known-issues 1.5.

Replaced with Lazy<bool> and LazyThreadSafetyMode.ExecutionAndPublication.

This makes concurrent callers wait, which is the requirement rather than a side
effect: the caller's next statement is GetFactory, which throws if registration
has not finished, so returning early was the bug. The wait is bounded to the
first call and only for threads arriving mid-registration; afterwards Lazy nulls
its factory and the getter is a plain field read. AddSingleton blocks in the same
place when a singleton is first resolved.

Verified: 12 runs of the affected assembly on net10, trx on every run, zero
recurrences. The two failures seen during verification were a different registry
- DependencyResolver's static ProviderServicesResolver - and one of them was on
pre-fix code across 32 runs, so that family is independent of this change and is
logged as known-issues 2.6.

2.6 also records why this is a patch: three traced intermittents are now the same
shape, the real fix is container-managed registration, and it is deferred because
a container cannot own a BCL static registry and because the package's phased
load makes placement a design question.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The engine ran RectilinearEdgeRouter itself after LayoutHelpers.CalculateLayout
returned. Two things were wrong with that.

The comment justifying it was false: LayeredLayoutEngine does read
EdgeRoutingMode and has a Rectilinear case that builds the same router. The
claim that a cluster-free graph ignores the mode was simply wrong.

Worse, routing separately discards the crossing reduction. The layered algorithm
orders shapes so edges flowing through its channels cross as little as possible;
a router run afterwards re-paths every edge independently as a shortest
obstacle-avoiding route and knows nothing about those channels. Only
EdgeRoutingMode.SugiyamaSplines consumes that work. So the pipeline minimised
crossings and then threw the result away, which is what the diagram showed.

Routing is now MSAGL's, selected by a ConnectorRouting property alongside
Algorithm: Orthogonal, Layered, Curved, Straight. RouteConnectors is kept but
commented out, with both errors recorded next to it.

Curve flattening had to improve to go with it. Taking each segment's endpoints
turns a spline into the polygon through its corners - correct only for the
orthogonal mode, where every segment is a line. Non-line segments are now
sampled.

The toolbar gains a temporary dropdown to switch routing on a live diagram,
which required a DropDownTemplate on the existing command template selector.
Both are marked TEMPORARY and are meant to go once a mode is chosen; selecting
a mode re-runs the layout immediately, unlike the Advanced toggle.

Verified: solution builds with 0 errors, all 14 test assemblies pass with trx
logging on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
On solution restore, frame activation ran ReloadArtifactIfNecessary, which
called GetNewOrExistingContext and thereby initiated the artifact's first
load. That load's OpenXmlModel pumps the message loop, the shell's deferred
doc-data load runs inside the pump, and it read IsDesignerSafe on the
VSArtifact still suspended inside its own Init - published to the model
manager but with no XLinq node yet. Validating that half-built artifact
computed a line number for an artifact-level error and asserted in
EFObject.GetTextSpan, six times, then opened the designer blank; a second,
proper load followed. The document loaded twice, the first time broken.

ReloadArtifactIfNecessary is a reload hook and must never start the first
load: it now returns unless ModelManager.GetArtifact (a pure lookup that
never loads) shows the artifact is already loaded. The normal doc-data path
loads and validates it properly.

Supporting robustness on the same path:
- EFArtifact.Init throws on a null model/document instead of registering an
  artifact with no node, and subscribes to transaction events only after the
  node is set.
- ModelManager.RegisterArtifact rolls a throwing Init back out of the
  manager's lookup tables.
- VSXmlModelProvider.GetXmlModel re-checks its cache after OpenXmlModel
  (which can re-enter during its pump) and disposes the redundant model.
- EntityDesignDocumentFrameMgr null-guards LayerManager, which is only
  assigned once Init completes.
- EFObject.GetTextSpan's null-node assert now names the element type, its
  pretty string, the artifact URI, and IsDisposed.

Co-Authored-By: Claude <noreply@anthropic.com>
Surfaces the diagram's layout choices in the Properties window and adds an
MSAGL-based grouping layout alongside the legacy DSL engine.

- Diagram carries LayoutMode (Legacy|Modern) and ConnectorMode; each shape
  carries GroupName. All three persist as attributes and are editable in the
  Properties window, with GroupName browsable only in Modern mode.
- Any layout-affecting property change (DiagramLayoutInput) re-runs the
  layout, so editing mode, connector routing, or a group name rearranges the
  diagram on its own.
- GroupingLayout/GroupingStrategy detect groups (explicit, fill colour, hub
  affinity, structural) and name each group after the entity it centres on;
  the modern engine writes its guesses back only for shapes that had none.
- A clear-groups button appears in the floating toolbar in Modern mode.
- LayoutEngineManager resolves a stateless engine per LayoutMode; the DSL
  engine reports Legacy and ignores connector mode. ConnectorRouting is
  removed.
- Rendering tests: TestEdmxBuilder plus GroupingLayoutTests.

Co-Authored-By: Claude <noreply@anthropic.com>
Sweeps the codebase so a firing assert shows its condition (and stack)
instead of a blank "Assertion Failed" banner: 344 single-line, single-arg
Debug.Assert(cond) calls across the product now read Debug.Assert(cond,
"cond"), matching the codebase's existing convention. Applied by a
char-accurate transform that skips anything it cannot parse cleanly, so
multi-line and ambiguous cases are left untouched.

Also adds the standard license header to source files that were missing it.

Co-Authored-By: Claude <noreply@anthropic.com>
…edraw, grouping opt-in

Property window
- Expose ManuallyRouted on a selected association's active-diagram connector
  (new Routing category, EFAssociationDescriptor). Off clears the points and lets
  the engine re-route; On pins the connector's current route. No selection
  redirect, so Association stays the selected object and Delete/Rename are intact.

Per-connector redraw
- LayoutEngineBase.RouteConnectors: route-only, moves no shape. MSAGL uses
  RectilinearEdgeRouter with all shapes as fixed obstacles; DSL freezes shapes and
  reroutes the links. EntityDesignerSurface.RerouteConnectors is the entry point.
- "Redraw Route" context-menu item, shown only in Modern mode; leaves the
  ManuallyRouted flag untouched (mutation-checked test).

Provenance fix
- EntityTypeShape_ChangeRule no longer clears ManuallyRouted on shape move/expand,
  so a hand-routed connector survives reopen instead of reverting to auto.

Grouping opt-in
- EnableGrouping / GenerateGroupNames Diagram settings (Modern only, default off),
  a Modern Layout Options property category, and GroupingLayout.GroupByExisting.
- RebakePolicy enum + ManualRouteRebakePolicy attribute (forward plumbing) + XSD.

Tests
- ConnectorRoutingTests (provenance across move/resize/load + malformed XML),
  MsAglLayoutEngine route-only tests, grouping opt-in tests, TestEdmxBuilder Route/Grouping.

Spec
- Two-model round-trip writeup, connector routing/provenance, single-connector
  redraw, and grouping opt-in rules in specs/diagram-layout-engines.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…icit

Package updates. The .NET runtime libraries move to a single 10.0.10 baseline
shared by the net48 and net10.0 targets, via a new DotNetLibrariesPackageVersion:
System.Text.Json, System.Resources.Extensions, System.Security.Permissions,
System.Drawing.Common, System.ComponentModel.Composition and the three
Microsoft.Extensions packages. 10.0.10 rather than 10.0.11 because its assembly
version, 10.0.0.10, is exactly the ceiling of the bindingRedirect in
devenv.exe.config, so the VSIX unifies on the copy Visual Studio already loads
instead of running a second one side by side.

Also: Azure.Identity to 1.21.0, the first release not deprecated for depending on
a deprecated MSAL; the Microsoft.Identity.Client pin removed so it resolves to the
4.83.1 Azure.Core asks for, under the host's 4.84.1.0 ceiling; EntityFramework to
6.5.2; MySql.Data.EntityFramework to 9.7.0; Oracle.ManagedDataAccess.EntityFramework
to 23.26.300; Microsoft.IO.Redist to 6.1.3; Microsoft.Data.SqlClient.SNI to 6.0.3;
ILRepack to 2.0.46; Krafs.Publicizer to 2.3.2; McMaster to 5.1.0; Svg.Skia to 5.2.3;
MSTest to 4.3.3; FluentAssertions to 7.2.2, held off the 8.x line because it ships
under a commercial license.

Two pins are gone. Microsoft.NET.Test.Sdk is carried by MSTest, and any pin below
its 18.4.0 floor fails restore with NU1109. Microsoft.SourceLink.GitHub is built
into the .NET 8 and later SDKs, so its package reference goes too.

Microsoft.ServiceHub.Framework and Nerdbank.Streams stay where they are: their
newer releases require Microsoft.VisualStudio.Threading 17.13, above the 17.10 line
the VSIX targets. Svg.Skia 5.x splits Svg.Animation and Svg.SceneGraph into separate
packages, which needed new packageSourceMapping entries.

Target frameworks. Every project now lists its own TargetFrameworks and
Directory.Build.props sets neither TargetFramework nor RuntimeIdentifier. The RID
was doing nothing: native assets land identically without it, verified by diffing
the whole bin/Release tree. The VSIX project keeps a singular TargetFramework on
purpose, since even a single valued TargetFrameworks triggers cross targeting and
breaks the VSSDK import.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PackAsTool rejects any TargetFrameworkIdentifier with a TargetPlatformIdentifier
set (NETSDK1146), so net10.0-windows made the CLI unshippable as a tool. Both the
CLI and the renderer it depends on now target plain net10.0 and pick up the Windows
Desktop assemblies through a FrameworkReference instead of the -windows moniker.
The renderer had to move too: a net10.0 project cannot reference a net10.0-windows
one (NU1201).

PrivateAssets="all" on the renderer's FrameworkReference is load bearing.
_CheckForTransitiveWindowsDesktopDependencies fails any non -windows consumer with
NETSDK1136, but it only inspects TransitiveFrameworkReference, so keeping the
reference from flowing lets the CLI reference the renderer and declare its own.

UseWindowsForms and UseWPF are gone from both. Neither project needs designer or
XAML build support; they only construct WinForms and WPF objects in memory, which
the framework reference alone supports. Windows only at runtime either way.

Verified by packing, installing with dotnet tool install --global, and rendering
Northwind to Mermaid and SVG through the installed dotnet-edmx command. Full suite
still passes, including the renderer's net10.0 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Restore failed on a clean runner with NU1100 for Microsoft.DiaSymReader,
System.Data.Common, System.Security.Cryptography.Pkcs and
System.Text.RegularExpressions. With source mapping enabled behind a <clear />,
a package matching no pattern matches no source at all, so restore cannot even
pick a feed to try.

This was latent rather than new. All four resolved locally out of the warm global
package cache, which is why it only surfaced on CI. Verified by clearing
~/.nuget/packages and restoring cold: the same four before, none after, followed
by a clean build and a full test run against the cold tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@robertmclaws
robertmclaws merged commit c9fa2dd into main Sep 2, 2026
5 checks passed
@robertmclaws
robertmclaws deleted the ai-tools branch September 2, 2026 02:32
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.

1 participant