The Best C# .NET Web Application Tech Stack: Choosing The Back End

best dotnet csharp server.p

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 and ConfigureServices methods, the Startup.cs and Program.cs files, and the Controllers.

Here’s an example from AspNetCore.Docs. Startup.cs:

And here’s the controller that represents the endpoint GET /WeatherForecast:

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:

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:

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:

An endpoint class:

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.

Share:

Enjoy the blog? I would love you to subscribe! Performance Optimizations in C#: 10 Best Practices (exclusive article)

Want to become an expert problem solver? Check out a chapter from my book Practical Debugging for .NET Developers

11 thoughts on “The Best C# .NET Web Application Tech Stack: Choosing The Back End”

    1. Thank you for the overview. I wasn’t aware of ASP.NET Core Minimal APIs before.

      Quick observation on use of .NET Core Functions for hosting APIs. This worked very well for us when our API needs were modest. However when we tried to use the same approach for a group of domain APIs in a much larger system, the lack of native OData support became a serious issue.

    2. Martin Lottering

      So what you’re saying is that we had a new web framework from Microsoft every year. So we got 22,5 web frameworks from Microsoft? Right? That is what you’re saying, or not?

      1. Michael Shpilt

        We can try and count then. ASP classic, ASP.NET Pages, WebForms, ASP.NET MVC 1, ASP.NET MVC 2, ASP.NET MVC 3, ASP.NET MVC 4, ASP.NET MVC 5, ASP.NET Web API, ASP.NET Core MVC, ASP.NET Core Web API, Silverlight, Razor Pages, Minimal APIs, Blazor client, Blazor server, Azure Functions.
        That’s 17, but I probably missed a few. And if you add related frameworks like Service Fabric and WCF then I’m sure we’ll get there.

  1. I’ll right away take holԀ of your гss fеed as I can’t to find your e-maiⅼ subscriptіon link or newsletter service.
    Ꭰo you have any? Pⅼeaѕe alⅼow me realіze so that I
    may just subscribе. Thanks.

  2. This is a very well written blog comparing the options available in the market.
    Short and simple

  3. Being a new developer, I can say, this blog is so well written that even I can understand and at least try to choose better options while creating a new project.
    THANK YOU

  4. Very nice article!
    However, you state constructor bloat as an issue in the context of dependency injection.
    May I point out that, for dependies that are only used in only some actions, the [FromServices] attribute can be used on the *action*. This avoids bloating constructors among other things.

    1. To elaborate, you do not use the attribute directly on an action itself, but on a *parameter* to the action. Add the parameter to the action just as you would to a constructor, and add the [FromServices] attribute.

Comments are closed.