Recently I upgraded a .NET Framework application to .NET (Core1). This line-of-business application was published using ClickOnce to a file share at the customer’s network. As a lot of my applications do (or did), by the way. Thanks to the Visual Studio team’s Upgrade function, the actual upgrade from, in this case, Framework 4.5 (!) to .NET 8 (LTS) was not too hard, apart from fixing a whole bunch of dependencies.
But, it turns out there are some caveats when you’re using ClickOnce.
SDK-style project files
Well, first of all I thought it was a good idea to update the project files to those nice new SDK-style .csproj files first. This can be done independently of the .NET upgrade. Since the new format is a lot more concise and better human-readable, I thought that would assist my updating the dependencies and other stuff later. That is true, but mind, that it will break your ClickOnce publishing!
Caveat #1. SDK-style project can be published using ClickOnce, but only for .NET (Core) applications, not for .NET Framework.
So, if you thought about upgrading in stages, pay attention that converting to SDK-style projects breaks your publish until you’ve completed upgrading all projects from Framework to Core as well. If you still require publishing the Framework app, stick with the old-style .csproj files until you’re ready to update both at once.
Configuration per environment
In almost all my .NET Framework ClickOnce projects I am using a small trick to set a different app.config for the published application. The regular app.config, usually in the root of the project folder, holds the configuration for my debug environment so that when I run the application in the IDE debugger I have the configuration that I need.
When publishing to the customer’s server, I stream the production environment config into the project by adding that as a separate app.config file in a subfolder of the project. Example structure:
Project\
+--Deploy\
| +--Prod\
| | +--App.config // production config
|
+--App.config // debug config
Of course, I do not want all of those in the project output at the same time, so I edited the project file once manually to add conditions, like this:
<ItemGroup Condition=" '$(Configuration)' == 'Release' ">
<None Include="Deploy\Prod\App.config" />
</ItemGroup>
<ItemGroup Condition=" '$(Configuration)' != 'Release' ">
<None Include="App.config" />
</ItemGroup>
When building in Release config, the deploy\prod\app.config file gets included, in all other configurations it is app.config. (You could add easily add more configurations to your project if you require more environments.) Once set-up, this is completely transparent to the IDE. In the Solution Explorer you see both files, but when building, only one gets included in the build. As long as you pick the Release config to be the one to publish with ClickOnce (as you should) this works like a charm.
Caveat #2. ItemGroup conditions do not work for app.config files in SDK-style projects.
Well, first of all, after upgrading to SDK-style projects, both ItemGroups had disappeared from the project file completely, removed by the upgrade tool. That made sense, as these SDK-style projects sort of “auto include” everything in the project directory. So the files still both showed up in the Solution Explorer as before and things looked okay.
My first idea was to simply re-add the above xml to the project file. Being explicit overrides the default behavior. This did not have the effect I intended. No matter the project config, I still always ended up with the debug configuration in the output directory, even for release mode.
Turns out, SDK-style projects have a special tag in a PropertyGroup for app.config files, aptly named <AppConfig> (did you guess?). It allows you to specify the app.config file to use. If not present in the project file, it defaults to the app.config in the root, which is why it is rarely present in a more-or-less default project. So I added
<AppConfig>App.config</AppConfig>
in my .csproj file. Which changed precisely nothing because it was the default anyway.
But! If you move that line into a <PropertyGroup> of it’s own, you can add conditions to the property group again.
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<AppConfig>Deploy\Prod\App.config</AppConfig>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' != 'Release' ">
<AppConfig>App.config</AppConfig>
</PropertyGroup>
Et voilà! When building in Release mode, the production config got added again instead of the default.
App.config change detection
In reality the fix above took me a while longer to discover because of another issue with app.config files, change detection. If you do not change anything else and just build again, the compiler does not always pick up changes to the app.config file and keeps the existing one. Which led me to think my fixes did not work at first.
Caveat #3. Fast up to date detection does not reliably detect config changes.
One can circumvent this by adding this to the project file (for my example):
<ItemGroup>
<UpToDateCheckInput Include="App.config" />
<UpToDateCheckInput Include="Deploy\Prod\App.config" />
</ItemGroup>
This notifies the “fast up to date check” to keep an eye on these files for changes.
Anything else?
Anything else you have run into upgrading .NET Framework applications that use ClickOnce to .NET and/or SDK-style project files? Let me know in the comments.
- I know it’s just “.NET” nowadays, but I’ll be calling it “.NET Core” or “Core” in this post sometimes, to better distinguish it from .NET Framework. ↩︎


Geef een reactie