Introduction
There is a lot of metadata which needs to be added to Azure Functions to allow for the definition to be picked up by Swagger and be presented in the Swagger UI. It needs to be named and tagged. It has to have the parameters specified. It has to have the return status defined with the type of response in the body. And this is just a few of the options. As the call gets more complicated the more metadata needs to be added to the definition. With all this metadata the code file which includes the function definition can get a bit “busy” and the attention is drawn away from the functionality and concentrates on the metadata. But is there a way to resolve this? Is there a way to make the functionality not be “drowned out” by the metadata? Let’s take a look!
Partiality All The Way
Partial classes have been around since C# 2.0 released in 2005 - https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-20 - and are a pretty cool feature.
Partial classes allow for class definitions to be split across files in your application and give developers a lot of flexibility. It really aided with ASP.NET WebForm applications which still included generated code in the code behind file. It allowed for a generated file to be created by the tooling and your custom code to be in a separate file. This removed the need for the magical “generated” region in the cs file (ew regions!) and made webforms a bit cleaner.
In 2007 C#3.0 was released which had a ton of features such as Lambda expressions, Extension methods etc. - https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-30. But also one of the little known features also to be released, or at least in my experience, was partial methods.
What is a Partial Method?
If you’re unfamiliar with a partial method it can be thought of having similar behavior as an abstract method. The signature and structure is defined but there is no implementation. However unlike an abstract method where the derived class provides an implementation in a partial method the implementation is provided in the same class definition potentially split across different files. This is the functionality we are going to look at to help us with the “metadata noise” problem.
Implement this in Azure Functions
So the concept is that an Azure Function is defined as a method on a class. This is straight forward. Due to this relative simplicity we can specify them as partial methods. Now partial methods do have some rules around how they can and can’t be defined - https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/partial-method - but Azure Function definitions fall into the rules so can be applied.
V3 In Process Function Example
The technique can be applied to both V3 “In Process” ASP.NET Core 3.1 based Azure Functions as well as V4 (see further down).
Starting Point
[FunctionName(nameof(SayHello))]
[OpenApiOperation(operationId: nameof(SayHello), tags: new[] { "name" })]
[OpenApiParameter(name: "name", In = ParameterLocation.Query, Required = true, Type = typeof(string), Description = "The **Name** parameter")]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "text/plain", bodyType: typeof(string), Description = "The OK response")]
public async Task<IActionResult> SayHello(
[HttpTrigger(AuthorizationLevel.Anonymous, nameof(HttpMethod.Get), Route = null)] HttpRequest req,
ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
string name = req.Query["name"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
string responseMessage = string.IsNullOrEmpty(name)
? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
: $"Hello, {name}. This HTTP triggered function executed successfully.";
return new OkObjectResult(responseMessage);
}
As we can see from the above there is a lot of metadata for the OpenAPI definition as well as the HttpTrigger
attribute decorating the request parameter. This is quite a lot of noise for a function which has limited functionality.
So how can we split this up?
partial class Function1
{
[FunctionName(nameof(SayHello))]
[OpenApiOperation(operationId: nameof(SayHello), tags: new[] { "name" })]
[OpenApiParameter(name: "name", In = ParameterLocation.Query, Required = true, Type = typeof(string), Description = "The **Name** parameter")]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "text/plain", bodyType: typeof(string), Description = "The OK response")]
public partial Task<IActionResult> SayHello(
[HttpTrigger(AuthorizationLevel.Anonymous, nameof(HttpMethod.Get), Route = null)] HttpRequest req,
ILogger log);
}
First we need to move all of the metadata attributes to a partial method definition. This has to be in the same class, which is now marked as partial, and has to have the same method signature. We can also decorate the HttpRequest
method parameter here with the Azure Functions HttpTrigger attribute to remove that noise as well from the implementation.
And now for the Implementation
partial class Function1
{
public partial async Task<IActionResult> SayHello(
HttpRequest req,
ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
string name = req.Query["name"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
string responseMessage = string.IsNullOrEmpty(name)
? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
: $"Hello, {name}. This HTTP triggered function executed successfully.";
return new OkObjectResult(responseMessage);
}
}
We can now construct the implementation of the Azure Function in a separate file potentially to decouple the noise from the implementation in the source code. Now obviously this won’t decouple the constructs at build/runtime but that is not what this technique is looking for, we are looking to aid with readability and maintainability in the source for the developers.
Notice we don’t have any of the attributes on the method, as they have been previous defined, and also the HttpTrigger
attribute isn’t required either.
V4 Out Of Process .NET 6 Function Example
The technique can also be applied to .NET 6 Out-of-process Azure Functions as well in a similar way.
Starting Point
public class Function1
{
[Function(nameof(SayHello))]
[OpenApiOperation(operationId: nameof(SayHello), tags: new[] { "name" })]
[OpenApiParameter(name: "name", In = ParameterLocation.Query, Required = false, Type = typeof(string), Description = "The **Name** parameter")]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "text/plain", bodyType: typeof(string), Description = "The OK response")]
public async Task<HttpResponseData> SayHello(
[HttpTrigger(AuthorizationLevel.Anonymous, nameof(HttpMethod.Get))] HttpRequestData req,
string name,
FunctionContext executionContext)
{
var response = req.CreateResponse(HttpStatusCode.OK);
response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
await response.WriteStringAsync($"Welcome to Azure Functions {name}!");
return response;
}
}
As with the V3 in process version previously discussed there is a number of metadata attribute applied to the function to work with Swagger.
So how can we split this up?
partial class Function1
{
[Function(nameof(SayHello))]
[OpenApiOperation(operationId: nameof(SayHello), tags: new[] { "name" })]
[OpenApiParameter(name: "name", In = ParameterLocation.Query, Required = false, Type = typeof(string), Description = "The **Name** parameter")]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "text/plain", bodyType: typeof(string), Description = "The OK response")]
public partial Task<HttpResponseData> SayHello(
[HttpTrigger(AuthorizationLevel.Anonymous, nameof(HttpMethod.Get))] HttpRequestData req,
string name,
FunctionContext executionContext);
}
Again as per the previous example we can split the function up so that all the metadata attributes are defined on the partial function. Again as with the v3 version we can move the HttpTrigger
attribute into this part of the definition so that it removes some of the noise from the implementation.
And now for the Implementation
partial class Function1
{
public async partial Task<HttpResponseData> SayHello(
HttpRequestData req,
string name,
FunctionContext executionContext)
{
var response = req.CreateResponse(HttpStatusCode.OK);
response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
await response.WriteStringAsync($"Welcome to Azure Functions {name}!");
return response;
}
}
The implementation method definition is now very clean and allows the developer to concentrate on the functionality. Like the v3 version this also can be split across different files etc.
Benefits of this technique
So why would you want to do this?
It allows for differentiation between the “definition” and the “implementation”. This can allow for the definitions to be defined up front, maybe by an architect or technical BA etc. to fit with the design of the API and then the developer can implement the “specification” as defined.
Any changes to the implementation won’t accidently change the API surface definition unless the requirements have changed. This should limit changes to the REST API routing from changing without appropriate versioning etc. as any method signture changes will fail to compile.
It removes the cognitive noise of all the metadata from the implementation of the function.
Downsides?
It could be seen as “magic” and not give the developers knowledge of the routing etc. which goes with the HttpTrigger
based function. Less knowledgeable developers could struggle to understand how the Azure Function could be triggered.
Conclusion
In this post we have looked at how we can use partial classes and partial methods to split Azure Function definitions into “definition” and “implementation”. This is to aid in removing the cognitive noise on functions which have a large number of attributes applied to the methods.
I think this is an interesting technique which could be used in collaboration with source generators to take the concept of “definition is defined by the Architect or Technical BA” further. I can see definitions being defined and constructed in a Swagger Definition or spreadsheet (or some other mechanism) and be passed through a source generator to create these function “definitions” which are then looking for the implementation to be built.
What are you thoughts on this? What crazy ideas have you used partial functions for? Let me know on Twitter @WestDiscGolf.