Until NetCore 2 came along, migrating an existing Net Framework project to dotnet core
was likely a painful exercise in futility, as you time-consumingly discovered just how many bits of the .Net framework don't exist on netcore 1. Small things, like key parts of AdoNet. It was a bleeding edge experience.
But then there was dotnet core 2
with not-very-far-off 100% Api compatibility. And now all is sweetness and light.
Seriously. It is. Huge chunks of your .Net framework project will now 'just work' on .netcore
, with little or no editing. In fact it's so easy, you might consider multi-targeting net framework and netcore just to show off.
Console apps and class libraries are straightforward. Considerations for UI and platform technologies:
- For AspNet, there is a learning curve from Mvc versions 3/4/5 to AspNetCore for which I will refer you to the tutorials. There is then work to do which I do cover below.
- For Windows Forms and WPF projects, I recommend you to the considerations in MS guide for porting Winforms.
- WWF and WCF-serverside are not (currently) migratible, although WCF-clientside is. Moving Web-facing WCF-serverside to dotnetcore with NancyFx or to AspNetCore would be a smallish-rewrite, about the same as moving to WebApi2; but it seems that MS are working on WCF serverside. Remoting, however is gone. So if your preference for WCF is that it's a better style that all this pseudo-resty AspNet nonsense, then consider Nancy.
Overview
- Start with a new, empty dotnet core 2 project
- Drag-n-drop all your existing code into it, excluding
AssemblyInfo.cs
- Deal with .settings and .config files
- Re-add your NuGet dependencies
- Deal with other code differences
- Build and Go!
Well okay, that last step, Build-and-Go, is more likely to be Build-and-Fix-The-Next-Compiler-Error-And-Build-Again. But it is mostly straightforward.
To migrate AspNet
to AspNetCore
there are more steps, and you do have to start with the learning curve for a whole new framework. That said, it's like someone thought, “let's redo Mvc as WebApi2 + Razor + Views but with a cleaner startup style and with mandatory dependency injection”. Your controllers will hardly change. I do find AspNetCore simpler, cleaner, easier to work with. Roughly, your steps are:
- Work through the getting started tutorials & learning curve. (Estimate 5-10 hours per person?)
- Migrate your startup config to the new approach (2-10 hours depending on how much novel startup code you have)
- Migrate any custom authentication to the new approach (An hour or so if you read the gotcha below)
- Consider whether your Attribute-based filters will remain as attributes or be re-worked into something else.
- Re-tool unit tests which mocked the old Asp.Net Mvc dependencies
Larger sets of projects
If you are dealing with not just a single project but a whole load of them, you should first look through Microsoft's guide to porting. The main reason to not work through those steps for a single project is that since netcore2, the fastest way to analyse “what problems will I have in porting” for a smallish project is to just do it! You can most likely finish the job already, faster than you can use the analysis tools to predict what problems you will have. That wasn't always the case before netcore 2. A couple of thoughts from that guide that I do recommend though:
- Check your third-party dependencies first. If they aren't available on dotnetcore, that's presumably a showstopper?
- Check you're aren't relying on technology that isn't available in dotnetcore.
Start with a new empty dotnet core 2 project.
To migrate an executable you'd create a console app. For a class library, you can make it a netstandard2 project, which makes the project available for use in .net 4.6.1+ / 4.7
as well as in dotnet core
.
The command line is very trendy in dotnet core, so you can do it all with dotnet new
instead of using a GUI. dotnet new
will show what templates are installed on your machine.
Drag-n-drop all your existing code in, excluding AssemblyInfo.cs
dotnet core projects assume, by default, that if there's a code file in the directory or a subdirectory then it's part of the project, so just dragging all your code into the new project directory will just work.
Don't include the AssemblyInfo.cs because that gets auto-generated from the .csproj file. If you have anything of interest in your AssemblyInfo.cs, edit the .csproj file and put it in there. The AssemblyInfo properties section of Additions to the csproj properties for dotnet core show you the Element Names to use if you want to re-add information. Something like:
<PropertyGroup> <TargetFrameworks>netstandard1.6;net40</TargetFrameworks> <AssemblyVersion>4.1.4.3</AssemblyVersion> <AssemblyFileVersion>4.1.4.3</AssemblyFileVersion> <PackageVersion>4.1.4.3</PackageVersion> <GenerateDocumentationFile>true</GenerateDocumentationFile> <Title>TestBase – Rich, fluent assertions and tools for testing with heavyweight dependencies: AspNetCore, AdoNet, HttpClient, AspNet.Mvc, Streams, Logging</Title> <PackageDescription><![CDATA[*TestBase* gives you a flying start with ....etc...</PackageDescription>
Note the new properties with names beginning with <Package...>
which will be picked up by dotnet pack
when creating NuGet Packages. Nuget is much easier with dotnet core
, it's kind of built-in instead of being an extra thing to learn and do.
Deal with .settings and .config files
There is a whole new approach to settings and configuration. You will have to learn it. It's good though. It lets you do things like this:
{ "AComponentDefaults": { "SomeSetting": "Me", "ANumericSetting" : 1.0, "Subsetting": { "Something" : "Sub" }, "JustOneLine" : "This" }
and then read a whole section as strongly-typed settings with a one-liner:
Configuration.GetSection("AComponentDefaults") .Bind(myComponent = new AComponent());
You can still use single-line settings of course:
var justOneLine=Configuration["JustOneLine"]
The new system deals easily with per-environment overrides, and has a whole new “get your config from all kinds of other sources than the settings file” capability.
Re-add your NuGet dependencies
This is straightforward. In Visual Studio (or in JetBrains Rider) use right-click -> Manage NuGet Packages
. On the command line it's dotnet add package
.
The big news here is that most of your NuGet dependencies already work on dotnetcore. All of the most downloaded NuGet packages are either multi-targeted or have packages for each platform. (The dependency trees for most packages for dotnet core is quite different to the dependency tree for net framework, but it makes no difference at all, on the whole).
Deal with other code differences
I don't think there are too many. Under netcore2, your major external dependencies – AdoNet
, HttpClient
and FileSystem
– are all either the same or quite similar. SqlClient
, Npgsql
, Dapper
are pretty much unchanged and the rest of the Framework is very much the same.
Main code changes:
- Scan the list of breaking changes, which are largely in low-level or platform specific areas.
- If you use reflection you must often use
type.GetTypeInfo().GetXXX()
instead oftype.GetXXX()
. If you're good with regex, this just needs a search-&-replace to fix. - EntityFramework Core is different, but not extremely different.
Build and Go!
And … deploy to Macos and Linux. Hurray.
Migrating AspNet to AspNetCore
Work through the getting started tutorials
Really. Don't try to skip the aspnetcore getting started learning curve. Be aware that the tutorials push the new Razor Pages approach, which you will want to ignore. Instead be sure you're clear on how the new approach handles startup, dependency injection, attributes, filters, and authentication. Your controllers and routing will largely work with minimal change.
Migrate your startup config to the new approach
So having done your learning curve, you understand that all your Global.asax.cs
and App_Startup
code will move into, or be called from, your Startup
class. And you will cleanly separate config setup—having learned about the new configuration approach–and you will use a dependency injection container to provide any global config to your controllers.
Fix-up ControllerContext changes
There are some fiddling tidyup changes on ControllerContext
and Request
properties–for instance userHostAddress is no more, you must look for HttpContext.Connection.RemoteIpAddress
instead. Global HttpContext
is gone, but of course you were always careful to use controller.HttpContext
weren't you?
Add the new interfaces to Attribute-based filters, or else rework them as middleware
You do need to learn about the new kinds of filters, and consider whether what you are doing with your filters should stay as-is in attribute filters or might it be simpler to move logic into the new middleware approach.
Migrate any custom authentication/authorization to the new approach
The mistake to avoid here, is trying to make your custom AuthorizationAttribute
work as an AspNetCore attribute. Don't. Instead,
- Move the logic of your custom
AuthorizationAttribute
into aPolicy
, which could be just a single method call. - Delete your custom attribute and let the built-in AuthorizeAttribute reference your new policy:
[Authorize(Policy="MyCustomPolicy")]
It would have saved me half a day if I'd realised this up-front. But on this plan, you can migrate custom authenticate in an hour or even minutes.
Re-tool your unit test controller dependencies for the new framework
There is some popular code on the web for mocking the dependencies of an Mvc 3 or 4 or 5 Controller.ControllerContext
. This must all be replaced.
Myself, for Mvc 4 & 5 I always used TestBase-Mvc
which gave me two simple extension methods:
var unitUnderTest= new MyMvcController(...) .WithHttpContextAndRoutes(); var webApiControllerUnderTest= new MyWebApiController(...) .WithWebApiHttpContext<T>(HttpMethod httpMethod, [Optional] string requestUri, [Optional] string routeTemplate); //Or, optional parameters to process the actual route urls from your RegisterRoutes config: controllerUnderTest .WithHttpContextAndRoutes( [Optional] Action<RouteCollection> mvcApplicationRoutesRegistration, [optional] string requestUrl, [Optional] string query = "", [Optional] string appVirtualPath = "/", [Optional] HttpApplication applicationInstance)
This makes sure a controller
can reference cookies, session, TempData, the Url.Action()
calls and even the global HttpContext.Current
in a unit test context.
For AspNetCore, I wrote TestBase.Mvc.AspNetCore
(soon to be renamed to to TestBase.AspNetCore
) which offers a similar thing:
var uut = new ControllerUnderTest().WithControllerContext(); uut.Url.Action("a", "b").ShouldEqual("/b/a"); uut.ControllerContext.ShouldNotBeNull(); uut.HttpContext.ShouldBe(uut.ControllerContext.HttpContext); uut.Request.ShouldNotBeNull(); uut.ViewData.ShouldNotBeNull(); uut.TempData.ShouldNotBeNull(); uut.MyAction(param) .ShouldBeViewResult() .ShouldHaveModel<YouSaidViewModel>() .YouSaid.ShouldBe(param);
It also has a large set of fluent assertions for ViewResults, FileResults, etc, etc. Once I'd written the new infrastructure, migrating my controller unit tests was mostly painless. (Nb it still needs a few changes for CompatibityVersion_2_2, it's currently written for 2.0.)
New in AspNetCore is the ease of testing not just individual controllers but the whole hosted application. The AspNetCore team coded a TestServer for their unit tests, and this server can be used, bootstrapped with your actual application's Startup code, and then tested with an HttpClient:
[TestFixture] public class WhenTestingControllersUsingAspNetCoreTestTestServer : HostedMvcTestFixtureBase { [TestCase(""/dummy/action?id={id}"")] public async Task Get_Should_ReturnActionResult(string url) { var id=Guid.NewGuid(); var httpClient=GivenClientForRunningServer<Startup>(); GivenRequestHeaders(httpClient, ""CustomHeader"", ""HeaderValue1""); var result= await httpClient.GetAsync(url.Formatz(new {id})); result .ShouldBe_200Ok() .Content.ReadAsStringAsync().Result .ShouldBe(""Content""); }
But I have come round to seeing this as automated integration testing, not unit testing. I would use it for testing e.g. content negotiation is working as expected, not for testing the domain logic of a controller action.
Conclusion
Since the arrival of netcore2
, the cost of migrating to DotNetCore is dramatically lower. DotNetCore tooling and extensibility is very good. Even migrating AspNet is not an excessive task. Even for just NetFramework 4 development, the new tooling is simpler and better. I reckon that dotnetcore is cheaper and easier to write and maintain. Both C# and the framework are evolving in ways that reduce your cost of development: And you get cross-platform deployment pretty much for free. At last.