Visual Studio tends to be fairly opinionated about how projects and files are organized within a solution. In the case of a Functions project, the New Azure Function
context menu option naturally guides you towards having a single function trigger per class.
But doing so isn’t stricly necessary, and in some cases, it makes more sense to group many function triggers in a single class. There’s more than one way to skin a cat, so I’ve written up a few different ways you can organize functions within a Function app.
Don’t Group At All
Of course, the first way to group functions is to follow the default behaviour of Visual Studio and not group them at all. It shouldn’t be the go-to way to structure your functions, but there are situations where it’s the right choice.
One class per function trigger creates clutter and spreads the code around a lot more. Before long, the project is full of namespaces that contain a single class. So why would you want to create functions like this at all? Two reasons come to mind:
- The function trigger does something completely different from every other function within the project, such as addressing a different business domain.
- The function is likely to have specific scaling needs in the near future. Mixing it in a class with other function triggers makes it more difficult to spin it off into its own Function app later on.
Group by Trigger Type
It’s likely that a Function app is going to have more than one function of the same trigger type, especially when building an HTTP API. Similarly, if you’re using Timer triggers as an alternative to the deprecated Azure Scheduler, you’ve likely got a plethora of timers in your Function App.
There are two ways to go about grouping the functions. The first is to create a folder and related namespace, where each function can be in its own class, but share a logical grouping in the namespace. Every time you want another function, you’d click New Azure Function on the appropriate folder.
The second grouping option reduces the number of classes by instead creating new functions within a previously existing class. The class would end up with all the function triggers within it.
FlightsApi.cs
[FunctionName(HttpApiFunctions.GetFlights)]
public async Task<IActionResult> GetFlights(
[HttpTrigger(AuthorizationLevel.Function, HttpTriggerMethod.Get, Route = null)] HttpRequest req)
...
[FunctionName(HttpApiFunctions.GetFlight)]
public async Task<IActionResult> GetFlight(
[HttpTrigger(AuthorizationLevel.Function, HttpTriggerMethod.Get, Route = null)] HttpRequest req)
...
[FunctionName(HttpApiFunctions.CreateFlight)]
public async Task<IActionResult> CreateFlight(
[HttpTrigger(AuthorizationLevel.Function, HttpTriggerMethod.Post, Route = null)] HttpRequest req,
[Blob(BindingParameter.SchedulerBlobSchema, FileAccess.Read)] Stream validationSchema,
[Queue(BindingParameter.ScheduledFlightQueue)]ICollector<Flight> queueCollector,
ILogger log)
...
Grouping HTTP Triggers together, Timer Triggers together, naturally structures the Function app in a way that:
- Makes it easy to find the function you’re looking for, since you know that all functions of the same type are in the same namespace or class.
- Simplifies moving all the functions of a given type to their own Function app.
Group by Business Domain
Finally, we can group the functions by the domain which they apply to. Typically, a business process has multiple steps that perform actions based on the result of the previous step. This can be represented as a sequence of functions within a Function app.
For example, an HTTP Trigger function could write a message to a Service Bus Queue, which is read by another function that processes the message and sends a message to an Event Grid, where a subscriber function notifies a user by email of an event.
A change to one of the functions is likely to cause a change in the downstream functions. Keeping each of those functions as a unit, either within a single class or namespace, makes it easy to find and understand what’s going on within the Function app.
To Group, or Not To Group
At the end of the day, there is a case to be made for all of the above approaches. The one you choose for one Function app might not be the same as for another Function app, since their needs might be totally different. I usually try to find the approach that is best suited for purpose, rather than applying the same layout for every Function app.