WCF from scratch in 2019
I work with a (big!) layer of WCF services and I want to know more about them and how we can adapt for the future. So I’m doing a deep dive! If I switch tenses in this post, apologies. It’s part stream-of-consciousness and part review.
The big questions I hope to answer:
- Can we push multiple WCF services from one WCF Project to separate Azure App Services? (No, we should use one-project-per-service)
- Can we add JSON/web endpoints to an existing SOAP service with just config? (Yes! With some config and code-level attributes)
- Can we add Swagger to our JSON endpoints? (Yes!)
You can jump down to the pictures
Getting started
Steps:
- Made a solution called
TacoServices
with a WCF Service Application project calledTacoService
, and renamed the originalIService1
andService1
files toILocation
etc. using the Solution Explorer so that both the code elements and the files would be renamed. But I still had to rename the code element forService1
. - Added a .Net Standard class library called
TacoServices.Common
- Wrote up my basic service to return some stub data
Service Project Web.config
To set up my config, I more or less followed this dotnetcurry guide to expose WCF services as SOAP and REST. Please note that REST is a misued term here; REST is not just JSON. Every dev has a different level of passion for the purity of the definition of REST. My preferred starting point is the Richardson Maturity Model, so I’ll be calling what we do here a ‘JSON API’.
Sample of config file:
<system.serviceModel>
<services>
<service name="Taco.Services.Location.LocationService" behaviorConfiguration="generic">
<!-- SOAP - No behaviorConfiguration, uses basicHttpBinding -->
<endpoint address="soap" binding="basicHttpBinding" contract="Taco.Services.Location.ILocationService" />
<!-- JSON - Needs a behaviorConfiguration with webHttp, uses webHttpBinding -->
<endpoint address="api" binding="webHttpBinding" contract="Taco.Services.Location.ILocationService" behaviorConfiguration="web" />
</service>
</services>
<behaviors>
<endpointBehaviors>
<behavior name="web">
<webHttp helpEnabled="true" />
</behavior>
</endpointBehaviors>
<serviceBehaviors>
<behavior name="generic">
<serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
</behavior>
<behavior name="">
<serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
</behavior>
</serviceBehaviors>
</behaviors>
<protocolMapping>
<add binding="basicHttpsBinding" scheme="https" />
</protocolMapping>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
</system.serviceModel>
Things to grok:
- In
<behaviours>
we have service-level and endpoint-level behaviours - The service (and all its endpoints) get the
generic
behavior we created - Only the JSON/web/RESTish endpoint gets the
web
endpoint behavior. The SOAP endpoint doesn’t need one - Endpoints must have different addresses and these will be appended after the SVC like
localhost:60601/LocationService.svc/soap/
which is kind of ugly. You can fix it with an IIS rewrite rule or with Azure API Management in production
If you run a WCF project while the .svc.cs
file is open in VS, it opens the WCF Test Client which will validate your service.
Running the test client, I followed the trail of errors:
- You can’t have two endpoints with the same name (i.e. you can’t have overloads in a
ServiceContract
) so I changedGetLocations(string searchString)
toSearchLocations(...)
- You have to mark a collection type with
[CollectionDataContract]
instead of just[DataContract]
WCF Test Client said “Added Service Successfully” but didn’t actually add my thing, this was because my services config was wrong (earlier version to the one above) and I’d configured all my endpoints to use the webHttp
behaviour (this takes away their SOAP/WSDL powers).
I made a new Console App to consume the services, added the Service Reference (using ‘Discover Services in Solution’), and implemented the basic features of my LocationService
.
SOAP works - making a second service
Everything in my LocationService
now works, so I want to make a second service.
A bit tricky to find the new ‘WCF Service’ option when you go to ‘Add New Item’ on a project. It’s in the Web group, near the bottom, but it doesn’t appear in any of the subcategories of Web (General, Markup, Scripts, etc.)
I created an OrderService
and stubbed out some behaviours.
One project - multiple App Service targets?
Nope. This isn’t the way to go. It makes a lot more sense to divide the services one-per-project and then have a PublishProfile for each project in the usual way.
I split the OrderService
into a separate project which wasn’t too hard with some cunning renaming.
The test app then couldn’t reach the Orders API any more. Following the errors again:
- I had to drop and re-add the Service Reference in the consuming project
- Had to add the new Orders Svc to the list of multi-startup projects
- Had to add the Newtonsoft.Json package to the new project because it was required by a dependency (my Common objects project)
Then it was fixed, and the Orders SOAP service started working again.
Test JSON
LocationService
Firstly, I discovered that the JSON endpoint has a default help page at: http://localhost:61142/LocationService.svc/api/help
…bear in mind that I used ‘/api’ as the address prefix of the JSON Service Endpoint in my web.config
Through the help page, I discovered that my JSON endpoints only accept POST right now. So I added the attribute [WebGet(ResponseFormat = WebMessageFormat.Json)]
into the ILocationService
contract.
Now they accepted GET, the two endpoints in LocationService just worked! I used JsonConvert.DeserializeObject
to get the result and it was exactly what I was expecting - surprising!
OrderService
The POST on OrderService did not work right away even though I was sending an object which matched the defintion on the help page at “http://localhost:18070/OrderService.svc/api/help/operations/PlaceOrder”
The fix was twofold:
Add a [WebInvoke...]
attribute as described in this WCF post by Dean Poulin
[OperationContract]
[WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
Result PlaceOrder(OrderRequest order);
Set the Content-Type header in the request (i.e. in the code of the consuming client) to ‘application/json’
client.Headers.Add(HttpRequestHeader.ContentType, "application/json");
Now everything works as well in JSON as it does via SOAP! Pretty cool!
Adding SwaggerWcf
I added the NuGet package with Install-Package SwaggerWcf
and had to do that on both services projects and on the common project.
My service projects didn’t have global.asax
files but the SwaggerWcf readme setup guide said I needed one. You can add a global.asax to your project in the Add New Item dialog by searching for ‘global’ and selecting ‘Global Application Class’. Or you can find it under Web/General.
It was tricky to configure SwaggerWcf. The docs are not great. It will work and come up at your configured URL as soon as you’ve sorted out the routing (below) but won’t have any valuable information until you’ve annotated everything with the attributes described in the readme.
Configure with RouteTable
Even though my project is just straight WCF (not really “ASP.NET”) eventually it was the ASP.NET way of configuring that won out. If you go with the self host, you need an absolute URL in the web.config which doesn’t work for me because I’m using IIS Express with its whimsically-numbered ports.
//Global.asax.cs
protected void Application_Start(object sender, EventArgs e)
{
RouteTable.Routes.Add(new ServiceRoute("docs", new WebServiceHostFactory(), typeof(SwaggerWcfEndpoint)));
}
Doing it this way, you do not need a <service>
entry for Swagger in web.config
. I found this pull request on a sample project helpful in fixing my config issues.
You might have to manually add a framework reference to System.ServiceModel.Activation
for the code above to work. Sadly, Intellisense doesn’t suggest the fix automatically (VS 2017).
Practical notes
If you run your project(s) while looking at the svc file and the WCF Test Client opens up, you have to wait for it to load before you can successfully get to the Swagger page.
In OrderService.svc.cs
, my attribute ended up looking like this:
[SwaggerWcf("/OrderService.svc/api")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class OrderService : IOrderService
…in order for the Swagger sample requests to work.
The sample project has you put loads of SwaggerWcfResponse
annotations on, and maybe your API is not that clever. You don’t need all of those. On the other hand, I was able to enhance my API by adding a tiny bit of extra code right before I return the response from OrderService.PlaceOrder
:
WebOperationContext.Current.OutgoingResponse.StatusCode = result.Success
? HttpStatusCode.Created
: HttpStatusCode.BadRequest;
I then went ahead and duplicated these changes on the LocationService. I thought I’d nailed it, went to test the ‘/docs’ page, and got:
Request Error
The server encountered an error processing the request. See server logs for more details.
It was because I forogt the swaggerwcf
section in the web.config for the LocationService project:
<configSections>
<section name="swaggerwcf" type="SwaggerWcf.Configuration.SwaggerWcfSection, SwaggerWcf" />
</configSections>
...
<swaggerwcf>
<settings>
<setting name="InfoTitle" value="LocationService" />
<setting name="InfoDescription" value="Interface for finding the locations which serve Tacos" />
<setting name="InfoVersion" value="0.0.1" />
<setting name="InfoContactUrl" value="http://github.com/stegriff" />
<setting name="InfoContactEmail" value="github@stegriff.co.uk" />
</settings>
</swaggerwcf>
Ok, weirdly, the swagger defs “bleed” into each other. The first service to startup knows only about itself, but the second picks up the details of both APIs. I’m still not sure why. I thought it was because one of my service projects held a reference to the other, and the common project had definitions affecting one project and not the other but was being pulled into both. But even after I changed those facts, still the LocationService has a Swagger def for both APIs, and the OrderService has only itself. Mysterious.
Outcomes - what did we get?
Firstly, you can find all the source code for TacoServices on GitHub.
This is what I get when I run the solution (the consumer project is in JSON mode). Directory listing is switched on, and you can click into a SVC file to see the service discovery screen.
The Swagger defs work offline, but to prove a point I set up an Azure App Service for each service, downloaded the PublishProfiles and published each project to the cloud, at taco-order
and taco-location
; here is one of the online Swagger defs:
(If it’s still online - unlikely - you can reach it at http://taco-order.azurewebsites.net/docs/)
Conclusion
WCF is a still a workable technology, and parallel SOAP with JSON is possible and practical. The URLs are ugly but this can be fixed with IIS Rewrites or with Azure API Management, which I’ll explore in the next post. Swagger is easy to integrate with the SwaggerWcf library, but there are some snags.
Overall, for me, it was a cool experience and a helpful deep-dive of the technology.
Sources
I’ll repeat my proviso that “REST is not JSON” and when these authors say REST they generally mean a plain-ol’ JSON service.
How to enable REST and SOAP both on the same WCF Service
https://debugmode.net/2011/12/22/how-to-enable-rest-and-soap-both-on-the-same-wcf-service
SwaggerWcf (NuGet Package)
https://www.nuget.org/packages/SwaggerWcf
5 simple steps to create your first RESTful service (WCF)
http://www.topwcftutorials.net/2013/09/simple-steps-for-restful-service.html
Expose WCF 4.0 Service as SOAP and REST
https://www.dotnetcurry.com/wcf/728/expose-wcf-service-soap-rest