Monday, November 27, 2017

How to multi-target a .NET Core class library

I am developing an OWIN middleware library and I wanted to build a single set of source files for multiple .Net Platforms/version. My first thought WHICH IS VERY WRONG was to create multiple csproj files and then manually create the nuspec file that would bring them all together.

It turns out I was going to do way too much work. It is actually very simple, the worse part is that one must manually edit the csproj file itself.

Suppose we start with a very basic csproj file. This one is from a project that builds a piece of OWIN Middleware.


<project sdk="Microsoft.NET.Sdk"> <propertygroup> <targetframework>netcoreapp1.0</targetframework> ... NuGet tags removed for brevity </propertygroup> <itemgroup> <packagereference include="Microsoft.AspNetCore" version="1.0.5"> <packagereference include="Microsoft.Extensions.Logging.Abstractions" version="1.1.2"> </itemgroup> </project>

As you can see the above project targets netcoreapp1.0. (look in the targetframework tag) I wanted to be able to also target netcoreapp2.0. To do this we need to change four things in the csproj file


  1. The existing itemgroup need a condition added to them to specify that they are for the currently targeted framework (netcoreapp1.0 in our example)
  2. The targetframework tag needed an S added to the end to make it targetframeworks
  3. Add the new target frame work to the list of frameworks in the targetframeworks tag
  4. New itemsgroup sections, with conditions, need to be added to support compilation of the additional target frameworks

Note: The above steps apply regardless of what additional framework(s) you are targeting. For a list of the framework monikers that should be used see Target frameworks

Now I will walk you thru the changes one at a time. First lets add the condition for netcoreapp1.0 to the itemgroup that contains the packagereferences.


<project sdk="Microsoft.NET.Sdk"> <propertygroup> <targetframework>netcoreapp1.0</targetframework> ... NuGet tags removed for brevity </propertygroup> <itemgroup Condition="'$(TargetFramework)'=='netcoreapp1.0'"> <packagereference include="Microsoft.AspNetCore" version="1.0.5"> <packagereference include="Microsoft.Extensions.Logging.Abstractions" version="1.1.2"> </itemgroup> </project>

Adding the highlighted piece will make that itemgroup apply only when compiling for netcoreapp1.0

Next lets update the target framework tag


<project sdk="Microsoft.NET.Sdk"> <propertygroup> <targetframeworks>netcoreapp1.0</targetframeworks> ... NuGet tags removed for brevity </propertygroup> <itemgroup Condition="'$(TargetFramework)'=='netcoreapp1.0'"> <packagereference include="Microsoft.AspNetCore" version="1.0.5"> <packagereference include="Microsoft.Extensions.Logging.Abstractions" version="1.1.2"> </itemgroup> </project>


Don't forget to update the start and end tags. Now lets add the additional framework we want to target, netcoreapp2.0 in this case.


<project sdk="Microsoft.NET.Sdk"> <propertygroup> <targetframeworks>netcoreapp1.0;netcoreapp2.0</targetframeworks> ... NuGet tags removed for brevity </propertygroup> <itemgroup Condition="'$(TargetFramework)'=='netcoreapp1.0'"> <packagereference include="Microsoft.AspNetCore" version="1.0.5"> <packagereference include="Microsoft.Extensions.Logging.Abstractions" version="1.1.2"> </itemgroup> </project>

Lastly lets add the package references needed for this project to compile on when targeting the netcoreapp2.0 platform.


<project sdk="Microsoft.NET.Sdk"> <propertygroup> <targetframeworks>netcoreapp1.0;netcoreapp2.0</targetframeworks> ... NuGet tags removed for brevity </propertygroup> <itemgroup Condition="'$(TargetFramework)'=='netcoreapp1.0'"> <packagereference include="Microsoft.AspNetCore" version="1.0.5"> <packagereference include="Microsoft.Extensions.Logging.Abstractions" version="1.1.2"> </itemgroup> <itemgroup Condition="'$(TargetFramework)'=='netcoreapp1.0'"> <packagereference include="Microsoft.AspNetCore" version="2.0.0"> <packagereference include="Microsoft.AspNetCore.Http.Features" version="2.0.0"> <packagereference include="Microsoft.Extensions.Logging.Abstractions" version="2.0.0"> </itemgroup> </project>

Now binaries for both platforms will be built when the project is compiled. If the project is configured to create a NuGet package via the GeneratePackageOnBuild tag the NuGet package will contain binaries for both platforms. If you do this to a project containing tests. you will then have tests for all supported platforms.

A few last details:
  • Once you edit the csproj by hand for multi-targetting, Visual Studio 2017 doesn't seem to be able to correctly make changes to the file. Each time I have tried to use the UI to make a change, I have then had to re-edit the file by hand to handle the multi0-targetting.
  • If you have an item group that needs multiple conditions. for example if you wanted to also target .net452, you need to add an Or between the conditions. It would look like this: <ItemGroup Condition="'$(TargetFramework)'=='netcoreapp1.0' Or '$(TargetFramework)'=='net452'>

Edit (4/30/2018): Note: In retrospect this post should have been titled how to multi-target a .Net Core application. For a class library it is best to target .Net Standard so that it is useable across multiple .Net platforms. For a more detailed explanation of .Net Standard see this blog post: Introducing .Net Standard.

Monday, November 20, 2017

Git commit-msg hook on windows

Git hooks

It took me a bit of tinkering to figure out how to properly write a git commit-msg hook that will run on windows. My goal was to ensure that all developers follow the commit message standard that was being developed in-house of prepending the commit message with the story number (we use Scrum so everything is a story).

Our story numbers all follow a basic format, the project acronym (between 3 and 7 uppercase letters) followed by dash followed by a number. An Examples of valid story numbers are PROJ1-123, SECRET-451, etc. 

Reading the documentation on customizing git hooks it quickly became obvious that we needed to write a commit-msg hook to validate the format of the commit message.

What I ended up with is the following, it was based on this blog post by Phillip Murphy.
       
#!/bin/sh
commit_regex='^[A-Z]\{3,6\}-[0-9]\{1,10\} .*'
echo $1
error_msg="Aborting commit. Your commit message MUST start with the story number followed by a space."

if [[ "$(grep "$commit_regex" $1)" == "" ]]; then
 echo "$error_msg" >&2
 exit 1
fi
 


Copy that script into a file in named commit-msg in .git\hooks. Note: the file has no extension.

The interesting pieces of developing this are that since it is shell script, the only way to test it was to install the hook and then try to do a commit to git. I am not completely certain what is executing the shell script as I have nothing on my Windows 10 box that I am aware of that will do it, it seem that git for windows may be executing the shell script???

As far as the syntax the regex expression was interesting I wanted to use [0-9]+ but that wouldn't work, it produced a syntax error so instead we are using {<min occurrence>,<max occurrence>} with the curly braces escaped with a backslash.

The other item that I found interesting was the use of grep to do a regex match. It works in this case because $1 is the file that contains the commit message. the commit message is written to .git/COMMIT_EDITMSG, you can verify this by adding echo $1 to the script

The next challenge is to get this installed on every developers computer in every copy of the repo. There are a number of answers, but the best for us seems to be the use of a template directory since this will be mandatory for all commits

A future enhancements is to validate that the story number that is found is actually a valid story number, this will help ensure no one fat fingered it on entry. 

As a last interesting side note, now that we know how to do this, we have decided for now that it is overkill for what we want to accomplish so all that is left of this mornings work is this post. 

The 2024 State of DevOps Report and the Importance of Internal Development Platforms

On the State of DevOps Report The State of DevOps Report, published annually by the DevOps Research and Assessment (DORA) team, has been a c...