Enforce Visual Studio Build Action in Projects – dotnet core Edition

In a previous post I described a method for enforcing a certain msbuild BuildAction on a sub-set of the project’s files, generating a build error if the expected action was not set. I now wanted to use this in a dotnet core project and with a minor tweak, it works just the same.

The use case is still the same, I’m using the lovely DbUp to perform DB schema migrations during deployments as part of an autonomous CI/CD setup. A pull-request which was recently merged contained SQL files which were not flagged as EmbeddedResources, meaning they were not picked up and executed (lucky for them!), resulting in a deployment failure. To prevent this happening again, I wanted to add the same check in as before.

I began by checking to see whether everything I needed was supported in the dotnet core world and luckily, support for XmlPeek (and XmlPoke) was added earlier this year. Woot. I created a new branch, opened up the project file, copy/pasta the old target and build. No errors. Good! Next I changed one of the SQL files to have a BuildAction of None, build, no errors. Not good.

After some trial and error, I discovered that the dotnet core .csproj files no longer use the XML namespace (http://schemas.microsoft.com/developer/msbuild/2003) that their older siblings use. I couldn’t find a reason why, but the MS project migration documentation simple states to remove them. Fair enough.

The updated target for a dotnet core project is below. Simple replace anything ‘SQL’ related to suit your own needs.

<Target Name="EnsureSQLScriptsAreEmbeddedResource" BeforeTargets="CoreCompile">
  <XmlPeek XmlInputPath="$(MSBuildProjectFile)" Query="Project/ItemGroup/*[not(self::EmbeddedResource)]/@Include">
    <Output TaskParameter="Result" ItemName="AllItems" />
  </XmlPeek>
  <ItemGroup>
    <Filtered Include="@(AllItems)" Exclude="SqlTemplate.sql" Condition="'%(Extension)' == '.sql'" />
  </ItemGroup>
  <Error Code="SQL" ContinueOnError="ErrorAndContinue" File="$(MSBuildProjectDirectory)\%(Filtered.Identity)" Text="All scripts require a BuildAction of EmbeddedResource" Condition="'@(Filtered)'!=''" />
</Target>

Maintaining Application Settings for an Azure Functions Project

Before you get too excited by the title of this post, it’s not good news. Yet. I’ve been looking for a decent workflow to solve this problem and am turning up blanks, which means there’s only one thing left: I’ll have to define one.

Right off the bat, there seems to be a lot of confusion at the moment around application settings in Azure Functions, mostly due to the rate of change in the Azure ecosystem. Even more so because the configuration model for Azure Functions is handled rather differently to App Service applications. I haven’t dug deep enough in to the architecture to understand why, but am guessing there’s a fairly fundamental reason for this.

For clarification:

  • appsettings.json is not used for deployed functions. This was only ever used for local development and has since been renamed to local.settings.json
  • web.config is not used. Refer to the folder structure for a Functions application
  • host.json can not be used to store application settings

The problem I’m trying to solve here is to find a clean way of managing an Azure Functions app’s application settings over time. For an App Service application, you can simply add an entry to appsettings.json, configure any overrides for your deployment environments and off you go. With Functions there’s no such luck. On day one, you can create a neat little ARM template to handle your infrastructure setup, including app settings, but what happens when you want to add one later? I’ve seen a few different solutions which basically all come down to hacks.

As a last resort, tonight I did a final search in the Azure Functions GitHub repository tickets and found a ticket detailing the issue that I assume many people are having. Well by the looks of it we’ll have an answer tomorrow at 20:00 CEST! Here’s a direct link to the live stream.

Update: From the webcast, the bottom line is that there are plans to enhance the process, but for now keep using the CLI. Great.

Deploy an ASP.Net Core 2.0 Application to an Azure App Service With Bitbucket Pipelines

Recently I took another look at Bitbucket’s Pipelines offering, their successor to Bamboo cloud which was removed last year (but lives on as an on-premise solution). Pipelines had previously been uninteresting to me as there was no support for traditional .NET apps, but now that I’m dealing more with .NET Core, I figured it would be worth a look. As it turns out, I abandoned this approach almost as soon as I had things up and running, choosing to go with the built in support offered by Azure App Services. A guide on that will appear soon.

The premise is cool, builds are run in Docker containers, defined using YAML and you get 50 build minutes included in the free account, with all the triggers and notifications you’d expect, e.g. allowing you to fire off Slack notifications to keep your team informed of the build health. If your code is sitting in a Bitbucket repo, it makes sense to try and keep everything under one roof (always with an alternative solution of course, you never know when that roof will collapse!).

I started off by following the guide provided by Atlassian and the YAML from this repository which immediately resulted in errors. Getting the build definition in to shape took a bit of messing around so I decided to write this post in the hopes that a) you can laugh at the silly mistakes I made along the way and b) provide a more up to date and comprehensive guide to deploying an application to the Azure App Service using Bitbucket Pipelines.

Trials and Errors

Problem #1 – Builds not being triggered

Cause: Incorrect casing of the branch name in the build definition file, ‘Development’ vs. ‘development’ (who would name the development branch ‘Development’ anyway?!).

Solution: Correct the casing in the bitbucket-pipelines.yml file.

Problem #2 – Error building the solution

+ dotnet build $PROJECT_NAME
Couldn't find 'project.json' in 'PROJECT_NAME'

Cause: This was the result of project.json being a dotnet core 1.0 feature which is now deprecated in version 2.0.

Solution: As this is a dotnet core 2.0 project, I needed to use a different Docker image, and changed the Docker image from

image: microsoft/dotnet:onbuild

to

image: microsoft/dotnet:2.0-sdk

Problem #3 – Error restoring Nuget packages

  + dotnet restore
  MSBUILD : error MSB1003: Specify a project or solution file. The current working directory does not contain a project or solution file.

Cause: It would appear that at some point between version 1 and 2, an error is raised instead of a warning when the restore command is unable to find a project/solution file. The repository was structured such that the solution was in a folder off the root directory.

Solution: Pass in the path to the solution file to the restore command. E.g.

- dotnet restore FolderWithSolution/MySolution.sln

Problem #4 – Error pushing to the Azure App Service Git repository

  + git push https://$AZURE_LOGIN:$AZURE_PASSWORD@appservicename.scm.azurewebsites.net/appservicename.git master
  error: src refspec master does not match any.
  error: failed to push some refs to 'https://username:$AZURE_PASSWORD@appservicename.scm.azurewebsites.net/appservicename.git'

This tripped me up as I hadn’t taken the time to process what the error was telling me and ended up on a bit of a wild goose chase, resulting in me cloning the App Services Git repository locally and creating a master branch. Note: Do not do this, jump to #6 to see why.

Cause: The build was being executed on the Development branch, but I was trying to push code to the App Services’ master branch.

Solution: Change the Git push command, specifying the source and destination branch names:

git push https://$AZURE_LOGIN:$AZURE_PASSWORD@appservicename.scm.azurewebsites.net/appservicename.git Development:master

Problem #5 – Error pushing to the Azure App Service Git repository, take two

+ git push https://$AZURE_LOGIN:$AZURE_PASSWORD@appservicename.scm.azurewebsites.net/appservicename.git Development:master
  fatal: unable to access 'https://username:$AZURE_PASSWORD@appservicename.scm.azurewebsites.net/appservicename.git/': Illegal port number

Well that’s weird! HTTPS is pretty standard after all no? Just for good measure I went and changed the Git push command again, adding the port in:

git push https://$AZURE_LOGIN:$AZURE_PASSWORD@appservicename.scm.azurewebsites.net:443/appservicename.git Development:master

Alas the error persisted.

Cause: Embarassingly, the value for the $AZURE_PASSWORD environment variable contained a colon. Oh dear, schoolboy error! That’s what you get for trying to generate a super secure password.

Solution: Change the Azure App Services deployment password such that it doesn’t contain any characters that will break the URL.

Problem #6 – Error pushing to the Azure App Service Git repository, take three

  + git push https://$AZURE_LOGIN:$AZURE_PASSWORD@appservicename.scm.azurewebsites.net:443/appservicename.git Development:master
  To https://appservicename.scm.azurewebsites.net:443/appservicename.git
   ! [rejected]        Development -> master (fetch first)
  error: failed to push some refs to 'https://username:$AZURE_PASSWORD@appservicename.scm.azurewebsites.net:443/appservicename.git'
  hint: Updates were rejected because the remote contains work that you do
  hint: not have locally. This is usually caused by another repository pushing
  hint: to the same ref. You may want to first integrate the remote changes
  hint: (e.g., 'git pull ...') before pushing again.
  hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Cause: Sigh. In order to create the master branch in problem #4, I had to push a commit, which meant that the repository had changes that would need to be merged. Something felt very wrong here so I decided to reset the Azure App Service Git repository, which is easier said than done.

Solution: I ended up deleting and re-creating the App Serivce, using the same details as before so that I wouldn’t have to go through and update my build definition and environment variables.

Success

At last, I saw a nice green build indicator. Finally! I’d eaten through 13 of those precious 50 build minutes by this point, but it was working and the builds were relatively quick, taking around 3 minutes apiece. Here’s what the bitbucket-pipelines.yml looked like at this point:

image: microsoft/dotnet:2.0-sdk

pipelines:
  branches:
    Development:
      - step:
          caches:
            - dotnetcore
          script:
            - dotnet restore SolutionName/SolutionName.sln
            - dotnet build SolutionName/ProjectName
            - git push https://$AZURE_LOGIN:$AZURE_PASSWORD@appservicename.scm.azurewebsites.net:443/appservicename.git Development:master

Once the code was being deployed, I hit the next issue. This repository contained a solution with two ASP.NET Core web apps and I couldn’t easily figure out how to specify which project to deploy. Deploying the entire build output would always result in the first website being active. At this point, due to other checkins, all of those 50 build minutes were used up! Towards the end, when building two projects, the builds were hitting 9 minutes. Rather than pony up the $10 for another 1000 minutes, I took my search elsewhere.

The Recipe for Success

If you want to give Bitbucket Pipelines a spin and would like to avoid my mistakes, follow this guide. These steps assume three things:

  1. You already have your app code sitting in a Bitbucket repository.
  2. You have an active Azure subscription and have already created your App Service.
  3. Your .NET Core solution has a single project you wish to deploy.

This guide will bypass the Bitbucket Wizard for adding the bitbucket-pipelines.yml file to your repository as it only allows you to commit this directly to your master branch. If that’s ok with you, then follow the wizard (just access the Pipelines for your repository and follow the steps), however the steps below will result in the same outcome.

Please refer to the Atlassian documentation for the bitbucket-pipelines.yml file specification.

Note: Pipelines will only run for branches in which the bitbucket-pipelines.yml file exists and are defined in the build file (unless you only use a default configuration). Depending on your Git workflow, you may want to add this file to a development branch first, then propagate it to other branches by merging.

  1. Create a new text file, in the root of your repository on the desired branch, with the name bitbucket-pipelines.yml
  2. Paste in the following content:
    image: microsoft/dotnet:2.0-sdk
    
    pipelines:
      branches:
        BRANCH_NAME:
          - step:
              caches:
                - dotnetcore
              script:
                - dotnet restore SOLUTION_FOLDER/SOLUTION_NAME.sln
                - dotnet build SOLUTION_FOLDER/PROJECT_NAME
                - git push https://$AZURE_LOGIN:$AZURE_PASSWORD@APP_SERVICE_NAME.scm.azurewebsites.net:443/APP_SERVICE_NAME.git BRANCH_NAME:master
    
  3. Go through and replace the values for the following to match your setup:
    • BRANCH_NAME
    • SOLUTION_FOLDER
    • SOLUTION_NAME
    • PROJECT_NAME
    • APP_SERVICE_NAME
  4. If you don’t have multiple branches to deal with, you can omit the branches: and branch name (BRANCH_NAME:) lines, replacing them with default:
  5. Commit the file and push to your remote
  6. If you did everything right so far, you should be able to navigate to the Pipelines section of your Bitbucket repository, scroll down and see the contents of your bitbucket-pipelines.yml file in the validator. If there are any validation errors, correct them now
  7. Click the Enable button Enable Bitbucket Pipelines
  8. To define the username and password that will be used to push to your App Service’s Git repository, you have two options. Follow this guide to set those credentials.
  9. With the credentials in place, open the repository in Bitbucket and navigate to Settings -> Pipelines -> Environment Variables
  10. Add a new variable called AZURE_LOGIN and set the value to your deployment username. Mark this as secured if you like
  11. Add a second variable called AZURE_PASSWORD, setting the value to your deployment password. It’s recommended that you mark this as secured to prevent it appearing in logs
  12. Commit a change to the branch you added the bitbucket-pipelines.yml file to and you should be all set! If you hit any errors, check out the list of problems I ran in to and see if you’re experiencing the same. Otherwise, hit up Google, or ask a question in the comments below

I use a VPN so I’m totally anonymous! Actually, you’re probably not…

It’s generally regarded that if you’re using a VPN, you’re safe and anonymous. That no one can see where you are located or trace requests back to you (assuming your provider doesn’t keep server logs). I’ve always kept in the back of my mind that during the course of casual browsing, with or without a VPN, you’re leaking a lot of information but I’ve never dived in to the particulars.

I’m on the road a lot, clocking up some 160,000 km last year. As such I need to use a static IP otherwise I’m constantly having to adjust whitelists, which isn’t always feasible. I also have to connect to unsecured networks in hotels and airports, so naturally VPNs are my best friend. Earlier today I decided to do a quick test to see just how anonymous a VPN keeps you. As it turns out, you can use a VPN and not actually benefit from it, with the exception of your ISP not being able to see what you’re up to.

Continue reading

Can .Net Class Libraries Utilise app.config files?

A few months back I was migrating a .Net solution with 110 projects from .Net 4.5 to .Net 4.6.2 as well as upgrading the Azure SDK. Each project came with its own set of NuGet packages. As you can guess, there were a decent number of packages that needed to be upgraded. Given that the whole solution required testing after the upgrade, then was as good a time as any to bring everything up to date.

When it was time to merge this branch back to the main development branch, there were a number of conflicts, mostly in app.config files belonging to class library projects. Virtually every class library project in the solution had an app.config which didn’t sit right with me. It was something I’d often noticed but never really given any thought to, until now that it was causing some pain.

Does an app.config file in a class library project do anything?

In a nutshell, no, but with a caveat. Configuration files have application wide scope, not assembly wide scope (ignoring machine configurations and publisher policy files). Even though the configuration files contain assembly binding redirects and other important things, they’re really not used (unless you have code to load settings from a local configuration file). It’s one of those things that you know the answer too, but there is a shadow of a doubt in there.

So why do these files appear?

Continue reading

NLog Timestamp Accuracy vs Performance

During a recent debugging session involving some juicy threading issues, the logs were showing some scary stuff. Given that we’re talking about threading issues, order of operations and timing is everything. And the operations were all out of order! When the log entries were ordered by record ID you have one sequence of events yet when ordered by timestamp you have another sequence. In terms of the desired behaviour, one is spot on (when sorted by ID), the other is very wrong.

Continue reading

SQL Error SQL72045 When Importing a BACPAC

Problem

You receive the following error when trying to import a BACPAC that was generated on a different server:

TITLE: Microsoft SQL Server Management Studio
------------------------------

Could not import package.
Warning SQL0: A project which specifies Microsoft Azure SQL Database v12 as the target platform may experience compatibility issues with SQL Server 2014.
Error SQL72014: .Net SqlClient Data Provider: Msg 12824, Level 16, State 1, Line 5 The sp_configure value 'contained database authentication' must be set to 1 in order to alter a contained database.  You may need to use RECONFIGURE to set the value_in_use.
Error SQL72045: Script execution error.  The executed script:
IF EXISTS (SELECT 1
           FROM   [master].[dbo].[sysdatabases]
           WHERE  [name] = N'$(DatabaseName)')
    BEGIN
        ALTER DATABASE [$(DatabaseName)]
            SET CONTAINMENT = PARTIAL 
            WITH ROLLBACK IMMEDIATE;
    END

Error SQL72014: .Net SqlClient Data Provider: Msg 5069, Level 16, State 1, Line 5 ALTER DATABASE statement failed.
Error SQL72045: Script execution error.  The executed script:
IF EXISTS (SELECT 1
           FROM   [master].[dbo].[sysdatabases]
           WHERE  [name] = N'$(DatabaseName)')
    BEGIN
        ALTER DATABASE [$(DatabaseName)]
            SET CONTAINMENT = PARTIAL 
            WITH ROLLBACK IMMEDIATE;
    END

 (Microsoft.SqlServer.Dac)

Solution

Execute the following T-SQL on your local instance:

sp_configure 'contained database authentication', 1;  
GO  
RECONFIGURE;  
GO  

Background

Initially I thought this error was arising due to enabling Transparent Data Encryption (TDE) on SQL Azure databases, but in actual fact it was down to switching to database level authentication rather than relying on server level logins.

A side-effect of this change was that the database is now considered to be (partially) contained, i.e. it has no hard dependencies on master. Read more on Contained Databases here. Support for contained databases is disabled by default as they have security implications. From the contained database authentication Server Configuration Option MSDN entry:

When contained databases are enabled, database users with the ALTER ANY USER permission, such as members of the db_owner and db_accessadmin database roles, can grant access to databases and by doing so, grant access to the instance of SQL Server. This means that control over access to the server is no longer limited to members of the sysadmin and securityadmin fixed server role, and logins with the server level CONTROL SERVER and ALTER ANY LOGIN permission. Before allowing contained databases, you should understand the risks associated with contained databases.

RDP With a Retina / HDPI Display

If you own a device with a high-resolution display and ever find yourself using RDP to connect to a remote machine, chances are you’ve hit the problem of the RDP client not honouring your scaling settings and displaying the remove machine with the full resolution. Not sure what I mean? Take a look at this…

Remote machine over an RDP instance

There are myriad solutions out there talking about either using mRemoteNG or an outdated version of Remote Desktop Connection Manager (the solution works with v2.2, the latest, at the time of writing, being v2.7). So what do?

Use the latest version of Remote Desktop Connection Manager and disable the compatibility option: ‘Disable display scaling on high DPI settings’.

  1. After installing Remote Desktop Connection Manager, right click on program and select Properties
  2. Select the ‘Compatibility’ tab
  3. Uncheck the setting ‘Disable display scaling on high DPI settings’
  4. Enjoy headache free RDP sessions!

Disable display scaling on high DPI settings

Note, you’ll still need to use RDCM to initiate the connection as this won’t magically apply to the standard Remote Desktop Connection program.

TypeLoadException when using SqlColumnEncryptionAzureKeyVaultProvider

Problem

An exception is thrown when trying to use the SqlColumnEncryptionAzureKeyVaultProvider for working with Always Encrypted columns in SQL Azure.

An unhandled exception of type 'System.TypeLoadException' occurred in Hyak.Common.dllInheritance security rules violated by type: 'System.Net.Http.WebRequestHandler'. Derived types must either match the security accessibility of the base type or be less accessible.

Solution

There’s an issue with the 4.1 version of System.Net.Http assembly. Downgrade to version 4.0.

Background

I’m working on a data migration project that involves moving encrypted data over to Always Encrypted columns. The first part of getting that up and running is to get the Always Encrypted SQL database driver up and running. I’ve worked a fair bit with the Azure Key Vault so was surprised to see the exception detailed above when first running my application. The stack trace wasn’t overly helpful:

   at Microsoft.Azure.Common.Platform.HttpTransportHandlerProvider.CreateHttpTransportHandler()
   at Hyak.Common.ServiceClient`1..ctor()
   at Microsoft.Azure.KeyVault.Internal.KeyVaultInternalClient..ctor()
   at Microsoft.Azure.KeyVault.Internal.KeyVaultInternalClient..ctor(KeyVaultCredential credentials)
   at Microsoft.Azure.KeyVault.KeyVaultClient..ctor(AuthenticationCallback authenticationCallback)
   at Microsoft.SqlServer.Management.AlwaysEncrypted.AzureKeyVaultProvider.SqlColumnEncryptionAzureKeyVaultProvider..ctor(AuthenticationCallback authenticationCallback) in d:\DsMain\DS_Main\Sql\mpu\shared\Security\AzureKeyVaultProvider\SqlColumnEncryptionAzureKeyVaultProvider.cs:line 56
   at AlwaysEncryptedKeyVaultIssue.Program.InitializeAzureKeyVaultProvider() in C:\Dev\AlwaysEncryptedKeyVaultIssue\AlwaysEncryptedKeyVaultIssue\Program.cs:line 21
   at AlwaysEncryptedKeyVaultIssue.Program.Main(String[] args) in C:\Dev\AlwaysEncryptedKeyVaultIssue\AlwaysEncryptedKeyVaultIssue\Program.cs:line 44
   at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
   at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
   at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStart()

After a fair bit of Googling, I eventually came across up a fairly fresh Microsoft Connect ticket detailing the issue.

The issue really is not obvious and hats off to ‘lucavgobbi’ for diagnosing the root cause! Luckily it’s quite simple to remedy, although there are a few caveats.

For the purpose of this post, I created a simple test project that demonstrates the issue. It’s very easy to create using the following steps.

  1. Create a new console application (targeting 4.6.1)
  2. Install the following 2 Nuget packages:
    • Install-Package Microsoft.SqlServer.Management.AlwaysEncrypted.AzureKeyVaultProvider
    • Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory
  3. Copy this example Program.cs from the demo source.
  4. Run the app and watch it explode!

On running the application, you’ll be presented with the lovely exception from above. So what do?

As described in the Microsoft Connect issue, we need to switch the version of System.Net.Http from v4.1 to v4.0. It’s important to note that this dependency is added via Nuget as a dependency of which is the first gotcha. Run the following command to downgrade the package:

Update-Package System.Net.Http -Version 4.0.0

Run the application again and BOOM, it still fails… Ok…. The problem here lies in your app / web confg (App.config or web.config). An assembly binding redirect is added when the package is installed, redirecting to version 4.1.0, totally subverting our package downgrade. This is again simple to fix, change the dependency from this:

<dependentAssembly>
    <assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
    <bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.1.0.0" />
</dependentAssembly>

To look like this:

<dependentAssembly>
    <assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
    <bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.0.0.0" />
</dependentAssembly>

Now you’re good to go!

Browse the demo source code on GitHub.

Source Code for the application’s Program.cs:

namespace AlwaysEncryptedKeyVaultIssue
{
    using Microsoft.IdentityModel.Clients.ActiveDirectory;
    using Microsoft.SqlServer.Management.AlwaysEncrypted.AzureKeyVaultProvider;
    using System;
    using System.Collections.Generic;
    using System.Data.SqlClient;
    using System.Threading.Tasks;

    public class Program
    {
        private static ClientCredential clientCredential;

        static void Main(string[] args)
        {
            InitializeAzureKeyVaultProvider();
        }

        public static void InitializeAzureKeyVaultProvider()
        {
            var clientId = Guid.NewGuid().ToString();
            var clientSecret = "ITS_A_SECRET";

            clientCredential = new ClientCredential(clientId, clientSecret);

            var azureKeyVaultProvider = new SqlColumnEncryptionAzureKeyVaultProvider(GetToken);

            var providers = new Dictionary()
            {
                { SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, azureKeyVaultProvider }
            };

            SqlConnection.RegisterColumnEncryptionKeyStoreProviders(providers);
        }

        public async static Task GetToken(string authority, string resource, string scope)
        {
            var authContext = new AuthenticationContext(authority);
            AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCredential);

            if (result == null)
                throw new InvalidOperationException("Failed to obtain the access token");

            return result.AccessToken;
        }
    }
}

Enforcing BuildAction on VS Project Items

Whilst creating an application to run the excellent SQL migration tool DbUp as part of a continuous deployment setup, I stumbled across a question I hadn’t thought about before. Is it possible to enforce a particular build action on certain files in a Visual Studio project? In this particular case, making sure all SQL files are marked as EmbeddedResource.

With DbUp, there are several ways of supplying SQL files to be run during a migration. In my case, I don’t know which set of scripts need to be run until the deployment is underway. After toying around with several ideas, I settled on the approach of embedding the SQL files in to the migration tool. I can then use the connection string that’s passed in to determine a specific folder of scripts to be executed. Having the scripts as embedded resources will allow me to package up the migration tool as an artefact that can be run only when needed, i.e. during a deployment and not during a build.

So how does it work?

There are several ways of adding custom targets to a project file. After Googling around I came across the ever helpful StackOverflow which offered up some solutions. The first approach is simple, but would require you to add a case for each of the BuildAction types that exist. And if a new one is added at some point, you have to update your code. No thanks, I’m too lazy for that.

The second approach looked much more maintainable by focusing on everything that isn’t what you want. The problem was that it didn’t work! Nothing would ever be matched. I wasn’t able to see any information in Visual Studio, so ran the build from the command line which told me the target was being run, but no elements were matched.

After lots of trial and error and head scratching, I came across another StackOverflow post detailing that you need to specify the namespace for elements in the XPath query. Of course!

The following code is placed in to the project file that you wish to run the check in.

<Target Name="EnsureSQLScriptsAreEmbeddedResource" BeforeTargets="BeforeBuild">
    <XmlPeek 
        XmlInputPath="$(MSBuildProjectFile)" 
        Namespaces="&lt;Namespace Prefix='msb' Uri='http://schemas.microsoft.com/developer/msbuild/2003'/&gt;" 
        Query="/msb:Project/msb:ItemGroup/*[not(self::msb:EmbeddedResource)]/@Include">
            <Output TaskParameter="Result" ItemName="AllItems" />
    </XmlPeek>
	
    <ItemGroup>
        <Filtered Include="@(AllItems)" Exclude="SqlTemplate.sql" Condition="'%(Extension)' == '.sql'" />
    </ItemGroup>
    <Error 
        Code="SQL" 
        ContinueOnError="ErrorAndContinue"
        File="$(MSBuildProjectDirectory)\%(Filtered.Identity)" 
        Text="All scripts require a BuildAction of EmbeddedResource" 
        Condition="'@(Filtered)'!=''" />
</Target>

What’s going on here then?

  1. The XmlPeek task will take the project file and run the XPath query, collecting all matching attributes and sticking them in to a new parameter called AllItems.
  2. The XPath query works by looking at child elements of each ItemGroup element, matching against any that are not of type EmbeddedResource.
  3. For each of these, it will take the value of the Include attribute and use that to populate AllItems.
  4. The ItemGroup that follows will filter the AllItems collection to include only things which end with ‘.sql’. These will be stored in a new parameter called Filtered.
  5. Finally, the Error target will be invoked if any items made it through. Including the ContinueOnError attribute makes it possible to see all of the files that cause the build to fail, rather than stopping on the first one.

Props to StackOverflow users jessehouwing and Teun D for getting me on the right path!