Migrate ASP.NET CORE 2.2 to ASP.NET CORE 3.1

.NET CORE 3.1 was annonced on 3rd December 2019. Since then our team goal was to migrate all our .NET core 2.2 services to .NET core 3.1.

We tried migrating the one our API services to .NET Core 3.1 in February 2019 but failed due to open issues with Swagger Cli for swagger file generation which is solved now. With sudden breaking builds in Azure Devops, due to end of support of .NET 2.2 we have to do a lot of patching to make things work. Our team finally decided and made a priority to migrate atleast one service to .NET 3.1 so that we can then do learn and implement all the learnings going forward.

I have written all the learnings and gotchas while working on the migration task.

https://github.com/domaindrivendev/swashbuckle.aspnetcore/issues/1323

Breaking Changes for version 2.2

As all the migrations comes with some breaking changes, below the blog where you can find all the breaking changes from version 2.2 to 3.1.
https://docs.microsoft.com/en-us/dotnet/core/compatibility/2.2-3.1

Critical Changes

  • Microsoft.AspnetCore.App is removed and has been added to the shared framework
  • Json serialize is built in with System.Text.Json (We will talk later about the support for NewtonSoft in the blog)
  • IHostingEnvironment is obsolete and we should use IWebHostEnvironment
  • Microsoft.AspNetCore.App or Microsoft.AspNetCore.Razor.Design will be integrated to share framework so we don’t need to add th a project as a package.

Starting your Migration Step by Step

Change Target Framework To 3.1

Edit *.csproj file of the project and make following below changes in the Project sdk

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.0.1" />
    <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.6" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.6" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.6" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
  </ItemGroup>

</Project>

Else you can change this from Visual Studio as well:

Remove obsolete packages from *.csproj file

<ItemGroup>
   <PackageReference Include="Microsoft.AspNetCore.App" />
   <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
 </ItemGroup>

Update the package to the latest version

Startup.cs Changes

services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new Info
        {
            Title = "My API", 
            Version = "v1",
            Description="API Name",
            TermsOfService="Internal Apis",
            Contact=new Contact 
            {
                Name="Team",
                Email="Team@Email"
            }
        });
        c.EnableAnnotations();
        c.DocumentFilter<HealthChecksFilter>();
        c.DocumentFilter<SwaggerFilter>();
        var xmlFile=$"{Assembly.GetExecutingAssebly().GetName().Name}.xml";
        var xmlPath= Path.Combine(AppContext.BaseDirectory,xmlFile);
        options.IncludeXmlComments(xmlPath);
    });

To new Open Api model supported in 3.1

services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo
        {
            Title = "My API", 
            Version = "v1",
            Description="API Name",
            TermsOfService="Internal Apis",
            Contact=new OpenApiContact 
            {
                Name="Team",
                Email="Team@Email"
            }
        });
        c.EnableAnnotations();
        c.DocumentFilter<HealthChecksFilter>();
        c.DocumentFilter<SwaggerFilter>();
        var xmlFile=$"{Assembly.GetExecutingAssebly().GetName().Name}.xml";
        var xmlPath= Path.Combine(AppContext.BaseDirectory,xmlFile);
        options.IncludeXmlComments(xmlPath);
    });

Disable Endpoint Routing for now

As we only wanted to migrate to .NET Core 3.1 and not use Endpoint routing feature as of now and set the MVC CompatabilityVersion to CompatibilityVersion.Version_3_0)

services.AddMvc(o=>{
    var policy=new AuthorizationPolicyBuilder()
      .RequiredAuthenticatedUser()
      .Build()l
      o.EnableEndpointRouting= false;
      o.Filters.Add(new AuthorizeFilter(policy));
})
.AddNewtonsoftJson(opt=>opt.SerializerSetting.ReferenceLoopHandling= ReferenceLoopHandling.Ignore) // to support existing NewtonsoftJson
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0);

Use IWebHostEnvironment instead of IHostingEnvironment env in startup

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
       {
           if (env.IsDevelopment())
           {
               app.UseDeveloperExceptionPage();
           }
           else
           {
               // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
               app.UseHsts();
           }

           app.UseHttpsRedirection();
           app.UseMvc();
       }

Build Pipeline Changes

As we have all are build and deployment automated there was few steps that I made in order to build my project in Azure Devops

- task: DotNetCoreInstaller@0
    displayName: "Use .NET Core SDK Tool Installer"
    inputs:
      version: 3.1.108

  - task: DotNetCoreCLI@2
    displayName: Restore Packages
    inputs:
      command: 'custom'
      projects: '$(buildprojects)'
      custom: 'restore'
      arguments: '--configfile "$(nugetConfig)" /p:Configuration=$(buildConfiguration)'
      versioningScheme: 'off'
      feedsToUse: 'select'
      includeNuGetOrg: false

Swagger file CLI Generation while build pipeline

Refer Below Article for more details regarding swagger file generation and go to section : Retrieve Swagger Directly from a Startup Assembly https://github.com/domaindrivendev/Swashbuckle.AspNetCore#retrieve-swagger-directly-from-a-startup-assembly Follow the below steps in your local env.
Using the tool with the .NET Core 2.1 SDK

  • Install as a global tool
  • dotnet tool install -g –version 5.6.0 Swashbuckle.AspNetCore.Cli
  • Verify that the tool was installed correctly
  • swagger tofile –help
  • Generate a Swagger/ OpenAPI document from your application’s startup assembly
  • swagger tofile –output [output] [startupassembly] [swaggerdoc]

Open API Schema constraint

Once All my builds were running successfully. But my release pipeline failed at last step of Updating swagger in APIM while deploying.
As our team manages lots of small micro services which are deployed to Azure API Management to be exposed to the external world, we have some nuget packages which act as an code re-usability, where manage our swagger DocumentFilter and HealthCheckup filter in nuget library which is referenced by all our APIs.
In the CS proj file of API Project, add below commands which are needed to generate swagger file in CI Pipeline

<Target Name="Swagger" AfterTargets="PrepareForPublish" DependsOnTargets="Build" Outputs="$(PublishDir)swagger.json">
<Exec Command="dotnet swagger tofile --output $(PublishDir)swagger.json $(OutputPath)\$(AssemblyName).dll v1" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
</Exec>
<Exec Condition="'$(ErrorCode)' != '0'" Command="dotnet tool restore" />
<Exec Condition="'$(ErrorCode)' != '0'" Command="dotnet swagger tofile --output $(PublishDir)swagger.json $(OutputPath)\$(AssemblyName).dll v1" />
</Target>

When I was working on the migration in my local environment and everything was working fine. I thought my task is done and I was flying in the sky feeling this was an easy task.

But this where all my wings were cut down.
When my release pipeline was failing again and again.

I decided to upload the generated swagger file in APIM directly via import Open Api and then I found the issue where it said that the

Field description is required in response object is REQUIRED.

Finally found something where I need to work on, I made all the required fix in the APIs where we didn’t had any description in the swagger file in nuget package library and APIs like (ping).

Once these changes were merged again my release failed due to below issue:

Cannot bind parameter ‘SpecificationFormat’. Cannot convert value “OpenApi” to type “Microsoft.Azure.Commands.ApiManagement.ServiceManagement.Models.PsApiManagementApiFormat”. Error: “Unable to match the identifier name OpenApi to a valid enumerator name. Specify one of the following enumerator names and try again: Wadl, Swagger, Wsdl”

Below is the Powershell script we were using to update the swagger in APIM

$apiFullSwaggerPath = "$(System.DefaultWorkingDirectory)/$($env:apiSwaggerPath)"
Write-verbose $apiFullSwaggerPath -verbose

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 

$ApiMgmtContext = New-AzureRmApiManagementContext -ResourceGroupName $env:apiManagementRg -ServiceName $env:apiManagementName      
Write-verbose $ApiMgmtContext -verbose 

Import-AzureRmApiManagementApi -Context $ApiMgmtContext -SpecificationFormat "Swagger" -SpecificationPath $apiFullSwaggerPath -Path $env:apiUrlSuffixInAPIM -ApiId $env:apiNameInAPIM

This was failing again and again I tried changing -SpecificationFormat “Swagger” to -SpecificationFormat “OpenAPI” but release step failed again.
In the end after testing everything in my local powershell, I used the new module AzApi as shown below and updated the script with Task version *5

Write-verbose $env:apiManagementRg -verbose 
Write-verbose $env:apiManagementName -verbose 
Write-verbose $env:apiNameInAPIM -verbose 
Write-verbose $env:apiUrlSuffixInAPIM -verbose
Write-verbose $env:apiSwaggerPath -verbose

$apiFullSwaggerPath = "$(System.DefaultWorkingDirectory)/$($env:apiSwaggerPath)"
Write-verbose $apiFullSwaggerPath -verbose

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 

$ApiMgmtContext = New-AzApiManagementContext -ResourceGroupName $env:apiManagementRg -ServiceName $env:apiManagementName      
Write-verbose $ApiMgmtContext -verbose 

Import-AzApiManagementApi -Context $ApiMgmtContext -SpecificationFormat OpenApi -SpecificationPath $apiFullSwaggerPath -Path $env:apiUrlSuffixInAPIM -ApiId $env:apiNameInAPIM

Once these changes were made and saved, My release was successful and .NET Core 3.1 was release to Development environment.

References:

https://docs.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-3.1&tabs=visual-studio

View at Medium.com

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: