WCF-Alternativen (Teil 2) – Eine Anleitung zur Migration von WCF zu Web API

In diesem Blogpost der Artikelreihe zu den Alternativen der Windows Communication Foundation (WCF) werden die Besonderheiten und Herausforderungen einer WCF-Migration als Vorbereitung zu einer späteren Portierung der Anwendung auf .NET Core beschrieben.

In diesem Beitrag wird dabei zunächst auf ASP.NET Core Web-API als eine mögliche Alternative eingegangen und Schritt für Schritt beschrieben, wie eine mögliche Migration von WCF zu ASP.NET Core Web API aussehen kann. Wie dies dagegen bei der Migration zu gRPC funktioniert, lest ihr im nächsten Blogartikel.

Vorgehen bei der Migration

In der Regel existiert ein separates WCF-Projekt in der Solution. Da eine direkte Umstellung nicht möglich ist, kann dieses Projekt zunächst unverändert in der Solution verbleiben.

Zunächst sollte ein neues Class-Library-Projekt für Shared-Objekte zwischen Server und Client angelegt werden. In dieses Projekt werden die ServiceContract Interfaces sowie die DataContract-Klassen aus dem WCF-Projekt kopiert und die WCF-spezifischen Attribute wie „ServiceContract“, „OperationContract“, „DataContract“, „DataMember“ usw. entfernt.

Client Projekt

Im WCF Service konsumierenden Projekt wird als erstes die WCF-Service-Referenz entfernt. Auch können die WCF-spezifischen Attribute wie „CallbackBehavior“ o. Ä. entfernt werden.

Eine neue Referenz zum zuvor angelegten Class-Library-Projekt für die Shared-Objekte wird hinzugefügt. Als nächstes kann im Client-Projekt eine leere Implementierung des jetzt im Class-Library-Projekt abgelegten ServiceContract Interfaces erstellt werden. Die „alte“ Initialisierung des WCF Service wird jetzt auf die noch leere Implementierung des ServiceContract geändert.

Abschließend müssen noch die Usings für die zuvor verwendeten DataContract-Klassen aus dem WCF Service auf das neue Class-Library-Projekt geändert werden. Damit sollte sich das Client-Projekt wieder kompilieren lassen. Um das Projekt auch wieder starten zu können, ist der <system.serviceModel> aus der *.config zu entfernen.

Web-API-Projekt

Für den neuen Server wird ein „Standard“ ASP.NET Core Web-API-Projekt angelegt und eine Referenz auf das neue Class-Library-Projekt hinzugefügt. Dies ist nötig, damit der Server dieselben Austauschklassen (vorher DataContract) wie der Client verwenden kann.

Für jeden bisherigen WCF ServiceContract wird nun in der Web-API ein Controller angelegt. Die Implementierung kann zum Einstieg aus dem „alten“ WCF Service übernommen werden. Danach müssen die Rückgabewerte auf ActionResult bzw. ActionResult<T> geändert werden. Es müssen die [Http..] Verb Attribute sowie eine Route für jede Methode angegeben werden. Auch sollte das [ProducesResponseType]-Attribut bei jeder Methode angegeben werden – dieses beschreibt den zu erwartenden Rückgabewert und wird später für die Generierung des Clients verwendet.

[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();
}

Beispiel eines zu migrierenden WCF Service Contract


[Route("api/zeitservice")]
[ApiController]
public class ZeitServiceController : 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()
    {
        
    }
}

Beispiel des erstellten Web-API-Controllers


Hinweis: Der auf diese Weise erstellte Web API Controller entspricht vermutlich nicht einer ressourcenorientierten REST API. Die API ist eher actionbasiert und spiegelt den „alten“ Service wider – was für die Umstellung aber zunächst in Ordnung ist.

Client-Generierung

Um für den Aufruf und die Verwendung der Web API möglichst wenig Code schreiben zu müssen, kann dieser generiert werden. Hierzu kann die OpenAPI-Spezifikation (vormals Swagger) verwendet werden.

Um die OpenAPI-Spezifikation und darauf basierend den Client Code zu generieren, gibt es verschiedene Möglichkeiten. Nachfolgend wird eine der Optionen beispielhaft beschrieben.

Damit die OpenAPI-Spezifikation automatisch generiert werden kann, wird zunächst das NuGet-Paket „NSwag.AspNetCore“ eingebunden und nach der Anleitung Getting started with NSwag im Web-API-Projekt konfiguriert. Danach kann durch den Aufruf der URL /swagger/ bei gestartetem Web-API-Projekt die API-Schnittstelle bereits im Browser getestet werden.

Übersicht Swagger
Abbildung 1: Übersicht Swagger
Detailansicht Swagger
Abbildung 2: Detailansicht Swagger

Der Client Code für den Zugriff auf die neue Web-API kann mit dem NSwagStudio generiert werden. In den Einstellungen des Generators sollte dabei auf die korrekte Angabe des Namespace für die Generierung des Clients geachtet werden. Eventuell sind projektspezifisch weitere Einstellungen nötig, bis das gewünschte Ergebnis generiert wird. Der vom Generator erstellte Client sowie die im Generator vorgenommen Einstellungen (*.nswag-Datei) sollten im Projekt abgelegt werden.

NSwag-Studio-Generator
Abbildung 3: NSwag-Studio-Generator

Client-Verwendung

Wird vom Generator das gewünschte Ergebnis generiert, ist zur Einbindung des neuen Clients nur noch eine Änderung nötig. Es muss lediglich die zuvor erstellte, noch leere Dummy-Implementierung des ServiceContract vom neu generierten Client erben und die leere Implementierung entfernt werden. Dabei muss jedoch sichergestellt werden, dass der neu generierte Client das ServiceContract Interface erfüllt.

Damit ist die Migration von WCF zu Web API bereits abgeschlossen und sollte nun getestet werden.

Bidirektionale Kommunikation

Wurde im WCF Service bidirektionale Kommunikation genutzt, muss diese jetzt über die Verwendung von SignalR realisiert werden.

Hierzu wird im Web-API-Projekt für jeden WCF CallbackContract eine Hub-Klasse angelegt, welche von Microsoft.AspNetCore.SignalR.Hub erbt. In der Startup.cs des Web-API-Projekts muss in der Methode ConfigureServices die Zeile „services.AddSignalR();“ hinzugefügt werden. Bei der Methode Configure wird bei der Definition von UseEndpoints je Hub ein Mapping hinterlegt „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
{
	
}

Änderungen an der Web-API-Startup Klasse und Definition des SignalR Hub


Durch Injection von IHubContext<EarningsHub> kann mit nur einer Zeile das Senden von Daten an alle oder nur spezifische Clients ausgelöst werden.

[Route("api/zeitservice")]
[ApiController]
public class ZeitServiceController : ControllerBase
{
    private readonly IHubContext<EarningsHub> earningsHubContext;
    public ZeitServiceController(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();
    }
}

Verwendung des SignalR Hub im API-Controller


Anschließend wird im konsumierenden Projekt zunächst das NuGet-Paket „Microsoft.AspNetCore.SignalR.Client“ hinzugefügt. Wie im nachfolgenden Beispiel zu sehen ist, muss die bisher vom Server aufgerufene Methode gar nicht geändert werden. Bei WCF wurde die Methode durch Verwendung des Callback Interface und deren Zuweisung bei der Initialisierung des Clients durch „new InstanceContext(this)“ ausgelöst. Bei der SignalR Implementierung wird eine Verbindung zum Hub aufgebaut und das auslösende Server Event an die bestehende Methode gebunden.

[CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Single, UseSynchronizationContext = false)]
public partial class Zeiterfassung : Page, IDataInputServiceCallback
{
    private DataInputServiceClient client;
    
    public Zeiterfassung()
    { 
        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 Beispiel Verwendung des WCF CallbackContract


public partial class Zeiterfassung : Page
{
    private HubConnection connection;
    
    public Zeiterfassung()
    {    
        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 Beispiel nach Umstellung auf SignalR


Die hier beispielhafte Verwendung von SignalR ist sehr einfach gehalten. Für einen produktiven Einsatz sollte eine robuste Implementierung mit automatischem Reconnect usw. sichergestellt werden – siehe dazu auch: https://docs.microsoft.com/de-de/aspnet/core/signalr/dotnet-client?view=aspnetcore-3.1&tabs=visual-studio

Nicht betrachtet wurden bei der Migration Querschnittsfunktionen wie Authentifizierung, Autorisierung, Logging und Fehlerbehandlung der Web-API-Aufrufe – diese Punkte sollten im Einzelfall geprüft und ebenfalls angepasst werden.

Fazit

Die Umstellung von WCF auf ASP.NET Core Web API ist mit überschaubarem Aufwand möglich. Die Implementierung der WCF Services kann mit kleinen Anpassungen an Rückgabewerten und Attributen direkt in die neuen Web API Controller übernommen werden. Durch die Verwendung der OpenAPI-Spezifikation kann ein Client für den Zugriff auf die Web API generiert werden, welcher das bisherige WCF ServiceContract Interface unterstützt. So müssen in der konsumierenden Anwendung lediglich ein paar Usings und die Initialisierung des Clients angepasst werden.

Dieser Beitrag wurde verfasst von: