.NetFramework类库迁移到.NetCore过程记录
适用场景
场景描述
目前在.Net开发中.Net Core已经是大势所趋了,未来的.Net 5也被认为是下一代的.Net Core。.Net Framework作为一个曾经的老大哥确实正在逐渐步下神坛,但是从.Net Framework到.Net Core的转换依然需要一个过程。相信各位小伙伴在工作中必然遇到过需要在新的.Net Core项目中引用以前的.Net Framework类库的问题。如果类库只有几个文件的话还好,直接复制粘贴重新创建.Net Core类库也不复杂,但是如果类库结构比较复杂,而且旧项目和新项目要一齐引用它的话就可能需要将这个类库变成多目标框架类库。
多目标框架类库
目标框架
所谓的目标框架可以理解为不同的CLR,如.NetFramework和.NetCore就是不同的目标框架,此外还有Xamarin等。在Visual Studio中右键项目选择属性即可看到。
多目标框架类库
所谓多目标框架类库就是一个类库可以被不同目标框架的项目引用,看起来似乎有点不可思议,一个dll怎么可能被不同的目标框架引用呢?但实际上是可以的。
如果是以.NetStandard为目标框架的类库,那么它天生就可以被不同目标框架的的项目引用,至于为什么,可以参考以下文档。
.NetStandard概述
如果不是也可以采用多目标框架的形式被不同目标框架引用,也就是该类库生成的时候会根据指定好的多个目标框架生成多个不同的dll,当被引用时会自动根据启动项目的类型选择对应的dll引用。像我们在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格式。
转换之前建议把类库中引用的第三方Nuget和版本找个东西记下来。
旧格式
新格式
可以看到,旧格式里面不仅保存了项目引用还有各种杂七杂八的东西,非常不好阅读。新格式中几乎就只保存了项目的引用,看上去非常舒服。
当然这个转换肯定不可能由人来完成。这时需要用到一个叫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了,所以引用的第三方库可以尽情使用最新的稳定版(考虑好代码兼容性)。
部分代码可能需要兼容实现
如果类库中某些调用的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中的左上角切换。
另外,因为类库已经是多目标框架了,所以类库项目的属性中目标框架一栏会变成灰色,这是正常的。
最后,还有一个注意的地方,后续如果这个类库还要引用其他Nuget,要注意选择好版本,在csproj文件中做好不同目标框架的版本控制。
参考文章
.NetStandard概述
将 .NET Framework 项目转换为 .NET Standard 项目
SDK 样式项目中的目标框架