DISCLAIMER: This post is purely a personal opinion, not representing or affiliating my employer's.
Applying DDD (Domain-Driven Development) methodology to your project requires several concepts. Ubiquitous Language (UL) and Domain-Specific Language (DSL) are only a few of them. For all members in a domain context, including domain experts and developers, using the same language (and terminology) to avoid getting confused from each other is one of the key concepts of UL and, in spite of not exactly corresponding concept, DSL is a "sort of" implementation of achieving UL within the domain context. It sounds very easy but applying DSL is pretty tricky and needs a lot of helps from experts. In this post, I'm going to discuss how to write DSL using Fluent API/Interface in an Azure Functions code. Very briefly.
NOTE: I wrote a blog post about an XSL mapper function using DSL in a very rough level. If you want to see the complete code, please refer to the repository.
Why DSL?
There are two types of DSL – external and internal. As many articles have already covered what the differences between both, I'm not repeating here, but we use the term DSL here pointing to the internal DSL. DSL is used to describe business logic, within the domain context, by using UL. Fluent API helps write DSL quite easily. Also, DSL expresses the business logic, not the code implementations – it's useful for encapsulation. In addition to this, DSL simplifies UL at the code level, and describes roles and intentions of each method precisely. In fact, from the domain perspective, it doesn't have to know how each method works internally, but it needs to know how those methods are related to each other, to build up a workflow.
I mentioned Fluent API (or Fluent Interface) a few times above. In the C# world, using extension methods really helps build the Fluent API. Of course, abusing this will result in violating Law of Demeter (LoD), so it should be carefully taken. It's also a good idea to start from a very specific situation to cover, rather than building it with lots of generics.
DSL for Domain Logic Workflow
OK. It's coding time. Let's have a look at the code below. Whenever you see my Function code, you always see this part as the first entry point.
public static IFunctionFactory Factory { get; set; } = new FunctionFactory(new AppModule()); | |
[FunctionName(nameof(XmlToXmlMapperHttpTrigger))] | |
public static async Task<HttpResponseMessage> Run( | |
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "mappers/xml/xml")] HttpRequestMessage req, | |
ILogger log) | |
{ | |
return result = await Factory.Create<IXmlToXmlMapperFunction, ILogger>(log) | |
.InvokeAsync<HttpRequestMessage, HttpResponseMessage>(req) | |
.ConfigureAwait(false); | |
} |
One of benefits using Fluent API is readability. It literally describes the workflow "fluently" by method chaining. Therefore, you can easily imagine what will happen when you run the code, just by reading the code. First of all, when the function is called, 1) it creates the service locator factory instance, IFunctionFactory
, 2) the factory creates the function instance, IXmlToXmlMapperFunction
, and 3) the function instance invokes the InvokeAsync
method to process the request and return the response, which is a simple workflow. As you can see, Fluent API has implement all the workflow.
Also, the method names used here are like Create
and InvokeAsync
, which is pretty intuitive to understand their roles and responsibilities. I can darely say I have implemented UL by writing DSL. However, this approach has a rabbit hole. It may violate LoD. Within the method chaining, moving one from the other might result in the NullReferenceException
error and this should really require defensive coding; otherwise the code might smell.
How can we write DSL, not violating LoD, with Fluent API? Let's have another example below. The InvokeAsync
method of the XmlToXmlMapperFunction
class contains the workflow – 1) it loads an XSLT file, 2) it loads DLL files that the XSL file refers to, 3) it loads the XML document to transform, and 4) it returns a transformed XML document.
The function class has the IXmlTransformHelper
instance that contains 1) LoadXslAsync
, 2) AddArgumentsAsync
, and 3) TransformAsync
methods. The code looks like:
var content = await this._helper | |
.LoadXslAsync(this._settings.Containers.Mappers, request.Mapper.Directory, request.Mapper.Name) | |
.AddArgumentsAsync(request.ExtensionObjects) | |
.TransformAsync(request.InputXml); |
All those methods actually returns the instance itself, IXmlTransformHelper
. Therefore, regardless of what happens inside the method, it returns the same return value and doesn't go out of scope, which satisfies LoD. It also keeps readability by having self-descriptive method names. If another method chaining is required, due to the business logic changes, it still conforms OCP (Open-Closed Principle) as each method returns the instance itself. This is one of considerations for DSL. If any small changes breaks DSL and/or method chaining, it will really be frustrating.
Here's another consideration. Each method here supports async
/await
. In other words, each method actually returns Task<IXmlTransformHelper>
instead of IXmlTransformHelper
. However, each method actually takes IXmlTransformHelper
inatance. How can it be done? Here's the trick. Let's have a look at the extension method, AddArgumentsAsync
below:
public static async Task<IXmlTransformHelper> AddArgumentsAsync(this Task<IXmlTransformHelper> helper, List<ExtensionObject> eos) | |
{ | |
var instance = await helper.ConfigureAwait(false); | |
return await instance.AddArgumentsAsync(eos).ConfigureAwait(false); | |
} |
This extension method wraps the IXmlTransformHelper.AddARgumentsAsync()
method. It gets the Task<IXmlTransformHelper>
instance as a primary parameter and rips the Task<>
part and invokes the actual method. Writing extension methods like this way brings much easier ways when constructing method chaining and we can get prepared for different situations with flexibility.
Is DSL Silver Bullet?
So far, we've briefly looked how we can build DSL using Fluent API, in Azure Functions. Someone may understand that DSL solves all the problems by making code succinct and simple. However, that's not true. DSL can't be coming out this easy way. Also not all situations does DSL works well. All we can do in the domain context is to, first of all, define UL, and build objects based on that UL. When the time comes, you will find out DSL becomes necessary and start refactoring your code.
The DSL code above are also not perfect. DSL should be continuously improving to get such flexibility. I hope this post gives a little bit of idea about using DSL in your domain.