Moving To .NET Core

Today I relaunched my blog on .NET Core. I was asked to blog my experience on this so I will.

Why Do It?

I suppose there are a few decent reasons to upgrade. First, I like to keep up with the Joneses and this was a good way of doing that. Second, before this I only had two reasons to open Windows: play Civilization III or make updates to the blog. Now I only have one. You might say “But Eric, you haven’t blogged in months!” True enough. Being able to easily update things on my Mac will be nice and I will blame the lack of blogging on having to use Windows, even if we all know that’s just an excuse.

What Was It Like?

It was a pretty good experience. But then again, my blog is pretty simple. Everything is file-based and cached in memory, so I did not have to deal with database libraries. Essentially, I only had to deal with ASP.NET and System.IO. The only thing that I had to rewrite was my RSS feed because the old SyndicationFeed apparently hasn’t been ported yet.

I did 99% of this on my Mac using VS Code. The other 1% involved creating a blank project using the Visual Studio tools just to compare what you get with that versus what you get when using the Yeoman generator.

The dotnet CLI worked just fine most of the time. I only twice had to nuke my obj directory because of a corrupt build. If you ever get the CLI error “Could not read the incremental cache file,” delete your obj folder and rebuild. The only other compilation problem that I had was in regard to the proper reporting of dependency compilation errors. There were a number of times that a class would fail to compile because a dependency would fail to compile but it reported only the error in the consuming class, not the dependency. This was annoying but I worked through it.

In terms of what I had to rewrite for the blog, it breaks down pretty cleanly. Everything that involved the web had to be either slightly changed or completely rewritten, depending on the situation. Everything that didn’t touch ASP.NET MVC or System.Web was almost completely unchanged. If I recall correctly, I only had to replace my usage of Date.ToShortDateTime(). This is a simple app so any large app will certainly require much much but for me, that part of the code survived almost entirely unscathed. Since I generally do a decent job of separating web plumbing code from not web plumbing code, that made the conversion much easier.

The roughest spot was the documentation, especially around configuration and error debugging. There is a lot of good information out there but I found little bits to be slightly out-of-date. But I suppose that is to be expected with something this young and this in flux.

I found myself mostly annoyed by VS Code. That’s partially because it does not have a tabbed interface yet, and partly because it isn’t Atom, which I have grown really fond of. I am going to have to install the C#/.NET support for that and switch back.

Snippets

Here are some snippets from the blog for your benefit or amusement if you want to poke fun at my code.

This is my Program.cs file, the start of my web app. Some of that came through the Yeoman generation process, some of it I figured out by poking around. If you were wondering how to pass in arguments to go down a different fork of code (I do it with my RSS), here is an example. I just enter dotnet run rss on the terminal and there it goes.


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

namespace Exemplar
{
    public class Program
    {
        public static void Main(string[] args)
        {
            if (args.Length != 0)
            {
                if (args[0] == "rss")
                {
                    RssGen.Generate();
                    return;
                }
            }

            var config = new ConfigurationBuilder()
                .AddCommandLine(args)
                .AddEnvironmentVariables(prefix: "ASPNETCORE_")
                .Build();

            var host = new WebHostBuilder()
                .UseConfiguration(config)
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseStartup()
                .Build();

            host.Run();
        }
    }
}

This is my Startup.cs file. This is roughly equivalent to Global.asax’s app start method. I actually haven’t seen an error page yet (not for lack of trying), so the app.UseDeveloperExceptionPage() stuff needs to be revisited so I can get that to work.


using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Exemplar.Web;

namespace Exemplar
{
    public class Startup
    {

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app,
            IHostingEnvironment env,
            ILoggerFactory loggerFactory)
        {
            Redirects.Load(); //specific to my blog

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMiddleware(); //specific to my blog
            app.UseStatusCodePagesWithRedirects("/error/doesnotexist"); //handle 404's with redirect

            app.UseMvc(routes =>
            {
                Routes.ConfigureRoutes(routes); //specific to my blog
            });

            PostConfig.LoadData(env.ContentRootPath); //specific to my blog

            Console.WriteLine("Startup!");
            app.UseStaticFiles();
        }
    }
}

HttpRequest does not seem to have a Uri property on it, so I stole this from somewhere on the web. Wish I recalled the link.


using System;
using Microsoft.AspNetCore.Http;

namespace Exemplar.Web
{
    public static class HttpRequestExtensions
    {
        public static Uri ToUri(this HttpRequest request)
        {
            var hostComponents = request.Host.ToUriComponent().Split(':');

            var builder = new UriBuilder
            {
                Scheme = request.Scheme,
                Host = hostComponents[0],
                Path = request.Path,
                Query = request.QueryString.ToUriComponent()
            };

            if (hostComponents.Length == 2)
            {
                builder.Port = Convert.ToInt32(hostComponents[1]);
            }

            return builder.Uri;
        }
    }
}

Finally, if you were curious what Middleware looks like, this is my redirect middleware. I have a few hundred incoming paths that I map to other things. That code is not shown but here is the middleware piece that calls it. If you want to see where this is hooked up, look above in Startup.cs.


using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace Exemplar.Web
{
    public class RedirectionMiddleware
    {
        private readonly RequestDelegate _next;

        public RedirectionMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            var redirectUrl = Redirects.RedirectUrl(context.Request.ToUri());

            if (!String.IsNullOrEmpty(redirectUrl))
            {
                context.Response.Redirect(redirectUrl);
                return;
            }

            await _next.Invoke(context);
        }

    }
}

So It Is Done

There you go. Perhaps someone will find this useful. Now that it’s done, I can say that I am happy with the result and with the way .NET Core is going. Bravo Microsoft.

Since I like to learn things and then write about and explain them, this whole experience really made me want to write a new ASP.NET book. I am looking for other streams of revenue and that would be fun but there’s other fish I need to fry at the moment. Maybe one day.

comments powered by Disqus