Virtual Developer Workshop: Containerized Development with Docker
If you've done any kind of web development using Visual Studio, you might be familiar with "Config Transformations". If you're not, don't worry. All will be revealed.
Most applications that you create under .NET are accompanied by some kind of configurations file. For web projects, this is usually "Web.Config" and for WPF and desktop apps it's "App.Config"
What you might not realize, however, is that when creating a web project, you actually can split your configuration into separate files, each containing a part that's specific to that profile.
By default, these two profiles are "Debug" and "Release" to match the Debug & Release builds of your web application.
Figure 1: Identifying the "Debug" and "Release" profiles
The idea for this is a very good one. It means that you can place a different set of configuration constraints (such as database connection strings) in each file; you then can have Visual Studio use the appropriate set when debugging or running as Release.
You also can add custom profiles to this, too, so you could, for example, have three profiles: "Development", "Staging", and "Production". Then, you can be able to switch between them as needed in Visual Studio.
If you attempt to do this with a normal application, however, you might be surprised to find that it doesn't work as expected.
For starters, you don't get the nested connection in the config files, and if you do manually create "App.xxxxxxx.config", it won't be linked to the root version of the file.
All is not lost, however, for a recent Windows service project I was working on (actually, part of the code base for the Lidnug website), I needed the option to set three different profiles so the three people working on the project where able to select their own setup easily. After a little bit of research, it turned out to be quite a bit simpler than I thought it might be.
Start off by creating yourself a simple desktop project in Visual Studio. It doesn't matter what it is, as long as it's NOT a web-based application.
As I often do in these posts, I've created a console app, which as you can see has nothing more than a basic setup.
Figure 2: The basic console app
You can add some code to the app if you want. I'm going to add an obligatory "Hello World" output here. The main emphasis, however, is going to be on making changes to the project settings in Visual Studio.
Once you have a project to work with, the first thing you need to do is open the configuration manager by right-clicking your Solution (not the project) and choosing "Configuration Manager" from the menu.
Figure 3: Choosing "Configuration Manager"
By default, the configuration manager only includes options for "Debug" & "Release".
Figure 4: Showing the "Debug" and "Release" options
If that's all you need, we don't actually need to make any changes here. If you want to add other profiles, in the configuration manager, click the drop down for "Active Solution Configuration"; then, select "New".
Figure 5: Selecting "Active Solution Configuration, New"
The "New" dialog will open.
Figure 6: The "New" dialog is open
Give your new configuration a name, and if you want to base the settings on an existing configuration, select the configuration to base it on in the "Copy Settings From" drop down.
When you click OK, and then close the configuration manager, you should see that you now can select your configuration from the normal "Debug/Release" drop down in Visual Studio.
Figure 7: Selecting your configuration
As with any profile, if you go into your project properties, settings, or other panels, you can build sets of project configurations that are specific to that profile.
We're now ready to start altering the project file itself.
Unloading the Project
Right-click the project in your Solution Explorer, and select "Unload Project" from the menu.
Figure 8: Selecting "Unload Project"
Once you do this, your Solution Explorer will update to show that you no longer have access to the Solution Explorer for your project files.
We then need to make some changes, directly to the project definition. So, once again, right-click the project; this time, choose to edit the project file.
Figure 9: Choosing to edit the file
This will open the XML contents for the actual project file into your Visual Studio Editor, which, if you've never looked at one before, might seem quite intimidating, but don't worry. It's really not as scary as it looks.
As you scroll up/down through the file, you'll recognise settings you've made for your projects, and while you can change things here, I'd recommend that you don't because often, the settings here are also synchronised with other parts of Visual Studio's various editors.
The bit we're actually going to change is not edited anywhere else in Visual Studio, however, so scroll to the very bottom of your project file, and find the part of the file that looks like this:
Figure 10: Isolating the "Project" section
We're going to add to this a target called "AfterCompile". This target will be run once the compilation and build of your project has successfully completed. This rule will be set up to use the same files and build assemblies that a web project usually uses to perform the same task.
First, however, a little bit of a warning....
If you're going to make this modification, and then commit the changes to some kind of version control, you MUST make sure that everyone working with this solution has a copy of the assemblies from the correct version your building with.
The disk path you'll be using to load the assemblies is dependent on the version of Visual Studio you are using. In this example, I'm using VS2013, so the path to my Build assemblies is as follows:
Notice, it's the 'v12.0' that's important. This is different for each version of Visual Studio, 12.0 being the correct one for VS2013. For others, you'll need to check your drive where VS is installed to find the correct one, and then change it as needed.
You can, if you want to, actually put the DLL that you need from this path into your project/solution folders anywhere you like, and make sure that the build system can reference it when you define the path. But, if you're anything like me, you'll most likely need to add some rules to your version control to allow binary files to be committed.
If you're not using version control, you can ignore this advice as long as you remember that uninstalling old versions when you do an upgrade is likely to break your projects, thus requiring you to fix them manually.
Once you're happy about paths and such like, the file you actually need to reference is:
and you can achieve this by adding the following code to your project file, just after the "Import Project" line:
<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\ VisualStudio\v12.0\Web\Microsoft.Web.Publishing.Tasks.dll" /> <Target Name="AfterCompile" Condition="exists('app.$(Configuration).config')"> <!-- Generate transformed app config in the intermediate directory --> <TransformXml Source="app.config" Destination="$(IntermediateOutputPath)$(TargetFileName).config" Transform="app.$(Configuration).config" /> <!-- Force build process to use the transformed configuration file from now on. --> <ItemGroup> <AppConfigWithTargetPath Remove="app.config" /> <AppConfigWithTargetPath Include="$(IntermediateOutputPath)$(TargetFileName).config"> <TargetPath>$(TargetFileName).config</TargetPath> </AppConfigWithTargetPath> </ItemGroup> </Target>
After you add it, the last section of your project should look something like this:
Figure 11: After adding the preceding portion of code
If it does, save the file, right-click your project, and "Reload" it back into Visual Studio.
Figure 12: Preparing to reload the project
At this point, you should be able to build your project, and see no errors or fails. If you do, the first thing to check is the path to the build assembly as mentioned earlier. If your build works, then congratulations. All you need to do now is just create the XML config files you need.
The one thing you still won't get, however, is the "Nested Appearance" (as shown in Figure 1), but you will still get to create files that contain your own processing actions for each profile.
For this tutorial, I kept the "Debug" and "Release" builds, and I added a "ShawtyDebug" build too, so let's now right-click our project and "Add New Item" from the menu.
Figure 13: Adding a new item
The file type you need is "Application Configuration File", and you need to name it as:
Replace <Profile> with "Release", "Debug", "ShawtyDebug", or whatever the name of the profiles you've created are.
Figure 14: The new config file has been added
Once you're done, they should appear in the Solution Explorer as regular files.
Figure 15: The new files are visible in Solution Explorer
By default, Visual Studio will add the following content to these new files:
<?xml version="1.0" encoding="utf-8" ?> <configuration> </configuration>
For the transformations to be recognized, you'll also need to make sure these files reference the correct namespace because VS won't add that in automatically for you. Make sure you change these new files so they look as follows:
<?xml version="1.0" encoding="utf-8" ?> <configuration xmlns:xdt="http://schemas.microsoft.com/ XML-Document-Transform"> </configuration>
You then can continue to add configuration information to them.
The Final Part: Adding Some Content to the Configurations
A typical use case for doing this is to use a different database connection for debug than for release. In my case, I also had settings that I needed to maintain that told the app which IP addresses to listen on.
Whatever you choose to do, remember that anything in the main "App.Config" file will be the default if no rules to perform any transforms can be found. Open "App.Config" in Visual Studio and we'll add a dummy connection string, which we'll call our Default Connection string.
Make sure your "App.Config" includes this section:
<connectionStrings> <add connectionString= "This is the default connection string" name="MyConnection" providerName="Some.Provider.Assembly"/> </connectionStrings>
This will be the default connection string.
Next, open "App.Debug.Config" and add the following between the configuration tags:
<connectionStrings> <add connectionString= "This is the debug connection string" name="MyConnection" providerName="Some.Provider.Assembly"/> </connectionStrings>
As it stands, if you compile this in a debug build, the connection string won't be replaced. However, to make this work, you need to modify the addition you made to the "App.Debug.Config" file slightly.
We need to add a couple of transformation commands as follows:
<connectionStrings> <add connectionString= "This is the debug connection string" name="MyConnection" providerName="Some.Provider.Assembly" xdt:Transform="Replace" xdt:Locator="Match(name)"/> </connectionStrings>
What these two rules say is "match the attribute called name, and then replace the entire found tag with this one." This will mean that this entire element starting at "<add" will get replaced in your default "App.Config" file.
If you add these transforms to an element that has further elements inside of it, the entire nested set of elements will also be replaced.
There are a lot of different rules you can use here, but rather than list them all, I'll just advise you to read the following MSDN page:
where you'll find all the details.
If you now make similar changes with the transform tags to each of your other files, you'll find that whichever profile you now choose when building your app will be merged with the default when creating the output. Once in place, it works really well for the settings section.
If you use the settings editor within your application properties to set some settings, copy those elements into the appropriate Debug/Release/Profile versions of your config and edit the values as needed. You also can swap between different settings applied directly to other parts of your code, too. I often use this for example to set Self Hosting URLs for things like NancyFX, Owin & Web-API.
Seen something you can't explain in .NET, or simply just want a refresher on something you've not used for a long time? Come find me on Linked-in in the Lidnug user group, or leave me a comment below and I'll see if I can do a post on the subject.