Did you notice that Microsoft announces a new and amazing web development framework each year? This year we had Minimal API hosting , before that we had Blazor , and before that we had ASP.NET Core . My point is that as the years go by, we get more and more technologies, and it’s getting hard to keep track of them all. In this article series, we’ll go over all the ways to build a web application using Microsoft technologies and try to make some order in the mess. Our constraint is to use .NET and Azure.
In the previous article , we covered options for the client-side, including SPA frameworks, Web Assembly, and ASP.NET MVC . Some of those choices, like Blazor Server , Razor Pages , and ASP.NET MVC, were server-rendered pages. But in the case of single-page applications, which is sort of the industry default in recent years, the client needs a REST API server that will return the actual data. In this post, we’ll see how to create such an API server in .NET.
.NET Server-Side technologies
Microsoft created a lot of web development frameworks over the years. To make things clearer, here’s a brief historical recap:
Our journey begins in 1998 when Microsoft released Classic ASP pages. They were replaced by ASP.NET pages and ASP.NET Web Forms in 2002 which were a success at the time. Moving forward to around 2005, new frameworks like Ruby on Rails gained popularity with the MVC pattern, to which Microsoft responded in 2007 with their own MVC framework called ASP.NET MVC . This is around the time that Silverlight was released, which was plugin-based and not a server rendering HTML/CSS. Even though many .NET developers loved Silverlight, fate had something else in mind when Apple announced iPhone wouldn’t be supporting plugins . The technology died out around 2011 . In 2012, following the success of SPA frameworks like AngularJs , ASP.NET Web API was released, which was using a similar MVC model but providing only a server-side REST API. In 2016, another server-side option was added to this stack, whose name strangely didn’t include ASP.NET. This was Azure Functions, which offered a serverless model. In the same year 2016, .NET Core was released, bringing with it ASP.NET Core MVC and ASP.NET Core Web API , the successors to their respective previous versions. Blazor Server and Blazor WebAssembly , the last two major frameworks added to our story, were first released at 2018 and became GA in 2019 and 2020 respectively. Finally, .NET 6 was released in late 2021 along with ASP.NET Minimal APIs .
I hope this history lesson didn’t confuse you even further. To simplify, this whole journey leaves us with just a few realistic modern options for a server-side API:
There are other options if you want your ASP.NET server to render pages, in which case, you can choose between ASP.NET Core MVC, Razor pages, and Blazor Server. We covered those in the previous article . But if you chose a client-side option like a single-page-application (SPA), then you’ll need a back-end server which will be one of the above. Let’s see how those fare against each other.
ASP.NET Core Web API
Web API is the canonic choice to create an API in .NET. It had a lot of versions and iterations since 2012 and grew into a mature and productive framework which also happens to be very performant. It follows an MVC pattern where you map URL routes to Controller classes and specific endpoints to methods (known as Actions). There are a myriad of features and support libraries. There’s good inherent support for dependency injection. Things like authorization and serialization are easy to implement and customize thanks to a middleware model support where you can add behavior before or after specific stages in a request.
You have everything you could hope and dream for in terms of features and customization for your REST API server.
There are some downsides to this framework, at least in my personal opinion.
- The attributes system to represent URL routes and operation types (GET, POST, etc) isn’t great.
- Each controller usually includes multiple endpoints, that often need different dependencies. They are injected into the constructor and saved as members, so you end up with bloated controllers that contain too much noise.
- There’s a lot of boiler-plate code and ceremony required for a simple application. While this code is usually generated by Visual Studio’s template, it’s still hard for newcomers to understand how all the parts come together. Those include the
Configure
andConfigureServices
methods, theStartup.cs
andProgram.cs files
, and the Controllers.
Here’s an example from AspNetCore.Docs . Startup.cs:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
And here’s the controller that represents the endpoint GET /WeatherForecast
:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
// The Web API will only accept tokens 1) for users, and 2) having the access_as_user scope for this API
static readonly string[] scopeRequiredByApi = new string[] { "access_as_user" };
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
}
Note that the example above is the classic way to write ASP.NET Core Web API. .NET 6 reduced the ceremony a bit and now the code from Startup.cs
moved to Program.cs
and became somewhat nicer
.
By the way, to see how ASP.NET Core is doing in terms of performance, check out TechEmpower benchmarks and look for “aspcore”. ASP.NET Core is usually one of the top performers.
ASP.NET Core Minimal APIs
If you ever worked with Node.js, you know that it takes about 5 lines of code to create an app with an HTTP endpoint. So why does a simple ASP.NET Web API requires 5 files and dozens of lines of code? The answer is it doesn’t. It’s just the way this technology evolved, the C# language, and an opinionated view by the ASP.NET team. So with that in mind, the new Minimal APIs template allows you to do the same thing Node.js does in 5 lines of code, in 4 lines of code.
Program.cs:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
You can observe that there aren’t any using
statements, no namespaces, and no classes. This is all part of the C# 10 magic that includes Global using directives
, File-scoped namespace declaration
, and top-level statements
.
In addition, there are no special methods that you need to override like Configure
and ConfigureServices
. Instead, you do everything in simple functional code, like this:
var builder = WebApplication.CreateBuilder(args);
// Inject services:
builder.Services.AddSingleton<SomeClass>(new SomeClass());
var app = builder.Build();
// Consuming services is just a matter of adding them as a parameter
app.MapPost("/checkDI", ([FromServices]SomeClass someClass) =>
{
var client = httpClientFactory.CreateClient();
return Results.Ok();
});
app.Run();
You can even omit the [FromServices]
attribute.
What about authorization, middleware, Swagger, and all the other million features we have in ASP.NET Web API you might ask? Everything you know and love is still there. Just simpler.
Another advantage is a slight performance improvement due to removing some overhead.
There’s an interesting open-source alternative to Minimal APIs called Fast Endpoints , which also allows writing an HTTP server in .NET without the ceremony and overhead of ASP.NET Web API. Here’s a snippet:
Program.cs:
global using FastEndpoints;
var builder = WebApplication.CreateBuilder();
builder.Services.AddFastEndpoints();
var app = builder.Build();
app.UseFastEndpoints();
app.Run();
An endpoint class:
public class MyEndpoint : Endpoint<MyRequest>
{
public override void Configure()
{
Verbs(Http.POST);
Routes("/api/user/create");
AllowAnonymous();
}
public override async Task HandleAsync(MyRequest req, CancellationToken ct)
{
var response = new MyResponse()
{
FullName = req.FirstName + " " + req.LastName,
IsOver18 = req.Age > 18
};
await SendAsync(response);
}
}
Pretty neat, right?
Azure Functions
Azure Functions is quite different from the ASP.NET offerings. It offers a serverless architecture model, in which a server does exist but you sort of don’t have to think about it or manage it. You write the code and a server will magically appear and run that code when needed. Azure will scale automatically according to the request load, allowing many server instances (up to 200) to run in parallel, handling your requests.
This is marketed as a programming model where you think only about solving problems and not about managing your server. I don’t entirely buy this because an Azure App Service also offers managed hosting, in which you don’t have to worry too much about managing your VM and you can scale automatically just as well. However, there is something to be said about some Azure Function features that an App Service just doesn’t provide like:
- You can write your HTTP handler in Azure Portal without even having to open Visual Studio.
- The Function can run in response to an HTTP request or to other triggers , like running code when a file is uploaded to blob storage, respond to database changes, respond to queue messages, or run scheduled tasks.
- You can chain a series of functions together using durable functions , and create entire flows from your functions with one triggering another and passing on the state.
You might be familiar with a feature of an Azure App Service called WebJobs that allows running scheduled background tasks. You can think of Azure Functions as WebJobs as a service, except it’s richer in features.
Azure Functions has three main types of hosting plans :
- Consumption plan is the default plan where you pay only for time and resources when your functions are running. The server instances will automatically scale according to usage, even in high load. There’s a maximum timeout of 10 minutes, after which your function will be in fact killed. In this mode, the servers that run your functions will become idle after a while, in which case the next time a server will have to cold start before a function can run, which can add a lot to your API request latency.
- App Service plan (aka Dedicated plan) is when you pay for a dedicated Azure App Service, which is used to run the Azure Functions. This takes care of the cold start issue since the service is always on. There’s also an unlimited maximum timeout for long-running functions. But you will pay for the dedicated VM for the whole time and not just for when your code runs.
- The Premium plan is like the consumption plan with benefits. It pre-warms servers to make sure there isn’t any cold-start downtime. It removes the maximum running time limitations. It supports virtual networks, which the basic consumption plan doesn’t.
By the way, the consumption plan includes a very tempting free grant of 1 million requests and 400,000 GBs of resources. We’ll talk about it a bit when trying to choose between the options.
What about Azure Kubernetes Service (AKS) and microservices?
We talked in this post about all the server-side options for your applications with .NET. This might be an ASP.NET Web API or an Azure Function. But Kubernetes is an orchestrator for multiple services, not an API server. Choosing between a Kubernetes cluster or a VM is a deployment choice. You might choose to do microservice architecture with AKS where each service is an ASP.NET Web API project. Or you can deploy those same ASP.NET Web API projects to Azure App Service instances. There are pros and cons to each approach, considerations like latency and isolation, and more interesting stuff when choosing a deployment method, but we’ll talk about that in the next post.
By the way, Kubernetes isn’t the only option for microservices in Azure, there are some very compelling reasons to use the lesser-known Azure Service Fabric . But more on that in the next post as well.
Making a choice
I think we covered all the popular modern technologies for an API server in .NET. Let’s go over them and try to figure out the pros and cons of each.
-
ASP.NET Core Web API – This is the classic .NET solution for a RESTful API. You can’t go too wrong by going this way. Sure, there are some issues, but you get used to them and it’s not a huge deal. There’s a good chance your developers will already know ASP.NET Web API and that’s a big benefit.
-
ASP.NET Core Minimal Hosting APIs – The core technology in Minimal APIs and Web API is the same and the difference between them is not that big. Instead of using controllers, you’ll be using methods for each endpoint. This seems to be a fairly safe choice, so I would definitely use Minimal APIs in small or medium projects. In large applications, the choice is a bit tougher since ideally, you would want to give this tech some more time. Microsoft tends to make big changes in versions 2 and 3. See .NET Core for case and point. I’d also love to see how the community responds over time.
-
Fast Endpoints – We briefly talked about Fast Endpoints as an opinionated alternative to Minimal APIs. I have to admit that I absolutely love the approach this library takes. Everything is very well thought out, like the way you create a class per endpoints, the routing and verbs configuration , the mapping , and the validation . It’s based on ASP.NET Minimal APIs code, which means less risk. It’s faster than ASP.NET Minimal APIs and much faster than ASP.NET Web APIs. The GitHub repo looks very good with over 1,000 commits and 106 out of 112 issues closed as I’m writing this.
Having said that, it’s hard to use open source libraries in the universe of .NET. It’s a known issue with Microsoft making their own projects for anything big and then 99.9% of the community preferring Microsoft’s version. This means open source projects are never as invested and thus more dangerous to use. What if the maintainer of Fast Endpoints decides to stop maintaining? It’s not like 20 others are eager to take their place. It’s up to you to take all that into account when deciding.
-
Azure Functions – I think there should be a very good reason to use Azure Functions as your application’s API, and let me explain why. The development of both solutions is pretty similar. You’ll write both Functions and ASP.NET in Visual Studio projects. You’ll publish both to Azure in a similar way. Both can be auto-scaled or not. The complexity of Azure Functions can be just as big or small as an Action in your Controller.
The choice should be whether you want a serverless model or not. Consider that when running in a Consumption plan , you’ll occasionally have to wait for your Functions to cold-start, which can take as much as 10 seconds . Not the end of the world, but not great either. As for the cost, it’s smaller if you have few requests, but at some point, an App Service or a Virtual Machine will be cheaper. Since a single App Service is pretty cheap, you might as well pay for it and not worry about monitoring your Azure Function costs. Or you can host your Azure Functions in an App Service plan, in which case there isn’t much of a point to use them in the first place. You can also use the Premium plan , which will solve the cold-start problem but cost more.
I think Functions are great for a lot of things, but not for an application’s Web API. You might have scheduled tasks that you want to perform, listen to queue events, or run some code when there are database changes. Functions are great at these kinds of background tasks, and I suggest using them for those.
What’s next?
Now that you chose client and server technology, it’s time to deploy them to Azure. In the next blog post, we’ll explore all the options to deploy and host in Azure, including App Services, Static Web Apps, Kubernetes, and others.
Hope this was useful to you, stay tuned to the next one. Cheers.