• Korzh.com Blog
  • /
  • ASP.NET Core default project structure explained (part 2)

ASP.NET Core default project structure explained (part 2)

Startup class

As we already mentioned in the first article, the Startup class is the entry point for all initialization code in your application. To make it short, the Startup defines what exactly your application will do and how exactly it will work.

By default, any Startup class in the ASP.NET Core application includes three main parts:

  • The constructor where you can define some internal variables, set some configuration settings, or perform application-wide initializations. For example, we suggest our users set here their license keys for our EasyQuery library.

  • ConfigureServices method. Here we register all necessary services in the DI (dependency injection) container and define the configuration settings for some of them. In case you are not aware of what Dependency Injection is, here is an article on our blog that explains this concept in simple words.

  • Configure method. This is the stage when you set up the middleware pipeline for your project.

Both ConfigureServices and Configure methods are called automatically while the application starts. You just need to define the services and the middlewares there correspondingly.

Now, let’s take a look at each of these sections more in detail.

Startup class constructor

The default ASP.NET Core project template contains only the following one line of code in the constructor:

        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

It just saves in the class property the configuration object passed here by the DI container, so we can use it in other methods. In the first article we have already considered the mechanism of the configuration working in ASP.NET Core applications.

ConfigureServices method

As it was mentioned before, the ConfigureServices method contains the function calls that register different application services in the DI container. It can be authentication/authorization services, MVC or Razor Pages engine, a service for working with DB, or any other possible service.

The only parameter of the ConfigureServices method is the services of the IServiceCollection type.

IServiceCollection is just a list of ServiceDescriptor items. Each of them maps some type (usually an interface) to a particular implementation of this type.

According to the interface, we can use only such usual IList or ICollection functions like Add, AddRange, Remove, and other methods. However, as you have already noticed, ConfigureServices contains such calls as services.AddDbContext, services.AddRazorPages, and similar. You might ask, “How is it possible?” The answer is extension methods. Each sub-system used in our project (like Entity Framework Core or MVC engine) defines an extension method (sometimes even a few) for the IServiceCollection interface to simplify the registration in the DI and configuration of the services available in that sub-system.

NB: Please note that the order of the services you register in the ConfigureServices method is not important (except in some very special cases). This method is executed only once upon your application start.

So, let's take a closer look at the default ConfigureServices’ content.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddRazorPages();
}

So, there are only three calls there: AddDbContext, AddDefaultIdentity, and AddRazorPages. Let’s inspect each of them separately.

AddDbContext

This method is provided by the Entity Framework Core framework. It registers and configures our database context. The Actiondelegate passed in the parameter allows us to define the parameters of this DbContext. For example, with UseSqlServer call, we tell our DbContext to use a particular type of connection (SqlConnection) with a particular connection string. The connection string itself is taken from the configuration so it can be easily changed in Development or Production environments. You can find the default connection string in the appsettings.json configuration file, which we already analyzed in the first article.

A few words about the “Builder” pattern

You might notice the interesting way of passing the settings for the DbContext. Instead of providing an object with options, we use a procedure (Action) with one-parameter options of the DbContextOptionsBuilder type.

That’s because, to construct the DbContext settings properly, Entity Framework Core uses the Builder Pattern. This pattern is helpful when you need to perform several steps to construct some complex objects and simply defining a class or structure is not enough. For example, when you need to call some methods depending on the settings in the configuration. If you have a case like ours then you call UseSqlServer().

There is one more reason we use a delegate (function) instead of a plain object. There might be a case when you will not need that service (DbContext) at all. For example, your web app has some actions that do not require database access. The DbContext object will never be created for such requests, and so we will not need its options. The good thing about a delegate is that it can be called exactly when you need to construct the necessary object.

I paid so much attention to this pattern because it is used quite often, especially in the process of registering services in DI (and we will use that for the next function call as well).

AddDefaultIdentity

The second function call in the ConfigureServices was added here because of the Authentication option we turned on when creating our project. It registers a bunch of services for authentication and authorization tasks such as IUserStore, IRoleStore, IPasswordHasher, UserManager, SignInManager, and many others. All of them are part of the ASP.NET Core Identity sub-system.

This method also uses the Builder Pattern, but in a slightly different way. Instead of passing the builder in the delegate’s parameter, this function returns an IdentityBuilder object, so you can call other functions with simple chaining.

The only function called there is AddEntityFrameworkStores. It tells ASP.NET Identity where to store all those entities required for auth tasks (users, roles, claims, etc.). As the name of the method suggests, we will use our Entity Framework services for these tasks. The type of parameter in this method (between < .. >) defines the DbContext class used to store all auth objects.

AddRazorPages

The last function call in the ConfigureServices adds the services necessary for Razor Pages engine working. It appears here because of the template we chose at the beginning. If we had selected a template with Model-View-Controller options at that point, we would have had something like services.AddMvc() here.

Configure method

The last method in Startup class defines the middleware pipeline of our ASP.NET Core app. If you don’t know what a middleware pipeline is, learn it, since it defines how a web application works and how it processes every HTTP request.

ASP.NET Core middleware pipeline

Unlike the services registered in ConfigureServices (remember, their order is not important), the order of middlewares defined in Configure has crucial significance. As you can see from the picture above, the first defined middleware is called for each request while every next one in the pipeline processes only those requests that were not processed by the middlewares defined before it.

So at the beginning of the pipeline, we need to place the middlewares that are necessary for auxiliary tasks (like logging or authentication) and that don’t consume a lot of memory and processing time.

Let’s take a look at the default implementation of this method for the chosen project template:

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment()) {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
            }
            else {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints => {
                endpoints.MapRazorPages();
            });
        }

So, let’s go through the middlewares defined here one by one.

Exception handling and security transport

The first lines define different middlewares for Development and Production modes. If we are in Development mode, we define the middlewares that catch all exceptions in the pipeline and show a special page with extra information about the error (exception message, stack trace, etc.).

It’s possible because (as you can see from the image above) the processing of the request is returned to the first middleware in the pipeline before being sent back to the client.

In Production mode, we also catch all exceptions and then re-execute the request with the specified path (‘/Error’ in our case).

The second call executed only in Production is UseHsts. It adds a middleware that implements HTTP Strict Transport Security Protocol.

The next in the pipeline is HTTPS redirection middleware (app.UseHttpsRedirection). The name is self-explanatory. It just redirects all HTTP requests to HTTPS ones. These two middlewares (app.UseHsts and app.UseHttpsRedirection calls) are added because of the Configure for HTTPS option we turned on (actually, it was turned on by default) during the project creation.

Processing static files (UseStaticFiles)

The next middleware (added by the app.UseStaticFiles() call) takes care of all static files. Simply put, if the application gets a request for some .js, .css, or an image file (.png, .jpg, etc.), this middleware looks for a file with the requested name inside the wwwroot folder and returns it back if found. If not, it sends back a response with a 404 status code (“not found”).

There are overloads of UseStaticFiles that allow us to define another root folder for static files and set some other options.

Routing (UseRouting and UseEndpoints)

The next pair of middlewares are, maybe, the most important ones in the pipeline, since they define the routing for all other endpoints in your web app.

In simple words, they match a particular request to a particular endpoint, a piece of executable code that handles the request.

So, how exactly does it work?

In the Configure method we call app.UseRouting() to add EndpointRoutingMiddleware to our pipeline.

After that, we call app.UseEndpoints() to add EndpointMiddleware to the pipeline and define the endpoints. Each endpoint is an object that contains (as mentioned above) a delegate (so, a piece of code) that actually handles the request, plus some metadata (like an authorization policy).

We can use such extension methods as MapGet, MapPost, and others to add an endpoint that matches a particular request path or a path template. For example, the following pattern /something/{path*}, will be matched for all requests started with /something/.

Additionally, other services can define their own extension functions that are used to create necessary endpoints. For example, you can use the MapRazorPages function for Razor Pages or MapHub for SingleR. In our EasyData library we defined MapEasyData function that adds an endpoint for the API that processes all CRUD operations.

When our web app gets a new request (and it’s not processed by any middleware defined before UseRouting), the EndpointRoutingMiddleware matches it to some endpoint. So, one endpoint among all defined at the application start is becoming selected at that moment.

Then EndpointMiddleware actually calls the endpoint’s delegate to handle the request. Other middlewares that are added after app.UseRouting() but before app.UseEnpoints() can see which endpoint was selected by EndpointRoutingMiddleware and can change something (e.g., to apply an authorization policy) before EndpointMiddleware dispatches to the selected endpoint.

Authentication/Authorization (UseAuthentication, UseAuthorization)

These two middlewares are added in the Configure because of the Authentication option we turned on when we created our project. As already mentioned above, they already know which endpoint is selected and so can add some additional identity-related information to the request’s context (like filling its User property) or redirect to the “sign in/sign up” pages if the current request is anonymous but the selected endpoint requires authorization.

Wrapping Up

So, in this article, I tried to cover all pieces of one of the most-used ASP.NET Core application templates and figure out why they are used and how they work (from a very general point of view).

I hope this information will help you better understand the internal mechanism of ASP.NET Core framework and make your code clear and effective.

Thanks for the reading, and take care!

Subscribe

Subscribe to blog's updates. You will get notifications about new posts, and only them.

Recent posts

Supported by EasyQuery

EasyQuery is a multi-platform solution that lets you embedding ad-hoc reporting or advanced search functionality to your application