<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Modern Software Engineering]]></title><description><![CDATA[Modern Software Engineering]]></description><link>https://pourmonfared-azimi.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6a96c850deff7f4bf7c7b0a7/499801b6-d530-4be9-badf-733e17c19764.png</url><title>Modern Software Engineering</title><link>https://pourmonfared-azimi.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 23 Oct 2026 09:55:31 GMT</lastBuildDate><atom:link href="https://pourmonfared-azimi.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The GUIDs in your .sln were never data]]></title><description><![CDATA[Two people add a project on Friday afternoon. One adds Payments.Api, the other adds Payments.Worker. Neither has touched the other's code. Both branches…]]></description><link>https://pourmonfared-azimi.hashnode.dev/the-guids-in-your-sln-were-never-data</link><guid isPermaLink="true">https://pourmonfared-azimi.hashnode.dev/the-guids-in-your-sln-were-never-data</guid><dc:creator><![CDATA[Meysam Pourmonfared Azimi]]></dc:creator><pubDate>Fri, 04 Sep 2026 12:06:22 GMT</pubDate><content:encoded><![CDATA[<p>Two people add a project on Friday afternoon. One adds <code>Payments.Api</code>, the other adds <code>Payments.Worker</code>. Neither has touched the other's code. Both branches merge cleanly everywhere except one file, and it is the file nobody wants to own.</p>
<p>The conflict is not in the project list. It is thirty lines lower, in a block that looks like this:</p>
<pre><code class="language-plaintext">{8CDD8387-B905-44A8-B5D5-07BB50E05BEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8CDD8387-B905-44A8-B5D5-07BB50E05BEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
</code></pre>
<p>Both developers appended four rows to the same global section, keyed by GUIDs neither of them chose, neither of them can read, and neither of them can verify by eye. Resolving it means deciding whether two opaque identifiers belong in the file — which is a question a human is unusually badly equipped to answer, and a question that never had to be asked.</p>
<p>That is the part of the <code>.slnx</code> story worth writing about. Not that XML is prettier.</p>
<h2>What the old format was actually storing</h2>
<p>Microsoft's own extensibility documentation is precise about the two GUIDs on a <code>Project</code> line: "This statement contains the unique project GUID and the project type GUID. This information is used by the environment to find the project file or files belonging to the solution, and the VSPackage required for each project."</p>
<p>Read that carefully. The project type GUID identifies which VSPackage loads the project — a resolution the file extension already determines. The unique project GUID identifies the project within the solution — an identity the relative path already provides, because two projects at the same relative path are the same project. Neither GUID carries a fact you could not recover from the path.</p>
<p>What they did carry was a cost. Every relation about a project was expressed by repeating its GUID in a different global section: nesting in <code>NestedProjects</code>, build order in <code>ProjectDependencies</code>, configuration mapping in <code>ProjectConfigurationPlatforms</code> two or three rows (<code>ActiveCfg</code>, <code>Build.0</code>, sometimes <code>Deploy.0</code>) for every solution configuration the project participates in. Adding one project meant appending to three or four append-only regions of one file. That is the merge-conflict generator, and it is structural rather than cosmetic.</p>
<h2>What the schema says</h2>
<p>The format is defined by a real schema in the library that reads and writes it, <code>microsoft/vs-solutionpersistence</code>. <code>Slnx.xsd</code> on <code>main</code> defines a project as:</p>
<pre><code class="language-xml">&lt;xs:attribute name="Path" type="xs:string" use="required"/&gt;
&lt;xs:attribute name="Type" type="xs:string" /&gt;
&lt;xs:attribute name="DisplayName" type="xs:string" /&gt;
</code></pre>
<p>One required attribute: the path. No <code>Id</code>. <code>Type</code> is optional, and the library explains why in a doc comment on <code>ProjectType</code>: it "maps a friendly name and or a file extension to a project type id", and the extension "can be used to determine the project type automatically". The type GUID is a function of the path, so the format stores the path and computes the GUID.</p>
<p>Solution folders lose their lookup table too. In <code>.slnx</code> a <code>Folder</code> has one required attribute, <code>Name</code>, and projects nest inside it as XML children — containment instead of a <code>NestedProjects</code> section of GUID pairs. Build order survives as <code>&lt;BuildDependency Project="..."/&gt;</code> on the project it belongs to.</p>
<p>The configuration mapping is the most interesting collapse. Instead of N × M rows, the schema has a rule group <code>BuildType</code>, <code>Platform</code>, <code>Build</code>, <code>Deploy</code>, each with an optional <code>Solution</code> attribute (which solution configuration this matches) and a <code>Project</code> attribute (what the project should do). Those rules hang off either a <code>ProjectType</code> or an individual <code>Project</code>. So the defaults for every <code>.csproj</code> in the solution are stated once, against the type, and a project only appears in the file when it deviates. The excluded-from-Release test project is now a two-line exception rather than a row in a wall of rows.</p>
<p>And this is the claim the whole thing rests on, from the .NET Blog: the new format "is much more concise than the old format – but it contains all of the same data!" Same data. What was removed was encoding.</p>
<h2>The idea underneath</h2>
<p>A solution file is a serialization of a small labelled graph. Projects are nodes, solution folders are a tree overlay, build dependencies are edges, and the configuration mapping is a function from (solution configuration, project) to (project configuration, build, deploy).</p>
<p>The <code>.sln</code> format gave every node a surrogate key and then expressed every relation by repeating that key elsewhere. <code>.slnx</code> uses the natural key that already existed and was already unique inside the solution — the relative path — and expresses relations by containment.</p>
<p>The framing is mine, not Microsoft's, but it makes a prediction you can check. A three-way text merge conflicts on co-located edits. Surrogate keys guarantee that facts about one node are scattered into shared global regions, so two semantically disjoint additions land in the same line range and collide. Natural keys plus containment put an addition on one line, in a position determined by where the project lives. That is the mechanism behind Microsoft's much blander phrasing: "By simplifying the file structure, we've reduced the likelihood of merge conflicts in version control systems."</p>
<h2>Where the GUIDs came back</h2>
<p>Two honest limits, because a piece that only shows the good half is not worth trusting.</p>
<p>Visual Studio has been observed writing GUIDs back in. Issue #109 on <code>vs-solutionpersistence</code> shows VS rewriting <code>&lt;Folder Name="/Libraries/Http.Client/"&gt;</code> as the same element with <code>Id="896cf6f3-..."</code> on save. The schema on <code>main</code> declares only <code>Name</code> on <code>Folder</code>. The issue is closed with no visible maintainer answer, so whether current Visual Studio still does this is something to check on your own repo rather than take from me.</p>
<p>And extension-based type inference is not total. In dotnet/sdk#47638, <code>dotnet sln add</code> on a <code>.msbuildproj</code> failed with "Project has an unknown project type and cannot be added to the solution file." — while <code>dotnet sln migrate</code> kept the <code>Type</code> for the same project. That asymmetry is exactly why <code>Type</code> is in the schema as an optional attribute: inference is the default, not a promise.</p>
<h2>What the migration actually costs</h2>
<p>The conversion is one command, <code>dotnet sln MySolution.sln migrate</code>, and the interesting work is everywhere else.</p>
<p><strong>Toolchain floors, on every machine and not just yours.</strong> SDK 9.0.200 for the CLI. MSBuild or Build Tools for Visual Studio 17.13 or later to build. Visual Studio 17.14 to open. Your laptop is not the constraint; the CI image with a pinned SDK is.</p>
<p><strong>Two files in one folder is the real failure mode.</strong> Microsoft is blunt about it: "it's an error to run <code>dotnet build</code> or other commands that need to build in a directory with both a <code>.sln</code> and a <code>.slnx</code> — we don't know which one to build!" Delete the old file in the same commit, and update every workflow, script, Dockerfile and README that names it. Note also that <code>dotnet sln migrate</code> refuses to run if a <code>.slnx</code> of that name already exists — if you hit that, someone migrated before you and the two files may have diverged.</p>
<p><strong>Check what the old file carried besides the project list.</strong> Solution folders and the loose files pinned into them, per-project configuration mappings, build-order dependencies, and projects deliberately excluded from a configuration — that last one is invisible, because it is an absent <code>Build</code> rule rather than an absent project. Diff <code>dotnet sln &lt;name&gt;.slnx list</code> against the baseline, then read the XML for the rest.</p>
<p><strong>Multi-project startup is the thing that silently disappears.</strong> dotnet/sdk#48014 asked for <code>dotnet sln migrate</code> to carry <code>.slnLaunch</code> over as <code>.slnxLaunch</code>. It was closed as not planned. If your team runs three services on F5, verify that before you delete anything.</p>
<p><strong>Generators ignore your preference.</strong> Visual Studio 2026 exposes a "Default Solution File Format" option, but Unity — and anything else that regenerates a solution — writes whatever it wants.</p>
<h2>Where this is going</h2>
<p>The direction is settled. Since .NET 10, <code>dotnet new sln</code> emits <code>&lt;Solution&gt;&lt;/Solution&gt;</code> and you need <code>--format sln</code> to get the old thing back; the stated reason is that SLNX has proven "a stable and understandable format". Visual Studio 2026 makes the choice a setting. In .NET 11, currently in preview with a November 2026 target, the only solution-related change is that <code>dotnet sln</code> can create and edit solution filters (<code>.slnf</code>) from the CLI, and a filter's parent can be a <code>.sln</code> or a <code>.slnx</code>.</p>
<p>There is no .NET 11 breaking change for <code>.slnx</code>. The format stopped being news, which is the outcome a file format should want.</p>
<hr />
<p><strong>Sources.</strong> .NET Blog, "Introducing support for SLNX" (2025-03-13) · Visual Studio Blog, "New, Simpler Solution File Format" (2025-03-13) · Microsoft Learn, <code>dotnet sln</code> reference and the .NET 10 breaking change for <code>dotnet new sln</code> · Microsoft Learn, "Solution (.sln) file", VS extensibility internals · <code>microsoft/vs-solutionpersistence</code> — <code>Slnx.xsd</code>, <code>ProjectType.cs</code>, issue #109 · <code>dotnet/sdk</code> issues #47638 and #48014, PR #51156 · What's new in .NET 11. All accessed 2026-09-01.</p>
<p><strong>Not executed here.</strong> No .NET SDK was available in the environment where this was written, so no command above was run and no output was captured live. Every version floor, command and error string is quoted from the sources listed. Run the migration on one real repository before you run it on twelve.</p>
<p>If you have already moved a large solution: what did you find missing in the <code>.slnx</code> that you only noticed a week later?</p>
<p>#dotnet #csharp #softwarearchitecture</p>
]]></content:encoded></item><item><title><![CDATA[Central Package Management in .NET: One File, Zero Version Drift]]></title><description><![CDATA[Api.csproj references Serilog 4.0.0. Worker.csproj is on 3.1.1. A shared library nobody has touched since last winter is on something else entirely. The…]]></description><link>https://pourmonfared-azimi.hashnode.dev/central-package-management-in-net-one-file-zero-version-drift</link><guid isPermaLink="true">https://pourmonfared-azimi.hashnode.dev/central-package-management-in-net-one-file-zero-version-drift</guid><dc:creator><![CDATA[Meysam Pourmonfared Azimi]]></dc:creator><pubDate>Tue, 01 Sep 2026 12:53:33 GMT</pubDate><content:encoded><![CDATA[<p>Api.csproj references Serilog 4.0.0. Worker.csproj is on 3.1.1. A shared library nobody has touched since last winter is on something else entirely. The build is green. The tests pass. And then a behaviour differs between two services that were supposed to be running identical code.</p>
<p>Central Package Management (CPM) is NuGet's answer to that. It is not a new package format, not a tool to install, and not a migration you need a sprint for. It is one file, one property, and a handful of build errors that are on your side.</p>
<h3><strong>The File Structure</strong></h3>
<p>Create Directory.Packages.props at the root of the repository:</p>
<pre><code class="language-plaintext">&lt;Project&gt;
  &lt;PropertyGroup&gt;
    &lt;ManagePackageVersionsCentrally&gt;true&lt;/ManagePackageVersionsCentrally&gt;
  &lt;/PropertyGroup&gt;
  &lt;ItemGroup&gt;
    &lt;PackageVersion Include="Serilog" Version="4.0.0" /&gt;
    &lt;PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" /&gt;
    &lt;PackageVersion Include="Newtonsoft.Json" Version="13.0.3" /&gt;
  &lt;/ItemGroup&gt;
&lt;/Project&gt;
</code></pre>
<p>Then strip the versions out of every project file:</p>
<pre><code class="language-plaintext">&lt;ItemGroup&gt;
  &lt;PackageReference Include="Serilog" /&gt;
  &lt;PackageReference Include="Serilog.Sinks.Console" /&gt;
&lt;/ItemGroup&gt;
</code></pre>
<p>PackageVersion declares <em>which</em> version. PackageReference declares <em>who needs it</em>. The two concerns stop living in the same line, and the second one stops being copy-pasted across a dozen files.</p>
<p>The file follows the same MSBuild import rules as <a href="http://Directory.Build"><strong>Directory.Build</strong></a>.props — nearest one wins, walking up from each project — so a large monorepo can scope a subtree to its own set of versions if it truly needs to.</p>
<h3><strong>The four error codes worth knowing by heart</strong></h3>
<p>CPM's real value is that it converts a review comment into a compiler error. Four of them do most of the work:</p>
<p><strong>NU1008</strong> —&gt; someone left a version behind on a project reference:</p>
<blockquote>
<p>Projects that use central package version management should not define the version on the PackageReference items but on the PackageVersion items: Newtonsoft.Json.</p>
</blockquote>
<p>This is the guardrail. Once CPM is on, the old habit of pinning a version inside a .csproj simply stops building. No convention doc required.</p>
<p><strong>NU1010</strong> —&gt; a package is referenced but has no central version:</p>
<blockquote>
<p>The PackageReference items Serilog do not have corresponding PackageVersion.</p>
</blockquote>
<p>You will meet this one a lot during migration, and it is the friendliest error in the set: it hands you the exact list of PackageVersion entries you still have to add.</p>
<p><strong>NU1013</strong> —&gt; a deliberate escape hatch that has been closed:</p>
<blockquote>
<p>The package reference Newtonsoft.Json specifies a VersionOverride but the ability to override a centrally defined version is currently disabled.</p>
</blockquote>
<p>VersionOverride lets a single project deviate from the central version — the pragmatic escape hatch for the one service that genuinely cannot move yet. If you would rather have no escape hatches at all, set CentralPackageVersionOverrideEnabled to false and any attempt to use one fails with NU1013.</p>
<p><strong>NU1109</strong> —&gt; the one people meet after enabling transitive pinning:</p>
<blockquote>
<p>Detected package downgrade: Serilog from 4.0.0 to centrally defined 3.1.1. Update the centrally managed package version to a higher version.</p>
</blockquote>
<p>Which brings us to the feature that makes CPM more than tidy bookkeeping.</p>
<h3><strong>Transitive pinning</strong></h3>
<p>Turn it on with CentralPackageTransitivePinningEnabled:</p>
<pre><code class="language-plaintext">&lt;PropertyGroup&gt;
  &lt;ManagePackageVersionsCentrally&gt;true&lt;/ManagePackageVersionsCentrally&gt;
  &lt;CentralPackageTransitivePinningEnabled&gt;true&lt;/CentralPackageTransitivePinningEnabled&gt;
&lt;/PropertyGroup&gt;
</code></pre>
<p>Now your central versions apply to packages you never referenced directly. If a project pulls in Serilog.Sinks.Console and that package depends on Serilog, the transitively-resolved Serilog is lifted to the version in Directory.Packages.props — without adding a direct PackageReference to the project.</p>
<p>This is the answer to the "we have a CVE in a package three levels down" fire drill. One line in one file, and every project in the repository resolves the patched version.</p>
<p>The trade is NU1109: if a dependency demands a <em>higher</em> version than the one you pinned centrally, the restore now fails instead of silently unifying upward. That is a feature. It surfaces the conflict at the place where the decision belongs.</p>
<p>And note the precedence: with transitive pinning enabled, a VersionOverride on a direct reference still wins for that project. The narrower, more explicit statement beats the broader one.</p>
<h3><strong>GlobalPackageReference</strong></h3>
<p>The other quiet win. In Directory.Packages.props:</p>
<pre><code class="language-plaintext">&lt;ItemGroup&gt;
  &lt;GlobalPackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.400" /&gt;
&lt;/ItemGroup&gt;
</code></pre>
<p>Every project in the repository now references it. This is exactly right for build-time-only concerns (analyzers, source generators, SourceLink) and exactly wrong for runtime libraries, because the reference is not something an individual project opted into.</p>
<p>One line, and the analyzer rollout that used to be twelve pull request comments is done.</p>
<h3><strong>The gotchas</strong></h3>
<p>Three things reliably cost people an afternoon:</p>
<p><strong>Multiple package sources.</strong> If your NuGet.config defines more than one source, CPM will push you toward package source mapping (NU1507). This is a good thing, mapping which packages may come from which feed closes a real supply-chain hole, but it is work you did not plan for on migration day. Check your NuGet.config before you start, not after.</p>
<p><strong>Tooling that still writes versions.</strong> dotnet add package and some IDE dialogs will happily write a Version attribute into your .csproj. The build catches it with NU1008, which is the system working, but expect the papercut until everyone's muscle memory updates.</p>
<p><strong>Half-migrated repositories.</strong> CPM is per-project, not per-repository: a project without ManagePackageVersionsCentrally in scope keeps its own inline versions and quietly stays outside the system. If you migrate in stages, keep a list of what is not yet in, or the whole point is lost to a project nobody remembers.</p>
<h3><strong>The migration, in order</strong></h3>
<ol>
<li><p>Add Directory.Packages.props with ManagePackageVersionsCentrally set to true.</p>
</li>
<li><p>Build. Read the NU1010 list, it is your inventory of every package in the solution.</p>
</li>
<li><p>Add a PackageVersion for each, deciding the single version where two projects disagree.</p>
</li>
<li><p>Delete the Version attributes from the project files. NU1008 tells you the ones you missed.</p>
</li>
<li><p>Move analyzers to GlobalPackageReference.</p>
</li>
<li><p>Enable transitive pinning last, once the direct versions are stable, and resolve any NU1109 conflicts it exposes.</p>
</li>
</ol>
<p>It is an afternoon on a repository of moderate size, and it is the kind of afternoon that pays for itself the first time a security advisory lands.</p>
<hr />
<p><em>All error messages in this article are real messages captured from a .NET SDK 8.0.130 build against a local package feed, not quoted from documentation.</em></p>
<p><strong>What is the messiest version drift you have found in a repository you inherited?</strong></p>
<p>#dotnet #csharp #softwareengineering #CenteralPackageManagement</p>
<p><em>repo :</em> <a href="https://github.com/poormonfared/CPM-Demo"><em><strong>https://github.com/poormonfared/CPM-Demo</strong></em></a></p>
]]></content:encoded></item></channel></rss>