转换.NetFramework类库到多目标框架,可以同时匹配NetFramework和NetStandard以及NetCore
微软的Net Framework作为一个曾经的老大哥,在Windows下取得了非常大的成功,但如果要匹配多平台,多设备的应用需求,Net framework显然无法胜任,所以微软没有停止发展的脚步,依次搞出了Net Standard,NetCore,Net5.0,Net6.0等等。虽然一脉相承,但又互不兼容,我相信很多小伙伴在工作中必然遇到了这种情况,以前大量在Net Framework中创建的库文件很难同时用于Net Core或更新的项目,虽然简单的项目可以很容易迁移,但也解决不了同时在2套体系下维护的需求.
因此,微软搞了一个多目标框架类库的模式,这种模式有一定的灵活性,但也会带来很多冲突。不过在一定程度上是可以解决多目标框架的同时适用性。所以也不失为一个可以考虑的方案
多目标框架类库
目标框架
所谓的目标框架可以理解为不同的CLR,如.NetFramework和.NetCore就是不同的目标框架,此外还有Xamarin等。在Visual Studio中右键项目选择属性即可看到。
多目标框架类库
所谓多目标框架类库就是一个类库可以被不同目标框架的项目引用,看起来似乎有点不可思议,一个dll怎么可能被不同的目标框架引用呢?但实际上是可以的。
如果是以.NetStandard为目标框架的类库,那么它天生就可以被不同目标框架的的项目引用
像我们在Nuget中看到的类库大部分都是多目标框架类库,例如下图的Newtonsoft.Json就支持那么多的目标框架。
场景假设
假设现在我们有一个.NetCore 3.1的项目需要引用一个.NetFramework4.5.2的类库,同时该类库还被许多旧项目引用。此时,我们可以有两种方案来解决这个问题。
- 将这个类库变成.NetStandard类库(一了百了,谁都能用)
- 将这个类库保留.NetFramework4.5.2的目标框架,添加其他目标框架,作为一个多目标框架类库。
显然这里第一种方案是最省事的,但是如果这么简单就没必要特地写这篇文章记录了。根据微软自己的文档,.NetFramework4.5.2只能使用.NetStandard1.2,而1.2版本里面很多方法都用不了,最坑的是,很多引用到Nuget包还不支持.NetStandard1.2。
例如Dapper这个很常用的第三方库。Dapper是从1.5.0开始支持.NetStandard的,但是最低版本也要求.NetStandard 1.3 。由于我们还有其他.NetFramework的老项目要用这个类库,所以不能将它直接改成.NetStandard。需要在保持.NetFramework 4.5.2目标框架的基础上添加.NetStandard 2.0的目标框架,将它变成多目标框架类库。
迁移步骤
首先说明,迁移的过程中主要改动都针对类库的csproj文件进行。
先决条件
迁移之前有一些条件需要满足。
1. 必须要清楚迁移的类库引用了什么第三方库及版本。
因为后续我们需要根据不同的目标框架调整类库的引用项及版本,所以这里必须清楚原类库引用了什么,这样才能保证原有的东西不被影响。
2. 保证类库依赖的第三方库(or dll)有对应的目标框架版本
例如上例中的类库引用了Dapper,Dapper是支持.NetFramework 4.5.2和.NetStandard 2.0的,但是如果类库中直接引用dll,而这颗dll只有一个目标框架(非.NetStandard)的版本,那就不能迁移了。
3. 最好将dll引用换成Nuget引用
一些老项目特别喜欢直接用文件路径引用dll,直接将目标框架绑死,这时候就需要先查一下有没有相同的Nuget可以引用,如果有,就去除该dll的引用,改成引用Nuget。
示例
下文我将以一个.NetFramework 4.5.2的类库为例子,将其改造成即支持.NetFramework 4.5.2也支持.NetStandard 2.0的多目标框架类库。
该类库一开始引用了以下的第三方库。
Dapper v1.60.6
Microsoft.Data.Sqlite v1.1.1
类库代码只有一个执行SQL语句的方法。
using System;
using Dapper;
using Microsoft.Data.Sqlite;
namespace MyLib
{
public class MyClass
{
public string ConnStr { get; set; }
public MyClass(string connStr)
{
this.ConnStr = connStr;
}
public int ExecSQL(string sql, object param)
{
int ret = 0;
SqliteConnection conn = new SqliteConnection(ConnStr);
try
{
ret = conn.Execute(sql, param);
}
catch (Exception)
{
throw;
}
finally
{
conn.Dispose();
}
return ret;
}
}
}
csproj文件转换
第一步需要将旧的csproj文件格式转成新的csproj格式。
这时需要用到一个叫try-convert的工具,是微软自家的,可以安心使用。
安装:
打开cmd终端输入以下指令安装,注意需要.NetCore的SDK。
dtonet tool install --global try-convert
使用也是非常简单,在cmd中进入到类库目录,即csproj所在文件夹。直接执行try-convert即可。
cd {类库文件夹}
try-convert
完成后将看到项目的csproj文件变成了新的格式,旧文件也替你备份成{类库名}.csproj.old。
处理引用
工具处理完之后就该我们处理了,由于工具默认是帮我们将类库转成.NetStandard 2.0的版本,所以我们还要手动将它调回.NetFramework 4.5.2的版本。因为我们首先要保证原本引用这个类库的项目还能继续引用,然后再考虑增加新的目标框架被新项目引用。
保证原目标框架可使用
还记得前文提到的先决条件中的第一条吗?必须要清楚原类库引用的第三方库和版本。 这里就需要我们将这些类库保留,同时去除不需要引用。
刚转换完成的csproj文件。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<OutputType>Library</OutputType>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<ItemGroup>
<Reference Include="System.IO.Compression" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="System.ComponentModel.Composition" Version="5.0.0" />
<PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="1.60.6" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="1.1.1" />
<PackageReference Include="Microsoft.NETCore.Platforms" Version="1.1.0" />
<PackageReference Include="NETStandard.Library" Version="1.6.1" />
<PackageReference Include="SQLite" Version="3.13.0" />
<PackageReference Include="System.Collections" Version="4.3.0" />
<PackageReference Include="System.Collections.Concurrent" Version="4.3.0" />
<PackageReference Include="System.Diagnostics.Debug" Version="4.3.0" />
<PackageReference Include="System.Diagnostics.Tools" Version="4.3.0" />
<PackageReference Include="System.Diagnostics.Tracing" Version="4.3.0" />
<PackageReference Include="System.Globalization" Version="4.3.0" />
<PackageReference Include="System.IO" Version="4.3.0" />
<PackageReference Include="System.IO.Compression" Version="4.3.0" />
<PackageReference Include="System.Linq" Version="4.3.0" />
<PackageReference Include="System.Linq.Expressions" Version="4.3.0" />
<PackageReference Include="System.Net.Primitives" Version="4.3.0" />
<PackageReference Include="System.ObjectModel" Version="4.3.0" />
<PackageReference Include="System.Reflection" Version="4.3.0" />
<PackageReference Include="System.Reflection.Extensions" Version="4.3.0" />
<PackageReference Include="System.Reflection.Primitives" Version="4.3.0" />
<PackageReference Include="System.Resources.ResourceManager" Version="4.3.0" />
<PackageReference Include="System.Runtime" Version="4.3.0" />
<PackageReference Include="System.Runtime.Extensions" Version="4.3.0" />
<PackageReference Include="System.Runtime.InteropServices" Version="4.3.0" />
<PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" />
<PackageReference Include="System.Runtime.Numerics" Version="4.3.0" />
<PackageReference Include="System.Text.Encoding" Version="4.3.0" />
<PackageReference Include="System.Text.Encoding.Extensions" Version="4.3.0" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.0" />
<PackageReference Include="System.Threading" Version="4.3.0" />
<PackageReference Include="System.Threading.Tasks" Version="4.3.0" />
<PackageReference Include="System.Threading.Timer" Version="4.3.0" />
<PackageReference Include="System.Xml.ReaderWriter" Version="4.3.0" />
<PackageReference Include="System.Xml.XDocument" Version="4.3.0" />
</ItemGroup>
</Project>
首先,我们需要把目标框架改回net452,再把多余的引用去除。
何为多余的引用呢? System开头的,转换前没有引用的,全部去除。
至于为什么会产生这么多多余引用,是因为工具帮我们转换成了.NetStandard 2.0的类库但是类库中原本引用的第三方Nuget类库版本却不是支持.NetStandard 2.0的版本,所以它需要将旧的版本中引用到的东西全部引用回来。而现在我们需要改回.NetFramework 4.5.2,既然我们以前就没有引用过这些东西,现在自然也不用引用它们,所以大胆地删掉就好了。
将xml中的TargetFramework节由netstandard2.0改回net452, 把原先没有引用的包全部去掉。
因为我类库原先只引用了Dapper 和Microsoft.Data.Sqlite 所以处理完成之后的csproj文件变成这个样子。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net452</TargetFramework>
<OutputType>Library</OutputType>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<!--保留net452的引用-->
<ItemGroup>
<PackageReference Include="Dapper" Version="1.60.6" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="1.1.1" />
</ItemGroup>
</Project>
此时可以在旧项目中测试一下这个类库能否继续使用,把引用都整理好并测试可用之后就可以进行下一步:引入新的目标框架。
引入新目标框架
此时,我们希望在net452的基础上添加netstandard2.0的目标框架以便后续供.NetCore或者Xamarin使用。
首先,将TargetFramework节改成复数形式TargetFrameworks。
然后在记录引用的ItemGroup 中添加条件控制。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net452</TargetFrameworks>
<OutputType>Library</OutputType>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<!--保留net452的引用-->
<ItemGroup Condition=" '$(TargetFramework)' == 'net452' ">
<PackageReference Include="Dapper" Version="1.60.6" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="1.1.1" />
</ItemGroup>
<!--使用netstandard的引用-->
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
<PackageReference Include="Dapper" Version="2.0.78" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="5.0.0" />
</ItemGroup>
</Project>
因为这个类库已经可以支持.Net Standard 2.0了,所以引用的第三方库可以尽情使用最新的稳定版(考虑好代码兼容性)。
部分代码可能需要兼容实现
并不是所有代码都可以兼容实现,特别是vb.net,不同的框架的差异性导致灾难性的结果,此时要么暂缓迁移,要么采用重建项目文件的方式来实现迁移
如果类库中某些调用的API存在不兼容的问题,就需要使用预处理器指令编写条件代码,针对每个目标框架进行编译。
private string Test()
{
string ret = "";
#if NET452
ret = "Target framework: .NET Framework 4.5.2";
#elif NETSTANDARD2_0
ret = "Target framework: .NET Standard 2.0";
#else
ret = "Unknown";
#endif
return ret;
}
至此,该类库已经可以支持多目标框架了,不同目标框架可以在VS中的左上角切换。
另外,因为类库已经是多目标框架了,所以类库项目的属性中目标框架一栏会变成灰色,这是正常的。
后续,如果还需要添加其他目标框架,也需要手动编辑项目文件的方式添加,暂时没有更好的方式。
最后总结:多目标框架不是灵丹妙药,使用此模式会相入更多兼容性问题。所以除非不得已,需要同时在旧项目中作代码维护。一般不推荐使用多目标框架模式