In this post, I’m going to introduce several design patterns when we write Azure Functions codes in C#, by considering full testabilities.
Sample codes used in this post can be found here.
- As a DevOps engineer,
- I want to search ARM templates using Azure Functions
- So that I can browse the search result.
Basic Function Code
We assume that we have a basic structure that Azure Functions can use. Here’s a basic function code might look like:
As you can see, all dependencies are instantiated and used within the function method. This works perfectly fine so there’s no problem at all. However, from a test point of view, this smells A LOT. How can we refactor this code for testing?
I’m going to introduce several design patterns. They can’t be overlooked, if you’ve been working with OO programming. I’m not going to dive into them too much in this post, by the way.
Service Locator Pattern
As you can see the code above, each and every Azure Functions method has the
static modifier. Because of this restriction, we can’t use a normal way of dependency injections but the Service Locator Pattern. With this, the code above can be refactored like this:
First of all, the function code declares the
IServiceLocator property with the
static modifier. Then, the
Run() method calls the service locator to resolve an instance (or dependency). Now, the method is fully testable.
Here’s a simple unit test code for it:
Mock instance is created and injected to the static property. This is the basic way of dependency injection for Azure Functions. However, there’s still an issue on the service locator pattern.
Mark Seemann argues on his blog post that the service locator pattern is an anti pattern because we can’t guarantee whether dependencies that we want to use have already been registered or not. Let’s have a look at the refactored code again:
Run() method, the
IGitHubService instance is resolved by this line:
var service = ServiceLocator.GetInstance();
When the amount of codes is relatively small, that wouldn’t be an issue. However, if the application grows up quickly and has become a fairly large one, can we ensure if the
service variable is always NOT
null? I don’t think so. We have to use the service locator pattern, but at the same time, try not to use it. That doesn’t sound quite right, though. How can we achieve this goal?
Strategy Pattern is basically one of the most popular design patterns and can be applied to all Functions codes. Let’s start from the
Every function now implements the
IFunction interface and all logics written in a trigger function MUST move into the
InvokeAsync() method. Now, the
FunctionBase abstract class implements the interface.
While implementing the
IFunction interface, the
FunctionBase class add the
virtual modifier to the
InvokeAsync() method so that other functions class inheriting this base class will override the method. Let’s put the real logic.
GetArmTemplateDirectoriesFunction class overrides the
InvokeAsync() method and processes everything within the method. Dead simple. By the way, what is the generic type of
TOptions? This is the Options Pattern that we deal with right after this section.
Many API endpoints have route variables. For example, if an HTTP trigger’s endpoint URL is
productId value keeps changing and the value can be handled by the function method like
Run(HttpRequestMessage req, int productId, ILogger log). In other words, route variables are passed through parameters of the
Run() method. Of course each function has different number of route variables. If this is the case, we can pass those route variables,
productId for example, through an
options instance. Code will be much simpler.
There is an abstract class of
GetArmTemplateDirectoriesFunctionParameterOptions class inherits it, and contains a property of
Query. This property can be used to store a querystring value for now. The
InvokeAsync() method now only cares for the options instance, instead of individual route variables.
Let’s have a look at the function code again. The
IGitHubService instance is injected through a constructor.
Those dependencies should be managed somewhere else, like an IoC container. Autofac and/or other IoC container libraries are good examples and they need to be combined with a service locator. If we introduce a Builder Pattern, it would be easily accomplished. Here’s a sample code:
Autofac, all dependencies are registered within the
RegisterModule() method and the container is wrapped with
AutofacServiceLocator and exported to
IServiceLocator within the
Factory Method Pattern
All good now. Let’s combine altogether, using the Factory Method Pattern.
Within the constructor, all dependencies are registered by the
ServiceLocatorBuilder instance. This includes all function classes that implement the
FunctionFactory has the
Create() method and it encapsulates the service locator.
This function now has been refactored again. The
IServiceLocator property is replaced with the
FunctionFactory property. Then, the
Run method calls the
Create() method and
InvokeAsync() method consecutively by fluent design approach. At the same time,
GetArmTemplateDirectoriesFunctionParameterOptions is instantiated and passed the querystring value, which is injected to the
Now, we’ve got all function triggers and its dependencies are fully decoupled and unit-testable. It has also achieved to encapsulates the service locator so that we don’t even care about that any longer. From now on, code coverages have gone up to 100%, if TDD is properly applied.
So far, I’ve briefly introduced several useful design patterns to test Azure Functions codes. Those patterns are used for everyday development. This might not be the perfect solution, but I’m sure this could be one of the best practices I’ve found so far. If you have a better approach, please send a PR to the GitHub repository.
Azure Logic Apps will be touched in the next post.