When a few of us at Rally started to evaluate .NET Core, the main thing that made us nervous about making the jump was a lack of successful migration stories for enterprise-scale applications.

Well… we went for it anyway.

We hope this detailed travelogue of our migration will empower other teams by providing a concrete understanding of what it took to turn one of our Windows-only, .NET Full Framework applications into a cross-platform .NET application running on Linux.

Overview

To start off, I’ll just warn that this was not a weekend project. Nor was it a painless process. However, the good news is that all the migration steps are relatively independent which allowed us to tackle them one by one. Over a series of posts, I’ll be going into detail on each of the following efforts that got us to .NET Core.

  • Approach
  • External Dependencies
  • Internal Dependencies
  • Continuous Integration
  • Entity Framework Core
  • ASP.NET Core
  • Logging
  • Dependency Injection
  • Making the Switch to the .NET Core Platform
  • Deployment
  • Watching the movie “Core” starring Aaron Eckhart and Hilary Swank

Vocabulary

Before we dive in, I want to be very explicit about my vocabulary. Naming in the .NET Core world is a bit… tricky. Learning what in the world we were talking about or doing was a nontrivial effort in the overall project. What follows is each of the .NET buzzwords and how I’ll be using them.

  • .NET Core Platform
    • What you usually hear referred to as just “.NET Core”. For the rest of the article, I’ll use the typically implied qualifier “Platform” to distinguish from other .NET Core… stuff.
    • Cross-platform
    • Example: “Rally ported an application to run on the .NET Core Platform.”
  • .NET Full Framework
    • Your typical .NET, such as .NET Framework 4.6
    • Windows-only
    • Example: “Before porting, all of our apps ran on the .NET Full Framework.”
    • Technically calling this “.NET Framework” should suffice, but “Full Framework” is more explicit and is commonly used to distinguish between this and the Core Platform.
  • .NET Standard
    • A framework target for libraries
    • A library built to target .NET Standard can be referenced by either an app targeting the .NET Core Platform or the .NET Full Framework.’
    • If you target .NET Standard, you don’t need separate packages for Full Framework apps and Core Platform apps
    • Example: “Because NodaTime needs to be referenced by our Full Framework app and our Core Platform app, it’s good that it targets .NET Standard.”
  • NETStandard.Library
    • Not to be confused with .NET Standard, NETStandard.Library is a package that contains many of the APIs that historically were included in the .NET Framework install, such as System.Linq and System.IO.
    • Example: “Neat! It looks like the NETStandard.Library package pulled a lot of the System namespaces I’m used to.”
  • .NET Core SDK
    • Tools for developing applications on the .NET Core Platform as well as on the .NET Full Framework. Includes the dotnet CLI tool.
  • dotnet
    • Command line tool used for creating, building, packaging, and testing .NET applications (Core Platform or Full Framework).
    • Example: “Use dotnet restore to restore your packages.”

With that settled, let’s get started.

Why We Did It

Why would a software team volunteer for a difficult technical project with little public evidence of success? For us, it came down to cost, elasticity, and mind share.

Cost

Being on the Full Framework forces us to use Windows machines to run our software. On AWS, Windows instances are significantly more expensive than similarly-sized Linux instances. At the time of this writing, the C4 Large On-Demand price for Linux instances is $0.100/hour vs $0.192/hour on Windows. This difference is roughly the same across different instance types. With simply running our software on Linux, our EC2 costs are nearly split in half.

Elasticity

Because our infrastructure is immutable, nearly every change we make results in reprovisioning new servers from scratch. Because of this, the time it takes for an instance to start up makes a material impact.

Additionally, the time it takes for a Windows instance to come online is much longer than the turnaround needed to appropriately respond to increased traffic. If it takes 15 minutes for a new Windows-based instance of our application to start up, we might have either lost the need for the throughput or it’s too late and we’re drowning in the unhandled load.

With Linux servers (and especially the end goal, Docker on Mesos), the startup time is a small portion of what it was on Windows.

Mind Share

Rally Choice is only a part of the Rally product lineup. What’s more, all of our other products already run on Scala on Linux. It becomes easier to leverage existing tooling, code, and skills when we’re not riding two rails on Linux and Windows.

Approach

It would be a disservice to the reader to not set up at least a small amount of context for how this project originated as well as our overall strategy for tackling the port.

As described above, the Choice engineering team considered it a worthwhile effort to port at least a subset of our .NET services to the .NET Core Platform. This was cut and dried. Less well-defined was the strategy for actually doing so. This reasoning will take us back to Fall 2016.

The Spike

Choice teams operate using traditional Scrum, where each team has one or two week sprints which are filled with roughly the amount of work that the team estimates that they can accomplish given their delivery in previous sprints.

Generally, one sick day on a five-person team over a span of two weeks doesn’t significantly impact estimation and delivery. However, the week of Thanksgiving in the United States poses a unique project management challenge. Given that Thursday and Friday are official holidays and people often also take Wednesday, and maybe Tuesday, and maybe even Monday as well, how do you handle that team?

Instead of continuing sprint work for that week (which we thought would lead to poor morale for the few people left on each team), we instead opted to host a three-day hackathon. Defragmenting the people across the individual teams gave everyone present a focused initiative as well as gave many of us a chance to work with people we didn’t normally interact with on a day-to-day basis.

The hackathon itself was more effective than we could have imagined. It produced a tool for automating test cases, a chat bot for looking up the lunch schedule, and most impressively, delivered the application we use every day for finding what versions of what apps are in what environment.

Emboldened with this success, when we determined that the week between Christmas and New Year’s holidays would face the same project management challenge, we opted for another skunkworks effort: porting a project to the .NET Core Platform.

We did a small amount of preparation beforehand: evaluating our external dependencies at a high level, making sure replacement dependencies would fit our needs, and if things like Entity Framework Core could handle how we were currently using Entity Framework. However, the real work didn’t start until Tuesday, December 26.

For four days, we split up different parts of the project: some of us were tasked with updating libraries and dealing with breaking changes, others tackled the switch to ASP.NET Core, and a few poor souls focused on porting a significant portion of our tests.

We realized pretty early in the week that we weren’t going to end up with a production-ready port (curse you, Entity Framework lazy loading!), so we instead focused on getting to the smallest modicum of functionality to prove that given more time, the port would be feasible. This led to many noncritical chunks of code and tests being commented out while we focused on sprinting to the finish line: at least one working endpoint that touched the database with the app running on the .NET Core Platform.

On Friday afternoon, those of us who remained settled in a war room while tweaking what we hoped was the last Dependency Injection issue. At long last and with much celebration, we witnessed the simplest endpoint of that service respond with a 200 OK and a payload exactly matching what we were expecting. We had proven a port was feasible.

The Project

When we were faced with the threat of business as usual coming back after the new year, we had a decision to make: do we move ahead with the port?

By this time, we had a PR’d feature branch that had slowly incorporated everyone’s changes and the diff from master was somewhere around 20,000 lines. We decided that the amount of risk continuing with that branch was far too great and that the only option for that PR was to decline it. This was a difficult thing to do, but we knew that we had learned most of the steps we had to take to make the port happen and roughly the amount of effort involved.

Once we determined that the port was worth the effort of doing right, we pulled a couple developers that had been involved in the spike (myself included) and started a new strike team. Our team’s success criteria was simple: port one of our web services (Caracara) to the .NET Core Platform. The initiative was further broken up into the sections that make up the outline of this series.

Of note, the migration was done as small changes to our mainline branch, meaning all of our team’s changes went to production multiple times a week along with other teams’ feature work and bug fixes. We did not fork the codebase.

The important thing that made the project possible was establishing two rules at the onset:

1. PRs should be as small as possible

While this is good programming advice in general, this was especially important for our changes since they essentially amounted to a refactor. It’s tempting to tackle “refactor all the queries” in one fell swoop, but we wanted to move as fast as possible which meant having readable PRs for reviewers and optimal debugging when we introduced an issue.

2. If anything can be done to make the port to X a smaller diff, do it separately, where X is a library or framework

One of the things that surprised us in the spike was that it is totally possible to switch over to ASP.NET Core without making the major shift to the .NET Core Platform at the same time. Things like ASP.NET Core, Entity Framework Core, and .NET Standard packages can all be used by Full Framework applications. This meant we were running on Entity Framework Core in production on the Full Framework weeks before the switch to the .NET Core Platform.

Along this theme, we also prepared for breaking changes before switching to a new dependency or framework. One example of this is in our unit tests: for testing database access, we used the Effort in-memory database. We leveraged a feature of Effort, which was the ability to load seed data from CSVs. Because we knew we’d be switching to using the Entity Framework Core InMemoryProvider which didn’t have this feature, we migrated all of our tests away from relying on the CSV data. We instead added a suite of static data setup helpers, which would require almost no changes once we eventually got to switching to EF Core. Again, this change occurred over multiple PRs and before EF Core was in the picture. The Entity Framework post will contain more details about this change.

Ready?

Introductions out of the way, we’re free to dig in. Next post, you’ll see how we handled our External Dependencies. See you then!