.NET Core:

Porting a Production Web Service

Robert Hargreaves (@robertharg)

Agenda

  • What is .NET Core?
  • Why migrate?
  • Migration approach
  • Building and testing
  • Hosting

What is .NET Core?

What is .NET Core?

Cross-platform, open-source managed code framework with similarities to the .NET Framework

Concept .NET Framework .NET Core
OS Windows Windows, Linux, macOS
Runtime CLR CoreCLR
Base Library FCL, BCL CoreFX
Web ASP.NET & IIS ASP.NET Core
Entry point EXEs, DLLs
dotnet run myapp.dll

.NET Core does some things differently...

Reflection in .NET Framework:


					this.GetType().GetMembers()
				

Reflection in .NET Core:


					this.GetType().GetTypeInfo().GetMembers()
				

.NET Standard

A set of APIs that all .NET platforms have to implement - unifies the .NET platforms

.NET Standard 1.6

  • The current version - implemented by .NET Core 1.0
  • Not implemented by any .NET Framework version

.NET Standard 2.0

  • Doubles the number of APIs available
  • Implemented by .NET Framework 4.6.1 and
    .NET Core vNext
  • Can reference .NET Framework 4.6.1 libraries via compatibility shim

Targeting

  • Applications will target a .NET Core version
  • Class libraries target a .NET Standard version

ASP.NET Core

Re-architecture of ASP.NET, built on .NET Core

ASP.NET Core

  • Next generation of ASP.NET
  • Supports MVC and Web APIs
  • Faster performing than ASP.NET
  • Self-hosted web server - Kestrel
  • Supports OWIN middleware & servers
  • Can run on .NET Framework

ASP.NET Core Example


					using System.IO;
					using Microsoft.AspNetCore.Hosting;

					namespace Web.DNCore
					{
						public class Program
						{
							public static void Main(string[] args)
							{
								var host = new WebHostBuilder()
									.UseKestrel()
									.UseContentRoot(Directory.GetCurrentDirectory())
									.UseStartup<Startup>()
									.UseUrls("http://*:5000")
									.Build();
								host.Run();
							}
						}
					}
				

ASP.NET Core Gotchas

  • HttpContext.Current is null.
  • IIS modules unsupported. Use middleware instead.
  • Acceptance testing requires self-hosted app to be running (duh...)
  • Web.config no longer used.

ASP.NET Core Configuration

  • Strongly-typed configuration providers for JSON/XML/INI.
  • Environmental variables can override settings.
  • Concept of environments (Development, Production).

Why use .NET Core over
.NET Framework?

  • Cross platform
  • Open source, free
  • Separation of runtime and operating system
  • Cleaner, simpler deployment
  • CLI-style development

Why migrate to .NET Core?

  • Prefer open source over Microsoft licensing models
  • Substantial existing .NET investment in platform
  • Prefer incremental changes over re-writes
  • Developers & Ops already use Linux
  • Standardised Continuous Delivery pipeline

Why not to use .NET Core?

  • App uses COM, WCF, WPF, WinForms
  • Lack of library/dependency support
  • Lack of tooling support
  • App is a Monolith

Migration approach

Innovation Time

  • 2 days/month to work on tech innovation
  • Projects can be self-selected or suggested by another team member
  • Attempting to migrate an app to .NET Core was a suggested project

Migration Candidates

  • Console or Web API Microservice
  • Interactions are HTTP-based (XML, JSON)
  • Database engine support available
  • Comprehensive test suite (unit, integration, acceptance)

"Streaming Auth"

A microservice responsible for the authorising of media streaming requests.

Features

  • Nancy HTTP API
  • No database
  • Well tested (unit, integration, acceptance tests)
  • Deployed globally in AWS

Preparing projects for
.NET Core

  1. Identify the projects to migrate
  2. Check high-level dependency compatibility
  3. Target .NET Framework 4.6.1
  4. Upgrade dependencies
  5. Compile & Test
  6. Run .NET Portability Analyzer

.NET Portability Analyzer

.NET Portability Analyzer (VSIX and Console) can be used to identify code that will not compile in .NET Core

.NET Portability Analyzer

Maintain Compatibility

Code should support both .NET Framework
and .NET Core

Dual Platform Support

  • Increased feedback from compiler and tests for both targets
  • .NET Core support can reside in production branch/repository
  • Changes can benefit production code right away
  • Target code for specific platform using compiler pragmas

Project Structure

.NET Framework

.NET Core (Visual Studio 2015)

Shared Code; Seperate Projects

.NET Core (Visual Studio 2017)

  • project.json and *.xproj replaced by *.csproj
  • NuGet packages can either be referenced by packages.config or as PackageReference in *.csproj
  • Still recommend seperate projects!

DEMO

The Migration

Upgrade Application

  • Target .NET Framework 4.6.1 (was 4.0)
  • Upgrade dependencies:
    • Nancy 0.13 → pre-2.0
    • NUnit 2.6 → 3.5

NUnit

  • NUnit currently only supported by Resharper in Visual Studio 2015
  • Fairly unstable
  • As was Visual Studio Test Explorer...
  • dotnet test was more stable
  • Currently no support for NUnit in Visual Studio 2017 RC

RhinoMocks

  • No support for .NET Core
  • Could not run unit tests in .NET Core :(
  • Moq and NSubstitute supported

Unofficial Support

  • Forks exist to add .NET Core support:
    • log4net
    • DataDog Statsd Client
    • Nancy StructureMap Bootstrapper

  • Referenced libraries in project.json would not work outside Visual Studio (for some reason). Had to add package to NuGet...

Unsupported Libraries

  • Enyim MemcachedClient (work in progress)
  • No caching :(
  • Redis support in ServiceStack.Redis

log4net


								private static readonly ILog Logger =
									LogManager.GetLogger(
										MethodBase.GetCurrentMethod().DeclaringType);
							

Changed to:


								private static readonly ILog Logger =
									LogManager.GetLogger(typeof(Foo));
							

Supported in .NET Standard 2.0

Missing Synchronous Methods


								var request = (HttpWebRequest) WebRequest.Create(url);
								var requestStream = request.GetRequestStream();
							

Cowboy fix:


								var requestStream = request.GetRequestStreamAsync().Result;
							
  • Potential for deadlocks
  • Thrown WebException now wrapped in an AggregateException!
  • Better to use async/await through-out
  • Supported in .NET Standard 2.0

Missing Members


													var request = (HttpWebRequest) WebRequest.Create(url);
													request.Timeout = 5000;
													request.AllowAutoRedirect = false;
													request.UserAgent = "custom-user-agent";
													request.AddRange(0, 500);
												
  • Refactored to use HttpClient
  • Supported in .NET Standard 2.0

No AppSettings


								var foo = ConfigurationManager.AppSettings["Foo"];
							

Changed to:


								var foo = StreamingAuthConfiguration.Foo;
							

No HttpContext


								// set
								HttpContext.Current.Items["foo"] = "bar";
								// get
								var bar = HttpContext.Current.Items["foo"];
							

Changed to:


								AsyncLocal<string> foo = new AsyncLocal<string>();
								// set
								foo.Value = "bar";
								// get
								var bar = foo.Value;
							

Filesystem

  • Use Path.Combine(), not @"\"
  • Use Path.GetTempPath()
  • Use Environment.NewLine, not "\r\n"

Time Zone Fun!

Buggy code:


							new DateTime(1970, 1, 1).ToUniversalTime()
						

Windows:


							// 01/01/1970 00:00:00

Ubuntu 16.04:


							// 31/12/1969 23:00:00

Hosting

Hosting

  • Node.js apps currently deployed using AWS ECS (Elastic Container Service)
  • Aim is to deploy .NET Core apps as Docker containers

Docker

  • Microsoft provide .NET Core docker container base images:
  • 
    							FROM microsoft/dotnet
    							
    
    							FROM microsoft/dotnet:1.1-sdk-projectjson
    							

Example Dockerfile


FROM microsoft/dotnet:1.1-sdk-projectjson
WORKDIR /dotnetapp
COPY . .
RUN dotnet restore src/Domain.DNCore src/Web.DNCore
RUN dotnet build src/Domain.DNCore src/Web.DNCore

WORKDIR /dotnetapp/src/Web.DNCore
RUN dotnet publish -c Release -o out
EXPOSE 5000
WORKDIR /dotnetapp/src/Web.DNCore/out
ENTRYPOINT ["dotnet", "Web.DNCore.dll"]
							

							$ docker build -t streaming-auth .
							$ docker run -p 5000:5000 -d streaming-auth
					

Hosting

  • In production, Kestrel should be fronted by a Reverse Proxy.
  • On Windows, we'd use IIS. On Linux we'll use NGINX.
  • Docker Compose can be used to front our app with NGINX.

NGINX

  • Node.js apps currently deployed using AWS ECS (Elastic Container Service)
  • Aim is to deploy .NET Core apps as Docker containers

Docker Compose Example


version: '2'
services:
  streaming-auth:
    container_name: streaming-auth
    build: .
    ports:
      - "5000:5000"
  nginx:
    container_name: nginx
    image: nginx
    ports:
      - "8080:8080"
    links:
      - streaming-auth
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
					

NGINX Configuration Example


							server {
								listen 8080;

								location / {
									proxy_pass http://streaming-auth:5000;
									proxy_set_header Host $host:$server_port;
									proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
								}
							}
					

Demo

Conclusions

Red Flags

  • Incomplete support for NUnit in VS 2017 RC
  • Incomplete support for Resharper & .NET Core tests in VS 2017 RC
  • IDE inconsistency & instability
  • Final *.csproj format still TBD
  • No RhinoMocks = no unit tests in .NET Core :(
  • Memcached support not there yet (Redis?)

Red Flags Solved...

  • Migrate AppSettings to new ASP.NET Core Configuration
  • Log to STDOUT and forward Docker logs to Logstash
  • Integrate into CD pipline
  • A-B test code against Linux stack
  • Gradually ramp-up traffic to Linux stack

Cause for Optimism

  • Library support is growing all the time
  • With .NET Standard 2.0, porting is a use case
  • IDE and tooling issues will be fixed
  • It works! (more or less)
  • Have a go!

Thank You!

Any questions?

References

References