This blog post in the series on alternatives for the Windows Communication Foundation (WCF) describes the particularities and challenges regarding a WCF migration in preparation of the subsequent porting of the application to .NET Core.
This post will first address ASP.NET Core Web API as a possible alternative, and describe, step by step, how a migration from WCF to ASP.NET Core Web API can be done. The procedure for the migration to gRPC, on the other hand, is described in the next blog post.
Migration procedure
Usually, there is a separate WCF project in the solution. As a direct conversion is not possible, this project can remain unchanged in the solution for the time being.
You should first create a new class library project for shared objects between the server and the client. Then copy the ServiceContract interfaces and the DataContract classes from the WCF project to this project, and remove the WCF-specific attributes such as “ServiceContract”, “OperationContract”, “DataContract”, “DataMember”, etc.
Client project
First of all, remove the WCF Service reference in the project that consumes the WCF Service. The WCF-specific attributes such as “CallbackBehavior” and the like can be removed as well.
Add a new reference to the previously created class library project for the shared objects.
Next, you can create an empty implementation of the ServiceContract interface, which is now located in the class library project, in the client project.
Now change the “old” initialization of the WCF Service to the, as-yet empty, implementation of the ServiceContract.
Lastly, you have to change the usings for the previously used DataContract classes from the WCF Service to the new class library project. It should now be possible to compile the client project again. In order to be able to start the project again, you have to remove the <system.serviceModel> from the *.config.
Web API project
Create a “standard” ASP.NET Core Web API project for the new server, and add a reference to the new class library project. This is necessary to enable the server to use the same replacement classes (previously DataContract) as the client.
Now create a controller for each previous WCF ServiceContract in the Web API. For starters, the implementation can be adopted from the “old” WCF Service. Subsequently, the return values have to be changed to ActionResult and ActionResult<T>, respectively. The [Http..] verb attributes and a route for each method have to be specified. Furthermore, the [ProducesResponseType] attribute should be specified for each method; it describes the expected return value and will later be used for the generation of the client.
[ServiceContract(CallbackContract = typeof(IDataCallback))] public interface IDataInputService { [OperationContract] int CreateUser(User user); [OperationContract] int Login(User user); [OperationContract] List<Time> GetTimes(int userId); [OperationContract] void AddTime(Time time, int userId); [OperationContract] List<string> Projects(); }
Example of a WCF Service Contract to be migrated
[Route("api/TimeService")] [ApiController] public class TimeServiceController : ControllerBase { [HttpPost("CreateUser")] [ProducesResponseType(typeof(int), 200)] public ActionResult<int> CreateUser(User user) { } [HttpPost("Login")] [ProducesResponseType(typeof(int), 200)] public ActionResult<int> Login(User user) { } [HttpGet("Times")] [ProducesResponseType(typeof(List<Time>), 200)] public ActionResult<List<Time>> GetTimes(int userId) { } [HttpPost("AddTime")] [ProducesResponseType(200)] public ActionResult AddTime(Time time, int userId) { } [HttpGet("Projects")] [ProducesResponseType(typeof(List<string>), 200)] public ActionResult<List<string>> Projects() { } }
Example of the created Web API controller
Note: The Web API controller created this way will probably not correspond to a resource-oriented REST API. The API is more action-based and reflects the “old” Service—which is no problem for the transition for now.
Client generation
To avoid writing a lot of code for calling and using the Web API, you can generate it. The OpenAPI specification (formerly Swagger) can be used for this purpose.
There are several ways to generate the OpenAPI specification and, based thereon, the client code. One of these options is described below as an example.
In order for the OpenAPI specification to be generated automatically, you must first integrate the “NSwag.AspNetCor” NuGet package and configure it in the Web API project according to the instructions given in Getting started with NSwag. After that, you can already test the API interface in the browser by calling up the /swagger/ URL once the Web API project has been started.
The client code for access to the new Web API can be generated with NSwagStudio. In the settings of the generator, make sure that the namespace for the generation of the client is correct. Additional settings may be required for specific projects until the desired result is generated. The client created by the generator and the settings made in the generator (*.nswag file) should be saved in the project.
Use of the client
When the generator generates the desired result, you only need to make one more change to integrate the new client. The previously created, as-yet empty dummy implementation of the ServiceContract only has to inherit from the newly generated client, and the empty implementation has to be removed. It is important to ensure that the newly created client fulfills the ServiceContract interface.
The migration from WCF to Web API is now complete and should be tested.
Bidirectional communication
If bidirectional communication was used in the WCF Service, it must now be realized by means of SignalR.
For this purpose, create a hub class that inherits from Microsoft.AspNetCore.SignalR.Hub for each WCF CallbackContract in the Web API project. In the Startup.cs of the Web API project, add the line “services.AddSignalR();” in the ConfigureServices method. In the Configure method, a mapping is provided in the definition of UseEndpoints for each hub “endpoints.MapHub<EarningsHub>(“/Earnings”);”.
public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSignalR(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseEndpoints(endpoints => { endpoints.MapHub<EarningsHub>("/Earnings"); }); } } public class EarningsHub : Microsoft.AspNetCore.SignalR.Hub { }
Changes to the Web API startup class and definition of the SignalR Hub
By injecting IHubContext<EarningsHub>, you can trigger the transmission of data to all or specific clients with just one line.
[Route("api/TimeService")] [ApiController] public class TimeServiceController : ControllerBase { private readonly IHubContext<EarningsHub> earningsHubContext; public TimeServiceController(IHubContext<EarningsHub> earningsHubContext) { this.earningsHubContext = earningsHubContext; } [HttpPost("AddTime")] [ProducesResponseType(200)] public ActionResult AddTime(Time time, int userId) { Task.Run(async () => await earningsHubContext.Clients.All.SendAsync("EarningsCalculated", result)).GetAwaiter().GetResult(); } }
Use of the SignalR hub in the API controller
Subsequently, add the “Microsoft.AspNetCore.SignalR.CIient” NuGet package to the consuming project. As you can see in the example below, the method previously called by the server does not have to be changed at all. In WCF, the method was triggered by means of the callback interface and its allocation upon initialization of the client with “new InstanceContext(this)”. With the SignalR implementation, a connection to the hub is established, and the triggering server event is bound to the existing method.
[CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Single, UseSynchronizationContext = false)] public partial class TimeTracking : Page, IDataInputServiceCallback { private DataInputServiceClient client; public TimeTracking() { client = new DataInputServiceClient(new InstanceContext(this)); } public void EarningsCalculated(Dictionary<int, decimal> earnings) { // Client Methode welche vom Server aufgerufen wird } } [ServiceContract(CallbackContract = typeof(IDataCallback))] public interface IDataInputService { } public interface IDataCallback { [OperationContract(IsOneWay = true)] void EarningsCalculated(IDictionary<int, decimal> earnings); }
Client example using the WCF CallbackContract
public partial class TimeTracking : Page { private HubConnection connection; public TimeTracking() { connection = new HubConnectionBuilder().WithUrl("http://localhost:52841/Earnings").Build(); connection.On<Dictionary<int, decimal>>("EarningsCalculated", EarningsCalculated); connection.StartAsync(); } public void EarningsCalculated(Dictionary<int, decimal> earnings) { // Client Methode welche vom Server aufgerufen wird } }
Client example after conversion to SignalR
The use of SignalR described here as an example is very simple. For a productive deployment, you should ensure a robust implementation with automatic reconnect etc., see also: https://docs.microsoft.com/de-de/aspnet/core/signalr/dotnet-client?view=aspnetcore-3.1&tabs=visual-studio
Cross-cutting concerns such as authentication, authorization, logging and error handling of the Web API calls have not been considered in the migration. These issues should be checked and adjusted as well in each individual case.
Conclusion
The conversion from WCF to ASP.NET Core Web API is possible and relatively easily manageable. The implementation of the WCF Services can be directly adopted for the new Web API controllers with minor adjustments to the return values and attributes. Using the OpenAPI specification allows you to generate a client for access to the Web API that supports the previous WCF ServiceContract interface. This way, only a few usings and the initialization of the client have to be adjusted in the consuming application.