OdeToCode IC Logo

Moving APIs to .NET Core

Monday, August 13, 2018

I’ve been porting a service from .NET to .NET Core. Part of the work is re-writing the Azure Service Bus code for .NET Core. The original Service Bus API lives in the NuGet package WindowsAzure.ServiceBus, but that package needs the full .NET framework.

The newer .NET Standard package is Microsoft.Azure.ServiceBus.

The idea of this post is to look at the changes in the APIs with a critical eye. There are a few things we can learn about the new world of .NET Core.

Statics Are Frowned Upon

The old way to construct a QueueClient was to use a static method on the QueueClient class itself.

var connectionString = "Endpoint://..";
var client = QueueClient.CreateFromConnectionString(connectionString);

The new style uses new with a constructor.

var connectionString = "Endpoint://..";
var client = new QueueClient(connectionString, "[QueueName]");

ASPNET core, with its service provider and dependency injection built-in, avoids APIs that use static types and static members. There is no more HttpContext.Current, for example.

Avoiding statics is good, but I’ll make an exception for using static methods instead of constructors in some situations.

When a type like QueueClient has several overloads for the constructor, each for a different purpose, the overloads become disjointed and confusing. Constructors are nameless methods, while static factory methods provide a name and a context for how an object comes to life. In other words, QueueClient.CreateFromConnectionString is easier to read and easier to find compared to examining parameters in the various overloads for the QueueClient constructor.

The New World is async Only

The old API offered both synchronous and asynchronous operations for sending and receiving messages. The new API is async only, which is perfectly acceptable in today's world where even the Main method can be async.

Binary Serialization is Still Tricky

The old queue client offered a BrokeredMessage type to encapsulate messages on the bus.

var message = new BrokeredMessage("Hello!");

Behind the scenes, BrokeredMessage would use a DataContractBinarySerializer to convert the payload into bytes. Originally there were no plans to offer any type of binary serialization in .NET Core. While binary serialization can offer benefits for type fidelity and performance, binary serialization also comes with compatibility headaches and attack vectors.

Although binary serializers did become available with .NET Core 2.0, you won’t find a BrokeredMessage in the new API. Instead, you must take serialization into your own hands and supply a Message object with an array of bytes. From "Messages, payloads, and serialization":

While this hidden serialization magic is convenient, applications should take explicit control of object serialization and turn their object graphs into streams before including them into a message, and do the reverse on the receiver side. This yields interoperable results.

Interoperability is good, and the API change certainly pushes developers into the pit of success, which is also a general theme for .NET Core APIs.

Gravatar Todd Hilehoffer Monday, August 13, 2018
I have found the main difference in programming asp.net core seems to be async and dependency injection everywhere, just as you said...
Gravatar Anders Baumann Tuesday, August 14, 2018
Hi Scott. May I ask why you need to move to .Net Core? Is it just for the exercise? I ask because I have yet to see the actual business need or business advantage of .Net core if your application is already inside Azure and you have no intention of moving to another platform. Thanks in advance, Anders Baumann
Gravatar scott Tuesday, August 14, 2018
If not a fan of porting for the sake of porting. I still have web forms apps that started in 2002 in production, and that won't change! Three appealing features for this specific scenario are self-contained deployments, cross-platform deployments, and performance.
Gravatar Howard Richards Tuesday, August 14, 2018
Totally agree that static factory methods are better than too many constructors. Only just today I refactored a class to make the constructor private and replace it with some static methods that set the correct values. My code had incorrectly called a constructor with the wrong enum type, which wasn't picked up by the type checker, causing a bug.
Gravatar Anders Baumann Thursday, August 16, 2018
> "Three appealing features for this specific scenario are self-contained deployments, cross-platform deployments, and performance.". - Cross-platform deployment. Why is that interesting? You are working inside Azure. Isn't that a clear case of YAGNI ? - Performance: In my experience the performance problems are never in the C# code. 99.99% of the times the problem is in the database. And if the issue is in fact in the C# code then it is usually a matter of a poorly chosen algorithm. Not because of the framework as such. - Self-contained deployments. Where do you see the problem with the way a Web app is deploy with .Net 4.7 in Azure? Thanks, Anders
Thursday, August 16, 2018
Anders: Those would be valid points if the code ran in a controlled Azure environment, but the code runs outside of Azure in customer provided (and managed) environments.
Gravatar Marc Roussy Thursday, August 16, 2018
Since there's no brokered message in the .NET Core library, wouldn't that make inter-operability between a .NET Core sender and a .NET Framework consumer much more difficult?
Gravatar scott Thursday, August 16, 2018
Yes, that is a problem. You can see a number of issues related to that type of interop here: https://github.com/Azure/azure-service-bus-dotnet/search?q=brokeredmessage&type=Issues
Gravatar Sean Feldman Friday, August 17, 2018
Good post Scott. The title is slightly misleading, considering the post is entirely focusing on Azure Service Bus client for .NET Standard. A lot of changes your see in the client were not just "porting to .NET Standard", but also concious architectural decisions made by the team. Connection management - I'd counter your position with my excitment about static methods removal. There was way too much confusion about throughput, clients, and connections. All stemmed from the fact that MessagingFactory was hiding this information. By removing it and its static factory methods, developers are now in charge of making the right decision for their use cases, rather than using a default option hidden behind MF. Message body serialization - that's where the ball was dropped IMO. With some extra work interoperability can be achieved, but it's not trivial. Some slack for those that have used streams all along. Those implementation won't be affected by interop issue. So did happen with management operations. Gladly, it was recently addressed.
Your Comment