If you’re familiar with SOAP web services, you likely know that they use Web Services Description Language (WSDL) to document what operations a service provide, what parameters they accept, what they return and so on. This information can be used to generate proxy classes, which you can use to communicate with remote systems simply by calling class methods and all implementation details are handled for you under the hood.
While SOAP-based services offer many configuration options (e.g. you can switch from TCP to HTTP just by changing a config) and great tooling, sometimes it’s beneficial to use a less sophisticated approach and expose and call services directly over HTTP (that’s why AX 7 added JSON-based endpoints for custom services). Almost everything can communicate over HTTP, it doesn’t require any extra libraries, it’s very flexible and so on. There are some patterns how to design such services (especially REST), but you can do whatever you want. But it means that you have to give up features like code generation of proxy classes. Or not?
While HTTP web services don’t have to use any descriptions similar to WSDL, it doesn’t mean they can’t. One of your options is Open API (also known as Swagger), which allows you to describe services in JSON or YAML. There are many tools for working with these descriptions, such as an editor, code generators for many languages, a generator of documentation, you can generate Open API descriptions for your WebAPI projects, there is a support for Open API in Azure Logic Apps, in Azure API Management and so on.
Let me show you an example of such a description for a custom service in AX 7 (Dynamics 365 for Finance and Operations, Enterprise Edition). You don’t have to examine it in detail, just notice that it describes available operations, what URL you must use to reach it, what parameters it expects, what it returns and so on, and it also contains additional textual annotations.
{ "swagger": "2.0", "info": { "title": "User management", "description": "Services for user management offered by Microsoft Dynamics 365 for Finance and Operations, Enterprise Edition.", "version": "1.0.0" }, "host": "YourAX7Instance.cloudax.dynamics.com", "basePath": "/api/services", "schemes": [ "https" ], "paths": { "/SysSecurityServices/SysUserManagement/getRolesForUser": { "post": { "summary": "Gets security roles", "description": "Returns security roles currently assigned to the given user in Microsoft Dynamics 365 for Finance and Operations, Enterprise Edition.", "operationId": "getRolesForUser", "produces": [ "application/json" ], "parameters": [ { "name": "body", "in": "body", "description": "User ID as defined in UserInfo table.", "required": true, "schema": { "$ref": "#/definitions/GetRolesRequest" } } ], "responses": { "200": { "description": "Roles assigned to user", "schema": { "type": "array", "items": { "type": "string" }, "example": [ "-SYSADMIN-", "SYSTEMUSER" ] } }, "500": { "description": "Processing inside Dynamics 365 for Finance and Operations failed." } } } } }, "definitions": { "GetRolesRequest": { "type": "object", "required": [ "axUserId" ], "properties": { "axUserId": { "type": "string", "example": "admin" } } } } }
The first thing we can do with it is to show the same information in a more readable way. Open the online editor and paste the sample code there. On the right side, you’ll see it rendered as a nice HTML document:
The editor can also use YAML (and will offer you to convert JSON to YAML), which is a more succinct format, therefore you may prefer it for manually edits. I intentionally used JSON, because we’ll need it in the next step.
Code generation
Now let’s generate code for calling the service, so we don’t have to do it all by hand and deal with all implementation details of communication over HTTP, with (de)serialization of values and so on.
Before you start, download and configure AX service samples from GitHub and verify that they work. Then create a new console application in ServiceSamples solution, where we’ll call a custom service through generated client classes. My code below assumes that the project is called JsonConsoleWithSwagger; you’ll have to adjust a few things if you use a different name.
Client classes can be generated by several different tools, therefore the following process is just an example; feel free to use other options in your projects.
Download and install NSwagStudio (if you use Chocolatey, as I do, all you need is cinst nswagstudio
). Run NSwagStudio, paste the JSON on Swagger Specification tab, tick CSharp Client and change a few properties at CSharp Client tab:
- Namespace: JsonConsoleWithSwagger (or something else, if you named the project in Visual Studio differently)
- Class Name: UserManagementClient
- Use and expose the base URL: No
- Class style: Poco
You could use different parameters, such as different Class style, but this will suffice in our case.
Then press Generate Outputs, which will generate corresponding C# code. Copy it to clipboard, create a new class in your Visual Studio project and replace the file content with the generated code.
We need to add just two things – the actual URL of your AX instance and an authentication header. We don’t want to change the generated code itself, because we may need to regenerate it later and we would lose our changes. Fortunately, it’s not needed – the class is partial, therefore we can create another part of the same class in a separate file and keep generated code and custom code cleanly separated.
Create a new file in your project and paste the following code there:
using AuthenticationUtility; using System.Text; using System.Net.Http; namespace JsonConsoleWithSwagger { public partial class UserManagementClient { partial void PrepareRequest(HttpClient client, HttpRequestMessage request, StringBuilder urlBuilder) { PrependAxInstanceUrl(urlBuilder); client.DefaultRequestHeaders.Add(OAuthHelper.OAuthHeader, OAuthHelper.GetAuthenticationHeader()); } private void PrependAxInstanceUrl(StringBuilder urlBuilder) { string service = urlBuilder.ToString(); urlBuilder.Clear(); urlBuilder.Append(ClientConfiguration.Default.UriString); urlBuilder.Append("api/services/"); urlBuilder.Append(service); } } }
PrependAxInstanceUrl() takes the address of your AX from configuration and puts it at the beginning of the request URL.
Then the code sets the authentication header with the help of OAuthHelper from AuthenticationUtility project, therefore we must add a reference to it:
The last step is adding code to Main() method of Program class to actually call the service. We create a request object (please provide an existing user ID there), create an instance of the UserManagementClient class generated from our Open API document and call the operation. It’s asynchronous, as recommended, but we don’t really need that here, therefore the code immediately asks for the result and waits for it. Then we iterate roles received from AX and puts them into console.
GetRolesRequest request = new GetRolesRequest() { AxUserId = "user7281" }; var roles = new UserManagementClient().GetRolesForUserAsync(request).Result; foreach (string role in roles) { Console.WriteLine(role); }
That was easy – we didn’t have to bother about what exact parameters the services expects (and we would get a compile error if we did it wrong), we didn’t have to serialize objects to JSON or anything like that. The generator was able to create all the code for us, it just needed to know how the service looks like.
In this case, I wrote the Open API document by hand, which obviously took some time. A much better approach would be generating it from metadata of custom services in AX, and while I don’t have such a solution in the moment, it’s definitely doable. Some information is already in place (e.g. getting the list of operation is easy), services and service groups already have properties for description (although they’re currently empty in most cases) and things like parameter description can be included in XML documentation. It still doesn’t cover everything, but additional information can be easily provided in attributes. It’s exactly what Swashbuckle does, e.g. with SwaggerResponseAttribute and RequiredAttribute.
I think it’s something that Microsoft should use for its custom services, to provide documentation and to make custom services much easier to consume. Open API / Swagger is a natural choice for this purpose, because Microsoft is a founding member of Open API Initiative and already support it in several products. Maybe Microsoft could somehow utilize Swashbuckle inside the implementation of custom services, instead of building something new from scratch.
But even if Microsoft isn’t interested, I believe it would still be useful for many people, therefore the community could build a tool to extract information about custom services and generate Open API documents. It wouldn’t necessarily have to support all features (or not from the beginning); people can easily add textual annotations, examples and so on in Swagger Editor, if needed. But being able to automatically generate JSON-based client classes for any custom service would be really handy.
Hi Martin, the custom services only support GET and POST. I get a method not allowed for PUT. I am working on a requirement that requires me to expose a custom service using PUT, but am having trouble understanding why this is not working. Are the handler mappings in IIS for custom services blocked for PUT and PATCH? Also, I am curious to extract request headers from an incoming request in custom service. This is a simple use case in web service world, but dont see any way to extract the incoming request headers in x++.
Hi. You can use a free online tool to convert JSON to YAML https://freetools.site/data-converters/json-to-yaml
Hey Martin, great article. Here begins my journey looking for a better way to document my services with OpenAPI documents. After dropping development for a long time, I was finally able to implement an automatic documentation generator for the D365FO, which I intend to share with the community soon.
Here is my gratitude for the first steps on this path.
Hi Marcelo,
do you have an update on this work? We are be very interested on how you did this.
Thx,
Sven