差不多三年前,我发表了一篇博客,介绍了将一个WPF应用程序迁移到.NET Core 3的过程。我迁移的应用程序是一个简单的商品交易样本,叫做Bean Trader。不过,当时我只迁移了样本解决方案的一部分。Bean Trader解决方案包括一个WPF客户端和一个服务器端的应用程序,客户端与之通信以提议和接受交易。客户端和服务器使用WCF进行通信。由于.NET Core 3(以及随后的版本,如.NET 5和6)支持客户端的WCF API,但不支持服务器端的,所以我能够迁移Bean Trader客户端,但不得不让服务器运行在.NET Framework上。
随着最近宣布CoreWCF已经达到1.0,我很高兴能完成将Bean Trader样本升级到.NET 6的工作。
关于CoreWCF
CoreWCF是一个社区驱动的.NET基金会项目,它使WCF的表面区域在现代版本的.NET上可用。虽然CoreWCF不是微软的项目,但微软已经宣布它将为CoreWCF提供产品支持。虽然较新的技术如gRPC和ASP.NET WebAPI仍被推荐用于新的开发,但CoreWCF是一个很好的选择,可以帮助已有大量WCF依赖的项目转移到.NET 6。
尽管CoreWCF支持许多常见的WCF场景,但它并不支持所有的WCF功能。你的现代化经验可能会有所不同,这取决于你的WCF使用和CoreWCF中包含的功能之间的重合。如果你所依赖的功能还没有在CoreWCF中出现,请在Feature Roadmap问题中提供反馈,以便CoreWCF项目维护者能够根据社区的需要来确定工作的优先次序。
关于实例
Bean Trader应用(可在GitHub上找到)是我几年前创建的一个样本,在演示如何迁移到.NET Core时使用。因为这个样本最初只是为了展示客户端的现代化,所以Bean Trader服务相当简单。它由一个带有模型和接口的类库和一个控制台应用组成,控制台应用承载了一个WCF服务(使用带有证书认证的Net.Tcp传输),允许客户提出或接受不同颜色的豆子的交易。尽管这个示例程序很小,但我认为看到用于将其迁移到.NET 6的过程仍然是有用的。

迁移
运行升级助手
为了使迁移更快,我将使用.NET升级助手。升级助手是一个命令行工具,可以帮助用户交互式地将项目从.NET框架升级到.NET标准和.NET 6。升级助手还不能自动地从WCF迁移到CoreWCF(尽管它在项目的积压中),运行该工具仍然是有用的,这样项目文件将被迁移,NuGet包的引用将被更新,等等。在工具运行后,我将进行必要的修改,从WCF手动迁移到CoreWCF。
为了安装升级助手,我运行了follow .NET SDK命令:
dotnet tool install -g upgrade-assistant
安装了升级助手后,我可以通过在BeanTrader解决方案文件上运行它来开始迁移过程。通过在解决方案文件上运行(而不是一个特定的项目),我可以通过一次执行工具来升级类库和服务器控制台应用程序。
升级解决方案的命令是:
upgrade-assistant upgrade BeanTrader.sln
然后,该工具引导我完成一系列的升级步骤(这些步骤都是由升级助手自动处理的),如图所示:

我通过升级助手应用的完整步骤列表是。:
- 选择一个入口点。这允许我选择我最终想在.NET 6上运行的项目。根据选择,升级助手将决定哪些项目要升级,以何种顺序进行。我选择BeanTraderServer。
- 选择一个要升级的项目。这就转换了工具,开始升级一个特定的项目。它建议先升级BeanTraderCommon,然后再升级BeanTraderServer,这很合理,所以我将选择这个顺序。
- 备份BeanTraderCommon。
- 将BeanTraderCommon转换为SDK风格的项目。
- 更新BeanTraderCommon的NuGet包。这一步用System.ServiceModel.NetTcp等NuGet包代替System.ServiceModel.ServiceModel.NetTcp的汇编引用。
- 更新BeanTraderCommon的目标框架(TFM)。该工具推荐.NET Standard 2.0,因为该项目是一个没有任何.NET 6特定依赖的类库。
- 在这一点上,辅助库的升级已经完成,升级助手切换到升级BeanTraderServer。
- 备份BeanTraderServer。
- 将BeanTraderServer转换为一个SDK风格的项目。
- 用相等的NuGet包替换System.ServiceModel引用,正如对BeanTraderCommon项目所做的那样。
- 更新BeanTraderServer的目标框架。该工具推荐.NET 6,因为该项目是一个控制台应用程序。
- 禁用不支持的配置部分。升级助手检测到BeanTraderServer的app.config文件中有一个system.ServiceModel部分在.NET 6上不被支持(它将导致运行时错误),所以它为我注释了这个部分。稍后,我们将在另一个文件中重新使用这个被注释掉的部分来配置我们的CoreWCF服务。
- 在检查C#源代码以进行任何必要的修改时,升级助手对WCF的使用发出警告。警告信息提醒我,BeanTraderServer使用了服务器端的WCF API,这些API在.NET 6上不被支持,也不被升级工具所升级。它告诉我,我需要进行手动修改,并建议升级到CoreWCF、gRPC或ASP.NET Core。
1.

- 升级清理。在这一点上,升级助手已经完成了,所以它删除了一些临时文件并退出。
CoreWCF迁移
现在,升级助手已经完成了启动升级过程的工作,是时候升级到CoreWCF了。在Visual Studio中打开Bean Trader解决方案,我发现BeanTraderCommon类库构建成功了。该项目升级到.NET标准的工作已经完成。但BeanTraderServer项目有一些错误,与无法找到一些WCF类型有关。

为了开始升级到CoreWCF,我为CoreWCF.NetTcp 1.0版本添加了一个NuGet包参考。我还将BeanTrader.cs中的using System.ServiceModel; 导入改为using CoreWCF; 。这解决了所有的错误,除了program.cs中关于我如何创建一个ServiceHost的错误。
CoreWCF是建立在ASP.NET Core之上的,所以我需要更新项目以启动一个ASP.NET Core主机。BeanTrader样本是一个自我托管的服务项目,所以我只需要做一些小改动,设置一个ASP.NET Core主机来运行我的服务,而不是直接使用ServiceHost。为此,我将项目的SDK更新为Microsoft.NET.Sdk.Web (因为它使用了ASP.NET Core),使应用程序的Main方法成为异步的,并用下面的代码取代ServiceHost设置。
有不同种类的WCF项目(不是所有的项目都会直接创建和启动ServiceHost实例),但所有的CoreWCF应用程序都是作为ASP.NET Core端点运行。这里显示的代码使用了新的.NET 6最小API语法,以最小的代码启动和运行ASP.NET Core主机,但如果你愿意,使用其他ASP.NET Core语法也是可以的(例如,有一个单独的Startup.cs)。CoreWCF样本展示了这两种方法。
请注意,证书配置是从旧样本中复制的,仅用于演示目的。真实世界的解决方案会使用来自机器的证书存储区或Azure Key Vault等安全位置的证书。另外,这是一个很好的例子,说明在使用CoreWCF时可以改变服务主机属性,但设置服务器证书的场景是针对NetTcp场景的。对于HTTPS端点,SSL是通过ASP.NET Core APIs设置的,就像在其他ASP.NET Core应用程序中一样。
var builder = WebApplication.CreateBuilder();
// Add CoreWCF services to the ASP.NET Core app's DI container
builder.Services.AddServiceModelServices();
var app = builder.Build();
// Configure CoreWCF endpoints in the ASP.NET Core host
app.UseServiceModel(serviceBuilder =>
{
serviceBuilder.ConfigureServiceHostBase<BeanTrader>(beanTraderServiceHost =>
{
// This code is copied from the old ServiceHost setup and configures
// the local cert used for authentication.
// For demo purposes, this just loads the certificate from disk
// so that no one needs to install an untrusted self-signed cert
// or load from KeyVault (which would complicate the sample)
var certPath = Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location), "BeanTrader.pfx");
beanTraderServiceHost.Credentials.ServiceCertificate.Certificate = new X509Certificate2(certPath, "password");
beanTraderServiceHost.Credentials.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None;
});
});
await app.StartAsync();
我还用await app.StopAsync() 替换了原始示例中的host.Close() 调用。
在这一点上,应用程序的构建是没有错误的!唯一需要解决的问题是确保服务的配置是按预期加载的。
配置更新
如前所述,.NET 6默认不包括system.serviceModel部分。然而,很多现有的WCF应用程序都使用app.config和web.config来设置绑定关系。为了便于迁移,CoreWCF包含了可以从xml配置文件中明确加载配置的API。
为了利用Bean Trader服务器的WCF配置,我首先添加了一个对CoreWCF.ConfigurationManager包的引用。然后,我将app的app.config中的system.serviceModel部分(它被升级助手注释掉了)移到一个新的配置文件中。这个文件可以有任何名字,但我的文件叫 "wcf.config"。
在WCF和CoreWCF之间,WCF配置所支持的内容有一些小的差别,所以我需要对wcf.config做如下修改:
IMetadataExchange在CoreWCF中还不支持,所以要删除mex端点。不过,我仍然可以让WSDL可供下载。接下来我将展示如何做到这一点。<host>元素在服务模型配置中不被支持。相反,端点所监听的端口是在代码中配置的。因此,我需要从wcf.config中删除<host>元素,并在应用程序的主方法中添加以下一行:builder.WebHost.UseNetTcp(8090);。这句话应该放在对builder.Build的调用之前。
最后,我更新应用程序的主方法,将配置添加到ASP.NET Core应用程序的依赖注入容器中:builder.Services.AddServiceModelConfigurationManagerFile("wcf.config"); 。
在这一点上,该应用程序将工作,客户应该能够成功地连接到它。不过我还是想让WSDL可用,所以我打算对项目再做一些改动。首先,我在main方法中加入以下代码,让ASP.NET Core应用程序监听8080端口(因为这是之前下载的WSDL的端口):
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(8080);
});
其次,在注册服务时,我将添加一个对builder.Services.AddServiceModelMetadata() 的调用,以确保元数据服务是可用的,然后我将获得已注册的ServiceMetadataBehavior 实例(作为一个单子),并修改它以使WSDL下载成为可能。这段代码需要在构建应用之后,但在启动应用之前进行:
// Enable getting metadata/wsdl
var serviceMetadataBehavior = app.Services.GetRequiredService<ServiceMetadataBehavior>();
serviceMetadataBehavior.HttpGetEnabled = true;
serviceMetadataBehavior.HttpGetUrl = new Uri("http://localhost:8080/metadata");
有了这些改动,Bean Trader服务现在已经完全迁移到了.NET 6!我可以启动应用程序并连接到.NET 6。我可以启动应用程序,用现有的客户端连接到它。而且WSDL在localhost:8080/metadata上可用。要看到这篇文章中讨论的所有变化,你可以看看这个拉动请求,它更新了Bean Trader样本,这样,最后,样本的NetCore文件夹只包含了.NET Core和.NET 6的目标项目:

总结
Bean Trader样本只是一个小的应用程序,但希望这个示例能展示出让WCF服务在.NET 6上使用CoreWCF所需的各种变化。除了引用一些不同的命名空间外,WCF服务的实现没有变化,大部分的配置xml都被重新使用。我不得不对服务主机的创建方式做一些改变(现在服务是由ASP.NET Core托管的),但我仍然能够重复使用以前用于定制服务主机行为的代码。