“There are no shortcuts in evolution.“- Louis Brandeis
According to Microsoft, this is how the .net ecosystem looks today:
The landscape is still pretty fragmented. We basically see three completely separate stacks used for the development of different kinds of apps. We have the Windows-based full.net framework with .NET CLR and .net BCL that is used to develop desktop (WPF, Win Forms) or server-side (WCF, ASP.NET, ASP.NET WebAPI , ASP.NET MVC 1-5) applications. We have the modular, open sourced and cross-platform .net core stack with corefx and coreclr that is used to develop desktop (Console), UWP or server-side applications (ASP.NET Core). Finally, we have the XAMARIN stack used for mobile applications development based on Mono Class Library.
And this is the future .net ecosystem:
.NET Standard is a core cross-platform framework that will be reused by all .net stacks. The motivation is to unify the existing stacks and prevent future fragmentation and that’s very cool! You can think of .net standard like the finer grained and cross-platform equivalent of mscorlib, the huge assembly that’s part of the current full .net framework and that’s considered the core of .net BCL. For example, if the System.IO, System.Threading, System.Collection namespaces are defined in mscorlib.dll in the full .net framework, there are separate assemblies for these in .net standard.
Technically speaking, .net standard is a NuGet package that doesn’t contain an actual assembly but has a lot of dependencies on multiple fine grained NuGet packages that contain those fine grained assemblies. (System.Runtime, System.IO, System.Threading etc.)
Now comes the confusing part:
Some articles from Microsoft suggest that .net standard has been introduced together with .net core 1.0 with June 2016 RTM and earlier previews. Given this and the strategy diagram above that they suggested as the future, it’s easy to think that currently .net standard is the core of the .net core and that .net framework will also implement it in the future. You can imagine a future major version of .net framework (5 or 6) that will implement .net standard by refactoring the current mscorlib into the finer grained assemblies.
But…surprise! The .net standard compatibility matrix suggests that .net standard is already compatible with full .net framework versions even since .net 4.5! In other words, my app assemblies targeting .net standard 1.6.1 for example would be able to run on Windows instance having just .net framework 4.6.1 installed without .net core. But .net 4.6.1 has been released way back in November 2015. How is this possible?
Let’s dive into the details and see what’s happening on development and runtime environments when you mix .net standard, .net core and .net framework. I’ve created a sample solution for this mix using Visual Studio 2017 RC.
VS 2017 RC is already using the new csproj format for .net core and .net standard projects and the new .net core SDK that knows how to build these projects. As you might know, this is a quite recent change that has been introduced after the .net core RTM. Before that (VS 2015), projects targeting .net core or .net standard used an xproj file format and the dependencies were specified in JSON format in a project.json file. Microsoft switched back to MSBuild and csproj for .net core and .net standard projects for two main reasons:
- MSBuild is a key component of the .NET tools ecosystem. Tools, scripts and VS extensions that target MSBuild should now extend to working with .NET Core.
- MSBuild enables project to project references between .NET projects.
Pretty fair if you ask me. Besides, the second point is of key importance for implementing the Cone Architecture pattern as we’ll see shortly.
My ClassLibraryNetStandard project is a class library targeting .net standard 1.6.1. This makes it cross-platform! It includes a single trivial class Test:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace ClassLibraryNetStandard | |
{ | |
public class Test | |
{ | |
public string SomeProperty { get; set; } | |
public string SomeOtherProperty { get; set; } | |
} | |
} |
The ConsoleAppNetCore project is a console app targeting .net core 1.1.0. This is realized through a dependency on Microsoft.NetCore.App v1.1.0 NuGet package which further depends on NETStandard.Library 1.6.1 package. This project also includes a project reference to ClassLibraryNetStandard project and a NuGet package reference to Newtonsoft.Json v9.0.1.
In VS 2017, the .net core and .net standard projects dependencies are nicely grouped visually under a Dependencies node with separate sub-categories for different type of references: NuGet, Projects and Assemblies. The NuGet dependencies are also nicely displayed as trees including the actual assembly and their own NuGet packages dependencies:
The project file looks like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0"> | |
<PropertyGroup> | |
<OutputType>Exe</OutputType> | |
<TargetFramework>netcoreapp1.0</TargetFramework> | |
</PropertyGroup> | |
<ItemGroup> | |
<Compile Include="**\*.cs" /> | |
<EmbeddedResource Include="**\*.resx" /> | |
</ItemGroup> | |
<ItemGroup> | |
<PackageReference Include="Microsoft.NETCore.App" Version="1.1.0" /> | |
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" /> | |
</ItemGroup> | |
<ItemGroup> | |
<ProjectReference Include="..\ClassLibraryNetStandard\ClassLibraryNetStandard.csproj" /> | |
</ItemGroup> | |
</Project> |
As you expect, this setup is cross-platform. If I do either a framework-dependent deployment or self-contained deployment of ConsoleAppNetCore like explained here, I could run it on Windows, Linux and OS X.
Now comes the interesting part:
ConsoleAppNetFramework is a console app project that’s targeting the full .net framework 4.6.1. It also includes a project reference to ClassLibraryNetStandard and a NuGet reference to Newtonsoft.Json. I’m reusing the NetStandardClassLibrary like a core in two separate contexts: a cross-platform console app targeting .net core (ConsoleAppNetCore) and a Windows-based console app targeting the full .net framework (ConsoleAppNetFramework)!
The main method instantiates a Test object, serializes it and outputs the JSON to console:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using ClassLibraryNetStandard; | |
using Newtonsoft.Json; | |
using System; | |
namespace ConsoleAppNetFramework | |
{ | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
var test = new Test { SomeProperty = "One", SomeOtherProperty = "Two" }; | |
Console.WriteLine($"Hello from .net framework! Here's one .net standard Test instance:{JsonConvert.SerializeObject(test)}"); | |
Console.ReadLine(); | |
} | |
} | |
} |
Like mentioned earlier, I can run my ConsoleAppNetFramework on a Windows instance having just the full .net framework 4.6.1 installed without the .net core runtime by copying just 3 assemblies: ConsoleAppNetFramework.dll, ClassLibraryNetStandard.dll and Newtonsoft.Json.dll.
Looking at the references section in the assembly manifest of ClassLibraryNetStandard assembly:
It depends on System.Runtime, System.Resources.ResourceManager and System.Diagnostics.Debug assemblies. But those assemblies are part of .net standard and are installed by a .net core runtime installation (C:\Program Files\dotnet\shared\Microsoft.NETCore.App\1.1.0 on Windows).
How can then my app run on Windows with just .net framework 4.6.1 installed without .net core runtime and without me deploying those assemblies?
Taking a look in GAC (C:\Windows\Microsoft.NET\assembly\GAC_MSIL) I can find the System.Runtime, System.Resources.ResourceManager and System.Diagnostics.Debug assemblies! But those are just adapter assemblies that don’t contain any IL but just an assembly manifest including a reference to the full .net framework mscorlib and forwarding the CLR lookups for some types to this assembly.
Here is how the manifest of System.Runtime in GAC looks like:
So, in other words, running an application targeting the full .net framework and referencing a .net standard assembly on a Windows instance would cause the CLR to be redirected to the full .net framework mscorlib when trying to use some types from .net standard/.net core assemblies.
Another interesting thing is that the real cross-platform .net standard assemblies also have a reference to mscorlib 4.0.0.0 but if you look closely that’s not the same as the mscorlib in the full .net framework but a different, much lighter (and cross-platform too!), mscorlib assembly in .net standard. Here is the references section in the assembly manifest of System.Runtime that’s part of .net core runtime installation (C:\Program Files\dotnet\shared\Microsoft.NETCore.App\1.1.0 on Windows):
There are two different mscorlib assemblies, with the same name and version but different unique identities!
- mscorlib v4.0.0.0 public key token B7 7A 5C 56 19 34 E0 89 in full .net framework – Windows
- mscorlib v4.0.0.0 public key token 7C EC 85 D7 BE A7 79 8E in .net standard – cross-platform
Back to Cone Architecture
If you’re not familiar with my DIP-based metaphor, please check this out before reading further.
RoboBank is a learning project. It is a collection of samples of various technical patterns, principles, practices and technologies and a context to explore new things as the domain and the use cases become familiar. From a functional perspective it is a simplified banking system. It is meant to handle the needs of a multinational bank in a simplistic way.
The microservices and the Banking Portal app that are part of the RoboBank system are source controlled in separate git repositories in order to enforce the autonomy and heterogeneity that are specific to Microservices architectural style (independently develop, deploy and scale). The individual git repositories are linked to the main repository as git submodules.
The RoboBank Customer Service implementation is based on Cone Architecture pattern. The core of the application (Domain and Application logic) is being cleanly reused in multiple contexts:
- Azure Cloud Service (Web Roles), IIS, ASP.NET Web API 2, Mongo
- Azure Service Fabric, ASP.NET Web API 2 (self-hosted using Microsoft.Owin.Hosting.WebApp as web server), Mongo
- Azure Functions, Mongo
- Isolated (Service Integration Testing): IIS, ASP.NET WebAPI 2, Stub (in memory) repository implementations
But even if the core is cleanly reusable in multiple contexts it is still constrained to Windows since all the core assemblies are targeting the full .net framework.
RoboBank Account Service is a better implementation of Cone Architecture as it increases the reusability of the core. The core of the application is targeting .net standard 1.6.1 and this makes it cross-platform! The core is being reused in two separate contexts:
- Windows: Azure Cloud Service (Web Roles), IIS, ASP.NET Web API 2, EF6, .net framework 4.6.1
- Windows/Linux/OS X: asp.net core, EF Core 1.0, .net core 1.1.0
I’ve also built two Docker images including the cross-platform version of Account Service for Linux and Windows. You can find these on DockerHub here. You can easily pull them and run containers from them on your favorite platform since the build inside is using a stub (in memory) implementation of the repositories so there’s no DB dependency:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Threading.Tasks; | |
using RoboBank.Account.Application.Ports; | |
using System.Text; | |
namespace RoboBank.Account.Application.Adapters.NetStandard | |
{ | |
public class StubAccountRepository : IAccountRepository | |
{ | |
private const string OwnCustomerId = "RoboBank"; | |
private readonly IEnumerable<Domain.Account> _cache = new List<Domain.Account> | |
{ | |
new Domain.Account | |
{ | |
Id = "account1", | |
CustomerId ="customer1", | |
Currency = "USD", | |
Balance = 1000 | |
} , | |
new Domain.Account | |
{ | |
Id = "account2", | |
CustomerId = "customer1", | |
Currency = "EUR", | |
Balance = 1000 | |
} | |
}; | |
public async Task<IEnumerable<Domain.Account>> GetByCustomerIdAsync(string customerId) | |
{ | |
return _cache.Where(acc => acc.CustomerId == customerId).ToArray(); | |
} | |
public async Task<Domain.Account> GetByIdAsync(string id) | |
{ | |
return _cache.FirstOrDefault(acc => acc.Id == id); | |
} | |
public async Task<Domain.Account> GetOwnAccountByCurrencyAsync(string currency) | |
{ | |
return _cache.FirstOrDefault(acc => acc.CustomerId == OwnCustomerId && acc.Currency == currency); | |
} | |
} | |
} |
You can also find the Dockerfiles for the images here.
Besides reusing the core in various production scenarios I’m leveraging the new .net ecosystem for testing too:
I wanted to use xUnit .net, AutoFixture and Moq as it’s a really great tool chain but I had a problem: AutoFixture does not support .net standard. This basically means that the AutoFixture assemblies have a dependency on the full .net mscorlib (B7 7A 5C 56 19 34 E0 89) No worries! I can have my production code projects being cross-platform by targeting .net standard but have my test projects target full .net framework, use AutoFixture and reference the .net standard production code projects. That also means that I can only run my tests on Windows but that shouldn’t be a problem as long as the production code assemblies are cross-platform.
You can see this example in the RoboBank.AcocuntDomain (.net standard) and RoboBank.Account.Domain.Tests (.net framework) projects: