Introduction
With the release of .NET 6 round the corner and the advancements in ASP.NET Core (do we still call it Core?) there is a push to aid with making the introduction bar to .NET and C# a bit lower so it’s not scary for new developers to the ecosystem to get started. This has brought on a number of changes and enhancements to the language and new project templates to aid with this.
The language features which are used in these new .NET SDK Templates are:
- Top-level statements
- async Main
- Global using directives (via SDK driven defaults)
- File-scoped namespaces
- Target-typed new expressions
- Nullable reference types
And you can read more of an introduction to the templates on Scott Hanselmans blog - https://www.hanselman.com/blog/exploring-a-minimal-web-api-with-aspnet-core-6
So as you’re hopefully aware I’ve been investigating Azure Functions a lot recently and the whole concept got me thinking. Azure Functions with HttpTriggers are similar to ASP.NET Core controller actions in that they handle http requests, have routing, can handle model binding, dependency injection etc. so how could a “Minimal API” using Azure Functions look?
Now I don’t have the luxury of changing the compiler or adding language features to aid with my investigation but just how far can I get with what I currently have in v4 Azure Functions and out-of-process isolated .NET 6 hosting?
Let’s find out!
Show me the codez!
If you want to jump straight to the code it can be found - https://github.com/WestDiscGolf/MinimalApiFunctions
Program.cs
As with all top level statement programs and the MinimalAPI revolution we start in Program.cs.
var connectionString = Environment.GetEnvironmentVariable("SqlConnectionString");
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices(services =>
{
services.AddDbContext<TodoDb>(options =>
options.UseSqlServer(connectionString)
);
})
.Build();
await CreateDbIfNotExists();
await host.RunAsync();
async Task CreateDbIfNotExists()
{
var options = new DbContextOptionsBuilder<TodoDb>().UseSqlServer(connectionString).Options;
using var db = new TodoDb(options);
await db.Database.EnsureCreatedAsync();
// todo: migrations
}
Working through we can see that we are setting up the program host which will run the application. This uses the builder pattern using the HostBuilder
to setup the processing as required. This is very similar to the ASP.NET Core host building pattern however due to the fact we are in the Azure Functions world, and hence essentially a worker process, this uses some different extension methods and patterns. Essentially it is doing similar items to setup the defaults. This allows for registering middleware and services with the DI container so it has familiarity from an ASP.NET Core setup.
We next run some database creation processing to allow for creating a new db and then we run it to start. This database creation code is influenced by Damian Edwards’ MinimalApiPlayground implementation - https://github.com/DamianEdwards/MinimalApiPlayground/blob/main/src/Todo.EFCore/Program.cs#L145
Now we’re up and running, what’s next?
Show me the Functions!
I’m not going to walk through each function implementation but you can browse the code - https://github.com/WestDiscGolf/MinimalApiFunctions
The whole point of the “minimal api” is to keep the ceremony down to a minimal and to get up and running. This is what I have tried to do, within the remit of .NET 6 out-of-process Azure Functions, to show how to do something similar with Http Triggers.
So with all the functions the owning class takes the DbContext “db” as a constructor dependency which is then used in all of the functions we are going to discuss in the rest of the post.
Get List
[Function("todo-list")]
public async Task<HttpResponseData> Todos([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "todos")] HttpRequestData req, FunctionContext executionContext)
{
return await req.OkObjectResponse(await _db.Todos.ToListAsync());
}
Getting a list of Todo
items is probably one of the easiest. We are executing the ToListAsync
method on the DbSet of the data context. This is then returned in an OK 200 response construct. The above, and the following examples, use some extension methods to help with creating responses which we will discuss later.
We have the similar routing to GET the todo items as a traditional controller/action as well as the new minimal api. This function could be converted to an expression body but as other functions can’t then I decided to keep them consistent across the board.
The Complete and Incomplete listing end points are along the same lines but with the predicate in the Where
clause to check whether they are complete or not.
Get By Id
[Function("todo-find")]
public async Task<HttpResponseData> TodosFindById([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "todos/{id:int}")] HttpRequestData req, int id, FunctionContext executionContext)
{
if (await _db.Todos.FindAsync(id) is Todo todo)
{
return await req.OkObjectResponse(todo);
}
return req.NotFoundResponse();
}
The “Get By Id” uses some of the newer pattern matching constructs to avoid checking for nulls and additional local variables being introduced into the code. Once the query to the database is done and we have the target instance we return it using the same OkObjectResponse
extension method. The NotFoundResponse
extension method allows for Not Found response creation without having to manually find the right status etc each time.
Creating a New Todo
The next interesting function is the POST so we can actually save a new instance.
[Function("todo-post")]
public async Task<HttpResponseData> TodosPost([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "todos")] HttpRequestData req, FunctionContext executionContext)
{
var todo = await req.ReadFromJsonAsync<Todo>();
if (!MinimalValidation.TryValidate(todo, out var errors))
{
return await req.ValidationResponse(errors);
}
_db.Todos.Add(todo);
await _db.SaveChangesAsync();
return await req.CreatedAtResponse(nameof(TodosFindById), new { id = todo.Id }, todo);
}
This is where there are current limitations in .NET 6 Isolated Azure Functions. There are gaps in the model binding and currently I couldn’t find a way to add in your own. I investigated intercepting the request in middleware and around the area of the binding features including accessing “IFunctionBindingsFeature” but couldn’t determine how to extend this. I believe this is probably on purpose until they roll out a proper supported implementation. Due to this we have to read the content directly from the HttpRequestData
body and construct the instance ourselves.
We can then use a library by Damian Edwards called MinimalValidation - https://www.nuget.org/packages/MinimalValidation - to do our validation. This is a simple library which can perform validation based on validation constructs in System.ComponentModel.DataAnnotations
. This library is used in Damian’s own Minimal Api Playground repo - https://github.com/DamianEdwards/MinimalApiPlayground . We then can add the todo and save it to the db. Nothing clever here.
What I did want to do now was return the created todo with a “Created” 201 http status code but also construct the Location header for the newly created resource. I could have written the route in a string literal but that didn’t quite feel right. The pattern I was looking for was similar to how ASP.NET Core specifies the route name and route values and then using the routing table constructs the url. The issue I had though was there does not appear to be the same constructs in Azure Functions yet to allow for access to the routes of the registered functions etc. So I had to make it up myself. It’s proof of concept and rough but you’ll get the idea!
Constructing the Location
Calling Api Surface
Initially I wanted the calling API surface to be clean. The inspiration was the ASP.NET Core result processing where you specify the route and the route data.
return await req.CreatedAtResponse(nameof(TodosFindById), new { id = todo.Id }, todo);
The above allows for specifying the target routing action, the anonymous object to pass the routing data and the instance created. I was happy with structure and allowed to find the get end point Azure Function.
Constructing the Location
This is where the code gets clunky. It is what it is and due to framework and Azure Functions restrictions I don’t know how else to do it. In ASP.NET Core there is the ability to create url links which has access to the routing information but there doesn’t seem to be this construct in Azure Functions. Please let me know if there is a better way of doing this currently!
public static async Task<HttpResponseData> CreatedAtResponse<T>(this HttpRequestData request, string nameOfFunction, object routeValues, T data = default)
{
var methods = typeof(Functions).GetMethods();
var functions = methods.Where(x => x.GetCustomAttribute<FunctionAttribute>() != null).ToList();
var root = functions.FirstOrDefault(x => x.Name == nameOfFunction);
var methodParameters = root.GetParameters().FirstOrDefault(x => x.GetCustomAttribute<HttpTriggerAttribute>() != null);
var routeInfo = methodParameters?.GetCustomAttribute<HttpTriggerAttribute>()?.Route;
var template = TemplateParser.Parse(routeInfo);
var values = new RouteValueDictionary(routeValues);
Due to the restrictions mentioned above we have to use our trusty friend reflection. The assumption in this code is all the functions are defined in the same class however this does not have to be the case and we would need to look for all classes/methods in an array of assemblies. However to keep items relatively straight forward we are only looking at Azure Function methods defined on the Functions
class.
We then limit the methods by the FunctionAttribute
decorated attribute. We’re not interested in any of the base methods on the class or any others so we want to restrict down the result. Before anyone starts jumping on the “this could be combined and then have less lines of code” bandwaggon I don’t disagree but as its POC code and debugging through at different steps aids with determining what is available I went with the separate line approach for now.
We then want to find the method which is the target name and find the request parameter which has been decorated with HttpTriggerAttribute
. We need to get the details of this attribute so we can access the routing information specified as part of the definition.
Now we have the data we need, the route and the route values we can then use these to construct the route dynamically before adding it to the location header.
// get the api prefix from configuration somehow!?
var urlBits = new List<object>();
urlBits.Add(string.Empty); // HACK: additional item so the join below starts with a slash
urlBits.Add("api"); // HACK: this needs to come from hosting configuration some how
// now we have the template parsed need to push it together with an anonymous object
foreach (var segment in template.Segments)
{
foreach (var templatePart in segment.Parts)
{
if (!string.IsNullOrWhiteSpace(templatePart.Text))
{
urlBits.Add(templatePart.Text);
}
if (!string.IsNullOrWhiteSpace(templatePart.Name)
&& values.TryGetValue(templatePart.Name, out var value))
{
urlBits.Add(value);
}
}
}
var url = string.Join("/", urlBits);
The route construction is a bit rough and has a number of points where it could be improved. This is due to limitation of available information from the Azure Functions constructs and my limited knowledge in this area. I hope this area will improve in future versions of Azure Functions. There is limited checking in the code above and as I previously said it’s a proof of concept and a bit rough around the edges.
Once the location url is created we can construct the response.
var response = request.CreateResponse(HttpStatusCode.Created);
response.Headers.Add("Location", url);
if (data != null)
{
await response.WriteAsJsonAsync(data).ConfigureAwait(false);
}
return response;
This adds the generated location url as a header in the “Created” response so can be accessed from the calling code. It also allows for optional adding of the payload if the data has been specified.
Conclusion
In this post I have tried to create a “minimal api” using Azure Functions using on Http Triggers trying to use the ethos of the Minimal APIs in .NET 6. It’s been an interesting thought experiment about how it could be done using Azure Functions. It’s been interesting using the .NET 6 top level statements to work with the out-of-process Azure Functions setup. I’ve had restrictions due to not being able to introduce language features or change underlying framework functionality to support the experiment. I wonder if in the future there will be functionality to register azure functions in a similar way to registering routes with extension methods on the app builder maybe? Dynamically finding functions through reflection still seems a bit “odd” to me but it feels like this could be something the team could look into in the future.
If you have any thoughts or ideas then reach out to me on Twitter @WestDiscGolf.