Yo dawg, are you controllers getting plump?
Well I’ve got one technique you can use to drop pounds QUICK!
What is one of the most common ways things start to chub up?
Damn is it easy to slather on that biz logic fat right within our controller actions.
So what can we do about it?
Business Logic Thinning Strategies
There are multiple strategies you could choose to tackle business logic in a controller. Naming just two of them, you could:
Move the logic into reusable
Good if the logic is common amongst other controller actions like authorizing access to an entity. However, it does still couple the business logic with controller-related libraries. In other words, you wouldn’t use an
ActionFilter outside of an MVC or API route, which would mean you couldn’t use that business logic outside of that domain either. Perhaps you never would anyway, so in that case, an
ActionFilter might be a fine, pragmatic choice.
Move the logic into a service that is injected into the controller
This way works well because your business logic can live happily in its own reusable class, without coupling itself to any particular client technology.
This is the strategy we’ll be looking at for the remainder of this article.
Refactoring business logic into services
First, let’s clarify terms.
For our purposes, business logic is any code that implements the goals of the business. For an e-commerce application, this would be processes or tasks like creating orders, processing payments, creating and sending receipts, and adding or updating customer data.
In comparison, controllers are only responsible for managing the incoming HTTP request, delegating the work of satisfying the request, and passing on a response.
If you have trouble identifying the boundary between business logic and controller logic (clarifying responsibilities), send me a message on Twitter and I’ll try to help.
A service is really anything that… serves.
Whenever you call a method on one class from another, the calling class is the client and the class with the method is the service.
So there you have it, a service is a class with a method that can be called.
Sticking our business logic in its own class has a number of advantages. The ability to use the feature in areas beyond just the controller, the ability to test the business logic (and the controller) in isolation, and a big improvement in readability due to the fact that a blob or blobs of logic filling the controller can be reduced to clearly named method calls.
The code in question
Ok, I’m going to throw you for a bit of a loop.
The code we will be refactoring is actually an Azure Function, and not a controller.
Don’t freak out. Let me explain.
I’m choosing to use this code because it’s actual real world production code rather than an invented example.
On top of that, the code used to build an Azure Function is so gosh darn similar to a controller that you would barely know the difference if I didn’t call it out.
There are minor differences that I’ll point out, but the refactoring concepts in this article can still be applied 1-to-1.
For that reason I’ll be using the word controller and function interchangeably for the remainder of this article.
Let’s see the code…
Comparing Azure Functions and ASP.NET Controllers
You’ll notice right away that this code comprises a single class with a single method that returns an
IActionResult. Nearly identical to an action method endpoint of a
The main difference is that it is declared as static and passes a few arguments that you may not be used to (we’ll be changing that with our refactoring).
A brief explanation of behavior
This code is used as a listener for processing sales notifications that come from a shopping cart web app called SamCart.
You can configure an endpoint URL within SamCart’s admin panel that it will POST notifications to upon successful sales (or refunds). This code listens for those notifications and logs them to a Google Sheet spreadsheet using the Google Sheets API and it’s client library.
Tackling the task of refactoring
In an earlier article we looked at some strategies for code cleanup.
In this particular class, we have a fairly large
Run method that is doing multiple logical blocks of work.
Can you name them?
Setting up configuration, reading and parsing the HTTP request body into a
SamCartEvent POCO, and making use of the Google Sheets API to either add a new sales order or update the refund status of an existing order on a spreadsheet.
In order to simplify this code so that it’s easier to read and easier to change, let’s try to break it up into smaller bits and follow the Single Responsibility Principle more closely.
First, I found out the day of writing this article that Azure Functions allow you to benefit from the same automatic model binding that
Controllers are provided. I was lead astray by the original template-generated code that you can see is manually parsing the HTTP request body.
So our first change is to completely eliminate that parsing logic and simply declare a
SamCartEvent as the first argument to the
I’m also going to remove the logging calls and the logger argument for the clarity of this demonstration.
With that cleaned up, we are left with only two primary tasks. Setting up configuration, and making use of the Google Sheets API.
Creating a service class
Looking a bit deeper, we see that the configuration is only used by the Google Sheets API, so we could say that the work involved in setting it up is really along the same logical block of work of calling the API’s various services.
For that reason, it makes sense to move all of that code into it’s own class to be used as a service.
To do that, I’ll create a new class and method, named after what this code is actually doing from a high level. Ultimately we are using Google Sheets to log our SamCart events to a spreadsheet, so we move all of that code behind a method called
LogEvent for the class
One key difference here is that at the time of writing this article, there was no way to inject the ExecutionContext needed to derive the location of the configuration file, so the first line of code is an alternative way of doing so.
The rest is the same as before: getting the spreadsheet configuration and calling the appropriate API endpoint based on the type of
With this service class created, we now just need to make use of it in our original
The fastest way to do that would be “new it up” directly in the method. Let’s go that route first, and then we will improve upon it further on in the article.
Now we are looking good.
We have a much thinner method with a lot less reasons to change (code stability). Our method simply needs to hand off the work to a service and respond with a relevant result.
It is now more likely that the event logger would change, rather than the function or controller itself.
Can we improve this code further?
Earlier we mentioned “newing up” the event logger. If we instead make use of dependency injection, we can move the responsibility of choice onto the application itself.
Responsibility of choice is a term I’m coining just now which entails the decision of how a certain behavior is implemented.
As it stands right now, we are hardcoding the use of Google Sheets by our listener. Meaning it would require an actual code change and redeployment to ever log our events differently. This could be perfectly fine if you had no plans or requirements to change your logging mechanism, but for the sake of demonstration, let’s say that our customer mentioned possibly wanting to move the logging to Microsoft’s Excel, or to a plain old text file.
With that being the case, it would be in our benefit to allow changing the implementation of event logging, without having to change the controller.
Less change = less risk.
This is where a combination of interfaces and dependency injection come in handy.
First, we can extract an interface from our Google Sheets event logger.
With this in place, we are now free to create as many different logger implementations as we want.
As an example, we could have a very primitive console logger like this one.
Interfaces are fun, but without dependency injection we would still have to change the code of the controller’s
Run method to “new up” the alternative implementation.
Let’s instead make use of the built in dependency injection container that is provided by ASP.NET to move the implementation decision to the application startup configuration.
To do that in an Azure Function is very similar to how you do that in an ASP.NET MVC or Web API project, you simply register your types in the Startup.cs class and then use them via constructor parameters.
With it moved, we have improved the stability of the code by no longer needing to change the controller’s
We have also dramatically enhanced the readability of each individual component. Compare the above code snippet to the original code at the beginning of the article.
If you’d like to go deeper, I’ve put together a set of recipes with code samples and an exercise specifically for refactoring ASP.NET Controllers. It is not only the how, but provides a thorough exploration of the why.
You can get one of the recipes below (along with a discount code on the entire set).