Skip to main content

Command Palette

Search for a command to run...

ASP.NET Primer

Updated
5 min read

This blog entry is for those of you who are programmers, but haven't done a lot a of ASP.NET programming, or not much in the .NET Core era. It is easy to feel overwhelmed by the framework, just wanting to write some code, but not understanding how to write code in the confines and benefits the framework provides.

First, it is important to note the following things about ASP.NET:

  1. It is heavily dependent upon Dependency Injection

  2. Middleware is daunting, but it is powerful

  3. Folder structures are well-defined and mandatory (mostly)

Dependency Injection

First, let's talk about DI. Are you writing a controller and need resources outside your controller, like the application's configuration, a database context instance, and a logging instance? No problem, just put them in your constructor and DI will supply them when your controller is instantiated.

private readonly MyContext _db;
private readonly string _someSetting;
private readonly ILogger<MyController> _logger;

public MyController(MyContext db, IConfiguration config, ILogger<MyController> logger)
{
    _db = db;
    _logger = logger;
    _someSetting = config["App:SomeSetting"];
}

Building a class to provide common functionality Application wide? Then you should consider building a service so that it can be injected via DI.

public interface IClock
{
    DateTime GetTime();
    DateTime GetTimeUtc();
}

public class OffsetClock: IClock
{
    private readonly ILogger<OffsetClock> _logger;
    private readonly TimeSpan _offset;

    public class OffsetClock(ILogger<OffsetClock> logger, IConfiguration config)
    {
        _logger = logger;
        _offset = TimeSpan.Parse(config["Clock:Offset"]);
        _logger.LogDebug("Offset clock instantiated with offset: {Offset}", _offset);
    }

    public DateTime GetTime()
    {
        _logger.LogDebug("Getting time");
        return DateTime.Now.Add(_offset);
    }

    public DateTime GetTimeUtc()
    {
        _logger.LogDebug("Getting time in UTC");
        return DateTime.UtcNow.Add(_offset);
    }
}

Notice that the above example contained both an implementation class (OffsetClock) and interface (IClock). The implementation class uses other classes which themselves are supplied to the constructor via DI.

Now that you have a service, you need to tell DI about your service. But before you do, you should consider the lifetime of your shiny new service. Is each method call independent of each other? Then perhaps you want your new service to be a Singleton service. Should a single instance of your service be used by everything which needs it when all working for the same HTTP request, or other unit of work? Then you might consider a Scoped service. And finally if a new instance should be created every single time it is needed, then you need a Transient service.

Setting up your service is done when building the ASP.NET WebApplication, typically in Program.cs. WebApplicationBuilder.Services is the service collection where you can add services. There are a lot of different ways to do so, but let's look at just three for now:

  • AddTransient<T,U>() - Adds a transient service

  • AddScoped<T,U>() - Adds a scoped service

  • AddSingleton<T,U>() - Adds a singleton service

Looking back at our OffsetClock, it could easily be a singleton service since it doesn't maintain any pertinent information between calls.

builder.AddSingleton<IClock, OffsetClock>();

Now, the first time a component needs an IClock instance, an OffsetClock instance will be created. Every time after that when a component needs an IClock instance, this same instance of OffsetClock will be provided.

Middeware

The ASP.NET Middleware pipeline is amazing. It truly is. But it is dangerous, too. I wish I had understood it like I understand it now when I first started writing ASP.NET Core applications back in the ASP.NET Core 2.1 days. You wouldn't believe the spaghetti I wrote because I just couldn't make the middleware do what I wanted it to do.

Everything under the sun will show you the diagram of Middleware #1 passing the request to Middleware #2 which passes the request to Middleware #3, which completes the request. Then Middleware #3 passes the response to Middleware #2 which passes the response to Middleware #1 before returning the response back to the client.

But here's the important thing, and it is simple, but IMPORTANT. Honestly, I don't know why everywhere you see that middleware diagram you don't also see this statement. The ORDER of adding middleware to the pipeline when building the WebApplication defines the ORDER of the execution of the middleware.

So if you have an application which already had Authentication via app.UseAuthentication() and now want to add roles for users and thus add Authorization functionality via app.UseAuthorization(), then it is very important that you place the call to app.UserAuthorization() AFTER the call to app.UseAuthentication(). After all, if you try to authorize something before you know who the user is via authentications, well that won't work, will it?

💡
Honestly, that is a very good example. I know a web app out in the wild right now where with methods like "IsInRole(...)" and "IsAllowed(...)". Yep, I know it because I wrote it. Oh, I hope I don't have to do maintenance on it any time soon!

Anyway, middleware is important. It is powerful. Want to add logging of raw POST data and raw responses? Middleware could do that for you. Want to make use of more functionality provided by ASP.NET, such as adding MVC controllers to Razor Pages? It can do that too. But be mindful of the ordering - it is critically important.

Folder Structures

Many folders are mandated by ASP.NET out of the box, such as:

  • /wwwroot contains static files which are served directly

  • /Controllers contains controllers

  • /Views contains MVC views

  • /Pages contains Razor Pages (which I highly recommend using whenever able)

  • /Areas contains trees of Razor Pages and MVC Views located under a subfolder which equates to an HTTP path folder

I strongly suggest adopting these other root folders as well:

  • /Model contains model classes, possibly in a tree when there are multiple model sets (e.g. One for a database, one for API DTOs, etc.)

  • /Middleware contains middleware classes, or classes used by generic middleware

  • /Services contains DI services