With every .Net release, there are mixed emotions that come with it. Excited for learning and imagining ways to use new framework and language features, but then the reality sets in of how in the hell am I going to do this and still support my projects that are not quite ready to be upgraded to the latest framework version.
Case in point, Azure Functions
I have a fairly large project with multiple web sites and api projects. In addition, the solution also has a few Azure Function projects. Now the issue becomes, how do I start moving towards .Net Core 3.1 in my web projects while still supporting Azure Functions, which as of this post, doesn't yet support .Net Core 3.0 or higher in production? In fact, if you pull in a reference to a Microsoft.Extensions.DependencyInjection > version 2.2, it will blow chunks with:
Caution
A host error has occurred during startup operation '8f9792f9-2f7b-466f-a197-14fbdc127cde'. <your_function_project_output_name>: Method not found: 'Microsoft.Extensions.DependencyInjection.IServiceCollection Microsoft.Azure.Functions.Extensions.DependencyInjection.IFunctionsHostBuilder.get_Services()'. Value cannot be null. Parameter name: provider
The kicker on this one, is the DependencyInjection reference can be pulled in by any library/package reference in your dependency chain. So let's talk about a few options to implement in your shared libraries to prevent this type of issue. For example, we will take a package MySharedLibrary and support .Net Core 2.0 or 3.0 versions.
Note
Azure Functions support for .Net Core 3.x is in preview, but it is not supported in production workloads as of this post.
Specific Package Version Alignment
The most obvious way is to keep your package version aligned with major versions of the .Net Core releases.
Important
This can create a scenario where you now have 2 release branches you now need to maintain (1 for each package). Not a great solution, but it will work.
Package MySharedLibrary v2.x
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<AssemblyName>MySharedLibrary</AssemblyName>
<Version>2.0</Version>
</PropertyGroup>
<ItemGroup >
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
</ItemGroup>
Package MySharedLibrary v3.x
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyName>MySharedLibrary</AssemblyName>
<Version>3.1</Version>
</PropertyGroup>
<ItemGroup >
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.0" />
</ItemGroup>
Target Multiple Frameworks
If you are like me and like to maintain versioning scoped to the project at hand and not to the major framework, then targeting multiple frameworks in the same package is the way to go.
Note
Using the .NetStandard versions, you can target framework ranges instead of specific framework versions. In this case, we want to use .netstandard2.0 and .netstandard2.1 to target .Net Core 2.x and 3.x respectively. Notice the tag changes from TargetFramework to TargetFrameworks.
Package MySharedLibrary v1.x (Option #1)
Use the lowest common version package of Microsoft.Extensions.DependencyInjection version 2.2. When this package is consumed by a .Net Core 2.x web application version 2.2 is used and if it is consumed by a .Net Core 3.x web applicaiton the latest version of 3.1 for the Microsoft.Extensions.DependencyInjection is referenced auto-magically by the web sdk.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<AssemblyName>MySharedLibrary</AssemblyName>
<Version>1.0</Version>
</PropertyGroup>
<ItemGroup >
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
</ItemGroup>
Package MySharedLibrary v1.x (Option #2)
Another option is to be explicit on the package reference based on target framework using msbuild variable $(TargetFramework).
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<AssemblyName>MySharedLibrary</AssemblyName>
<Version>1.0</Version>
</PropertyGroup
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.1' ">
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
</ItemGroup>
.Net Version Symbols to Explicitly Target Framework Apis
Using preprocessor symbols to include/exclude code can be leveraged using the appropriate version symbol for your needs.
Note
In the example below, we will target the IAsyncEnumerable class and async-streams language features available only in .Net Core 3.x frameworks using symbols. Notice the preprocessor directives #if, #else and #endif to control what code is compiled based on the condition of the symbol.
// MIT License Copyright 2020 (c) David Melendez. All rights reserved. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
namespace Microsoft.Azure.Cosmos.Table
{
public static class AzureSdkHelper
{
//Begins including code available for .Net Core 3.x
#if NETSTANDARD2_1
public static async IAsyncEnumerable<DynamicTableEntity> ExecuteQueryAsync(this CloudTable ct, TableQuery tq)
{
TableContinuationToken t = new TableContinuationToken();
while (t != null)
{
var segment = await ct.ExecuteQuerySegmentedAsync(tq, t);
foreach (var result in segment.Results)
{
yield return result;
}
t = segment.ContinuationToken;
}
}
// This code is only included if the framework is not .netstandard2.1 (.Net Core 3.x)
#else
public static Task<IEnumerable<DynamicTableEntity>> ExecuteQueryAsync(this CloudTable ct, TableQuery tq)
{
return Task.Run<IEnumerable<DynamicTableEntity>>(() => ExecuteQuery(ct, tq));
}
#endif
public static IEnumerable<DynamicTableEntity> ExecuteQuery(this CloudTable ct, TableQuery tq)
{
TableContinuationToken t = new TableContinuationToken();
while (t != null)
{
var segment = ct.ExecuteQuerySegmented(tq, t);
foreach (var result in segment.Results)
{
yield return result;
}
t = segment.ContinuationToken;
}
}
}
}
Summary
We looked at ways to structure .Net packages to support multiple framework versions by using seperate versioned packages or (the most desirable option) to target multiple frameworks within the same package version. Then, we looked at using preprocessor directives with framework symbols to control code that gets compiled with each framework target. Using one or more of these techniques should keep your code backward compatible while enabling your codebase to stay current with the latest language and framework features.
Open a discussion Tweet