A while back I was porting a .NET Framework web service to .NET Core. This particular service has one API it exposes with two calls. Not a whole lot of logic, but what this little service does is make a whole bunch of calls to other services. The main one does a bunch of parallel calls, then some database calls, and finally tops it all off with a really long heavy call combining everything into some price and availability figures to show the user waiting patiently for the page to render. Why was I looking at this? A performance problem. Sometimes the call takes 1 second, sometimes it takes 20 seconds. So while I was porting the process I was also adding telemetry so we could visualize easily and quickly where all the time was going. This is how I ended up involved in OpenTelemetry, but more on that later.
My process was simple, port the calls one-by-one. Have you used IHttpClientFactory? It’s great for a bunch of reasons, but what I really like about it is the separation of concerns. My service class is fed an HttpClient instance and it doesn’t have to worry about where it came from, how it was configured, it’s retry rules, cleaning it up, or anything (practically speaking). I built all of the Http calls using that pattern.
But it’s never that easy, is it?
What I ran into was one of the calls in the middle of the process was going to an older accounts receivable system using WCF for its communication. Panic! Is WCF even available in .NET Core? The good news is that yes, it is available (at least the client bits) @ dotnet/wcf. The bad news is, it’s the same old WCF we had before. The same boring API surface (more or less).
So I went from writing code like this…
public class MyService { private readonly HttpClient _Client; public MyService(HttpClient client) { _Client = client; } public async Task ServiceOperation(object serviceRequest) { using StringContent content = new StringContent(JsonConvert.SerializeObject(serviceRequest)); using HttpResponseMessage response = await _Client.PostAsync("/api/", content).ConfigureAwait(false); response.EnsureSuccessStatusCode(); } }
Which is very elegant. To writing this…
public class MyWcfService : IDisposable { private readonly ChannelFactory<IProxy> _Factory; public MyService(IOptions<ServiceOptions> options) { Binding binding = new BasicHttpBinding( options.Value.ServiceBaseUri.Scheme == "https" ? BasicHttpSecurityMode.Transport : BasicHttpSecurityMode.None); _Factory = new ChannelFactory<IProxy>(binding, new EndpointAddress(options.Value.ServiceBaseUri)); } ~MyWcfService() { Dispose(false); } public async Task ServiceOperation(object serviceRequest) { IProxy proxy = _Factory.CreateChannel(); try { await proxy.SendDataAsync(serviceRequest).ConfigureAwait(false); } finally { IClientChannel channel = (IClientChannel)proxy; if (channel.State == CommunicationState.Faulted) channel.Abort(); else channel.Close(); } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool isDisposing) { _Factory.Close(); } }
Do you guys see what I’m seeing? It’s way more ceremony. And now the service class needs to worry about settings and bindings and cleanup and abstract jazz fusion ensembles and the joys of life that come from living above a nightclub.
If you’re crazy like me while writing this code you thought… why can’t we do this WCF client just like its Http cousin?
TL;DR: I did just that via SoapClientFactory which is available in Macross.ServiceModel.Extensions.
This is where the “slick” WCF on .NET Core comes in. “Slick” here means, a pattern that fits into the .NET Core style. Encapsulation, kiddoes. Using that lib you can take a ServiceContract
like this:
[ServiceContract] public interface ILegacyProductProxy { [OperationContract] Task<int> GetStatusAsync(); }
Bootstrap it like this:
public void ConfigureServices(IServiceCollection services) { services.AddSoapClient<ILegacyProductProxy, ProductService>(() => new ChannelFactory<ILegacyProductProxy>( new BasicHttpBinding(), new EndpointAddress("http://localhost/LegacyService/"))); }
And then have your WCF channel injected into your services via SoapClient<T>
type like this:
public class ProductService : ILegacyProductProxy { private readonly SoapClient<ILegacyProductProxy> _SoapClient; public ProductService(SoapClient<ILegacyProductProxy> soapClient) { _SoapClient = soapClient; } public Task<int> GetStatusAsync() => _SoapClient.Channel.GetStatusAsync(); }
The developer no longer has to worry about all the extensive WCF configuration or the non-standard cleanup. No more bugs because someone tried to put a using
around their client without realizing WCF throws an error when you dispose a faulted client.
I can’t really take credit for it, what I did was just steal borrow all the patterns IHttpClientFactory
employs and applied them to the WCF API. ProductService
is transient as is its SoapClient
. Each time you get one, you get a new instance. And when you are done with it, the container cleans it up for you. The ChannelFactory
is left open to manage connections.
Pretty slick right?
Part 2: Getting throttled
Now comes the fun part!
I ran this up the flagpole over at dotnet/wcf, to see what the experts thought, and I basically got destroyed: https://github.com/dotnet/wcf/issues/4139
Ouch! This is really why I wanted to do this post. The feedback is so painful, it’s funny. You get kind of used to it when you’re doing open source contributions. Most of my issues and pulls to dotnet end up this way. That’s the fun part of committing to these open source projects. They can’t all be zingers, right? You are going to have your code and ideas picked apart in front of the whole world. You have to be kind of crazy to even try. It can be rewarding but also frustrating and exhausting. It is often a painstakingly slow process. From what I’ve done so far, bug fixes are your best bet to get picked up. New APIs are a mixed bag. Sometimes they take your idea and someone else builds it. That’s not too bad. Sometimes they dismiss you. Sometimes they are unable to change things due to the risk of breaking stuff. Sometimes you will get outright ignored. I have things sitting out there I don’t know what’s going on with them, they’re just in limbo.
So the question is, why do it at all? Well, every once in a while they take your stuff, which is a really feel-good awesome moment. But just trying, you will become a better developer. First, you’ll read some really great and well-written, testable work. Just doing that, there is much to learn and absorb. Then you’ll get feedback, which will improve your overall work. The more you do it, the better you’ll get at it.
Everyone thinks they want to be a library developer. But to really do it, I respect these guys a lot. When you’re writing code for a payment product or a marketing site, or whatever, you have a pretty good idea of your users and the risk associated with your changes. Imagine working at the scale of dotnet/runtime. Your work is going to be used and abused in all kinds of unimaginable ways. It working isn’t often the hard part. Is your API designed well-enough to last forever? It’s not an easy task.
So what of Matt’s feedback on my “slick” SoapClient
? Should you use it in your projects if it’s so bad?
That was three months ago. I’ve had a lot of time to consider and internalize that feedback. I’ve thought a lot about it, and how to incorporate everything into the code. And I’ve decided after all that consideration that… I’m going to ignore most of it.
He said the build-up pattern and dependency injection is off. Well the primary goal of the project was to model the build-up pattern and dependency injection of IHttpClientFactory
so changing it would fundamentally change the project.
He said there are issues with the lifetime of the clients. This is a tough one! Big issue. But I’m not seeing it. I’ve combed over the dotnet/wcf source for issues. I think maybe Matt missed that the registrations are transient? Each time something in the code asks for a class registered to receive a SoapClient both will be created fresh. When they go out of scope, they will be disposed of by the container (closed or aborted). A SoapClient
isn’t going to be fed into multiple classes, it isn’t going to be reused. This is how IHttpClientFactory
works. If a class needs to call Open
on its SoapClient
, it is free to do so. If we need to explicitly call Open
on the factory, we can do that in the bootstrap.
He brought up using an “adapater” pattern, I tried to make that work but couldn’t. HttpClient
and SoapClient
are a bit different in that SoapClient
needs to know the channel type. No way to get around having to pass that in. There are three types needed: 1) The service type that will be requested (IService), 2) The implementation type (Service), and 3) The channel type (IProxy). 1 & 2 are what IHttpClientFactory
needs. SoapClient
has the third. You could force the requesting type to be the same as the proxy type, and there is an overload for that, but doing so forces everyone’s service to also expose the proxy interface, which a lot of people won’t want.
I didn’t ignore everything! He brought up issues with client credentials being changed during the build-up, I put in a check for that.
At the end of the day, I’m using SoapClient
to call basicHttp
and wsHttp
bindings and it’s working great! Feel free to take it for a spin and if you run into issues please let me know.
Thank you Matt for the time you spent looking at it, your feedback was absolutely helpful and I’m sure there are many ways to improve on things further.