Friday 22 May 2020

4 - ASP.NET to ASP.NET Core

What I thought would be a fairly straightforward change was upgrading from ASP.NET (framework) to ASP.NET Core. It actually took me a month to migrate our application.

Identity to Identity Core

I knew there would be authentication issues since ASP.NET Identity (which we used on our ASP.NET app) is a .NET Framework-only library.

Microsoft rewrote the library for .NET Standard/Core as ASP.NET Core Identity - this version is also the version used by a Blazor server-side template if you opt for the built-in authentication. However the database schema used by the Core version is modified so you need to upgrade. However, I was able to create a version of the database that included the new fields required for Core, but would also work against the legacy version. This meant I could test new the app using a copy of the live security database to ensure that when we migrated the live application, I could update the database and go.

It also meant that if disaster struck we could revert the live app back to using the .NET Framework version without having to reverse the database changes. Might be worth a separate article on this alone.

DI Model

One of the key differences between ASP.NET Framework and Core is the use of the services model and Dependency Injection (DI). Our old app had a number of 'techniques' (polite word for hacks) for  getting services, so a chunk of time was required to restructure controllers, services and code to work with this new approach. It's worth it though as the DI model is much simpler and makes unit testing much easier too.

JSON Serialization

Something I hadn't anticipated was that System.Text.Json is used as the Web API serializer by default in ASP.NET Core, and this caused a lot of problems with API calls from the client JavaScript.

Initially it was behaviours like camel-casing serialized names by default, e.g. a C# property "TestThis" is serialized as "testThis". This caused a number of API calls to fail as the deserialization on the client wasn't mapping properties on the client, where we had used the C# naming style.

Then other differences with serialization and deserialization of JSON kept cropping up - so eventually I gave up trying to fix lots of code, and switched to using Newtonsoft JSON:
      }).AddNewtonsoftJson( =>
      {
          // revert to original naming when serializing
          o.SerializerSettings.ContractResolver = new DefaultContractResolver();
          //

Resources and Scripts

In the ASP.NET Core model the static resources such as CSS, JavaScript and Images are all located in a new folder wwwroot. I took the upgrade as an opportunity to move our client-side resources such as Bootstrap, JQuery etc. to be loaded using LibMan. I'm not a fan of NPM and using that would probably also mean we'd need to implement build scripts such as Gulp or WebPack to copy files around. LibMan avoids this by allowing you to specify the libraries and even specific files and their destination.
Our own Scripts were located in a /Scripts folder using TypeScript, so I had reconfigure this to output the resulting JavaScript into the wwwroot folder. However when debugging we had an issue: the source TypeScript is only present in the /Scripts folder, which isn't part of the published application. I fixed that by amending the .csproj file with this section:
  <!-- copies TS source to wwwroot (only for Debug build) -->
  <!-- source: https://github.com/NuGet/Home/issues/6743 -->
  <Target Name="CopyTsToWwwRoot" BeforeTargets="Build" Condition="'$(Configuration)'=='Debug'">
    <Message Text="Copying scripts/ts to wwwroot" />
    <ItemGroup>
      <SourceTs Include="$(MSBuildProjectDirectory)\Scripts\**\*.ts" />
    </ItemGroup>
    <Copy SourceFiles="@(SourceTs)" DestinationFiles="@(SourceTs -> '$(MSBuildProjectDirectory)\wwwroot\scripts\%(RecursiveDir)%(Filename)%(Extension)')" />
  </Target>

This section will copy the .ts files into the target wwwroot folders and retain the folder structure, so the files are present.

The only thing I wasn't able to fix with LibMan was the loading of TypeScript type definitions, so I had to use NPM to load these. However these are development-only to enable TypeScript compilation, so we didn't need to copy the files into wwwroot so I was able to avoid having to implement WebPack etc.

Web Jobs

Our app runs on Azure and we also hosted a number of background WebJobs to perform various long running tasks and process Azure queues. Although we could retain these I wanted to bring these into the web application using the IHostedService support in ASP.NET Core 3.1

This is actually much better than WebJobs since the code isn't located in the AppData folder and is easier to set up and control. It shares the same IConfiguration system as the main web application as well. It's not strictly required for upgrading to using Blazor but I'd strongly recommend using this service over WebJobs.

3 - Build Process

Hopefully you have an automated build process!

Azure DevOps, GitHub and many other sources provide this as a service and once you've converted to using it you'll never go back. We use Azure DevOps for our code and CI/CD capabilities so it was pretty easy to upgrade.

Most of our libraries were written a while ago and used the "classic" build tool, which had a GUI interface and helped you create the build pipeline. However, it was really tedious to use on lots of projects, as you had to manually re-create the same process over and over for each one.

The newest hotness is the Azure Pipelines YAML support - you can now add a build file to source control and then configure a build pipeline to use this. The build pipeline is now part of the source control process and you can edit/upgrade/copy these more easily.

As most of my libraries follow a standard format I re-used the same .YML file in each project, just editing a single line to specify which project I wanted to package into a Nuget library.

Once I'd pushed the repository up to Azure DevOps, I just created a new pipeline and pointed it at the YML file in the repo.

# dotnet standard library build-test-pack-nugetpush

# This build is triggered if changes are pushed to the master branch
trigger:
- master

# Use the 'Default' agent pool (our own hosted agents)
# if you want to use Azure DevOps built-in agents, change to
#  vmImage: 'ubuntu-latest'
# or
#  vmImage: 'windows-latest'

# since this is proejct .NET Std and .NET Core either will work
# if it still has .NET Framework, use the windows version
pool:
  name: Default

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'
  project: '[project-folder]/[project-name].csproj'
  # this is used to signal which project is to be packaged into a Nuget package

steps:

# ensure the latest 3.1.x SDK is loaded for the build
- task: UseDotNet@2
  displayName: 'Load SDK using 3.1.x'
  inputs:
    version: 3.1.x

# restore packages, and include any in our internal feed
- task: DotNetCoreCLI@2
  displayName: 'restore packages'
  inputs:
    command: restore
    vstsFeed: '[internal-feed-ID]'

- task: DotNetCoreCLI@2
  displayName: Build solution
  inputs:
    projects: '$(solution)'
    arguments: '--configuration $(BuildConfiguration)'

- task: DotNetCoreCLI@2
  displayName: Run Unit tests
  inputs:
    command: test
    projects: '$(solution)'
    arguments: '--configuration $(BuildConfiguration)'

# this is a VSTS task I created to set environment variables from the project

# version and append the build number (it's in the marketplace)
#
# the old NUGET PACK used to allow wildcards e.g. 1.2.* but "dotnet pack" does not.
# e.g. if the project is version 1.2.3 this sets VERSION_BUILD to 1.2.3.[buildNo]

# Note: this uses PowerShell so won't work for ubuntu agents
#
- task: conficient.VersionReaderTask.version-reader-build-task.VersionReaderTask@1
  displayName: 'Generate build variables '
  inputs:
    searchPattern: '$(project)'
    buildPrefix: '.'

# package the project and version using the environment variable
- task: DotNetCoreCLI@2
  displayName: Pack
  inputs:
    command: pack
    packagesToPack: '$(project)'
    versioningScheme: byEnvVar
    versionEnvVar: 'VERSION_BUILD'

# push the .nupkg packages to our internal Nuget feed
- task: DotNetCoreCLI@2
  displayName: 'Push'
  inputs:
    command: 'push'
    packagesToPush: '$(Build.ArtifactStagingDirectory)/*.nupkg'
    nuGetFeedType: 'internal'
    publishVstsFeed: '[internal-feed-ID]'  # change this to your feed

# I've commented the pipeline to help you understand what each part does


2 - Nuget Packages

Nuget Packages

The second big issue with moving to .NET Standard is that any Nuget packages you consume need to have a .NET Standard support. Any Framework-only packages have to be replaced with something equivalent.

Common packages such as Newtonsoft.JSON already do this, so in many cases you won't need to do anything. Other packages might have a .NET Standard version with a different name. Some packages just won't and you'll need to find alternatives - or perhaps lobby the package owner to create a .NET Standard version.

We used iTextSharp 4.x in our application. This was a .NET Framework only package, but there was an iTextSharp 7 release that did support .NET Standard, but was now a chargeable package, and had a number of API changes from the 4.x version which would have meant a lot of rewriting our PDF generation library to work with it.

A search led us to iTextSharp.LGPLv2.Core which was a .NET Standard port of the 4.x version - it was totally compatible with our existing code, and still under a free licence, so we decided to use this.

Not all .NET Framework functionality is present in .NET Standard 2.0 but there are often Microsoft Nuget packages to covert these. An example is System.Drawing - a lot of the Framework version used the Windows GDI libraries so wasn't going to work for a portable framework like .NET Core/Standard.

Downstream Issues

One aspect of changing the package dependencies on upstream libraries is that every downstream library then has to use the same version as well. 

In .NET Framework I often had to include packages used by upstream libraries as transitive dependencies in .NET Framework never really worked very well, and this leads onto the mess that are Binding Redirects trying to resolve these.

The good news is that this is pretty much fixed in .NET Standard and .NET Core - a downstream app can use an upstream library and the dependencies will be included. You only need to import the packages directly if the app uses the library directly itself.

As part of my upgrade to .NET Standard I had the very satisfying task of deleting a lot of app.config files from the libraries.


5 - Adding Blazor

So in the previous step 4, we had upgraded our application to ASP.NET Core 3.1, but we still had no actual Blazor anywhere in the system. Ho...