We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept
Card image cap

Exploring ASP.NET Core Fundamentals - The Startup class

ASP.NET Core  • Posted one month ago

A Startup class is a simple C# class used to configure and construct an application request pipeline. It is one of the very first components which are picked up and executed by the aspnetcore runtime during application bootstrapping.

We can register abstractions and their implementations for the aspnetcore container to inject the instances of these implementations in place of the abstractions wherever requested. The collection on which these classes are registered is called as a service pipeline and the classes registered in this manner are called as "services".

We can also define a series of components which are to be executed in order while processing an incoming request. The resultant is called as a request pipeline and every single request passes through each and every component registered in the request pipeline before transforming into a response. These components which are tied to the request pipeline are called as "Middlewares".

Generally, the class comes along with the boilerplate code for all the web templates (webapi, mvc) provided in dotnetcore cli. It is linked up to the application host at the main method while building the web host using the UseStartup() method, which takes a type parameter of the Startup class.

Example:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

Anatomy of a Startup Class:

A typical startup class consists of two methods along with an optional injectable constructor. The dotnetcore host allows injecting only the below three services into the constructor by default:

IWebHostEnvironment - an interface for accessing the hosted application file system (typically the contents of the wwwroot folder), using which we can access the file system contents and perform I/O operations. This interface is introduced starting from aspnetcore3.0 and replaces the existing IHostingEnvironment used in aspnetcore2.2 and below.

IConfiguration - an interface for accessing the application configuration from a variety of sources such as appsettings json, keyvaults, environmental variables and so on. It is a dictionary of keys and values with the keys being the key names used in appsettings and Values relating to the values contained for the respective keys. appsettings can also contain groups of configurations called as Sections. These are accessed by GetSection("") method of the IConfiguration instance. In a similar way, the ConnectionStrings section of the appsettings json can be accessed via the IConfiguration using the method GetConnectionString("").

Example: configuration.GetConnectionString("DefaultConnection")

ILoggerFactory - an interface for logging framework provided by dotnetcore. It can be used to write logs onto various sources, like a database, file, or cloud logging providers like Azure Application Insights or AWS CloudWatch.

The loggerFactory segregates logging traces into Information, Debug and Error; which defines the type of log being made - Verbose, Debug purpose or Error logs respectively.

We can also maintain a separate Startup class for each environment - for Development, Staging or Release. A Startup class for a specific environment must be appended with the environment name - for example, Startup class for a Development environment would be StartupDevelopment.cs and the standalone Startup class represents a Startup class for Production / Release.

The Methods:

As mentioned before, the Startup class contains two methods: ConfigureServices() and Configure().

ConfigureServices(IServiceCollection services) :

In this method, we register classes as services to be injected by the container whenever some other component requests for an instance. We register a class as a service along with its abstraction and the container injects the instance of the concrete class in place of the abstraction wherever requested. This mechanism is called as Dependency Injection which is a phenomenon following the principle of Dependency Inversion.

The ConfigureServices method takes in a single parameter of type IServiceCollection, which is used to create and add new services. The services are added onto the pipeline using the Use<> method onto the IServiceCollection instance. Based on how a service is registered, the lifetimes for which the service object is persisted in the container varies.

Example:


public class Startup
{
    private readonly IWebHostEnvironment _env;
    private readonly IConfiguration _config;
    private readonly ILoggerFactory _loggerFactory;

    public Startup (
        IWebHostEnvironment env, 
        IConfiguration config, 
        ILoggerFactory loggerFactory)
    {
        _env = env;
        _config = config;
        _loggerFactory = loggerFactory;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        var logger = _loggerFactory.CreateLogger<Startup>();

        if (_env.IsDevelopment())
        {
            // Development service configuration
            logger.LogInformation("Development environment");
        }
        else
        {
            // Non-development service configuration
            logger.LogInformation($"Environment: {_env.EnvironmentName}");
        }

        Console.WriteLine("Configuration for MyKey: " + _config["MyKey"]);

        services.AddSingleton<IMyService, MyService>();

        services.AddDbContext<DatabaseContext>((options) => {
            options.UseSqlServer(
                  Configuration.GetConnectionString("DefaultConnection"));
        });

        services.AddMvc();    
    }
}

In the above code sample, we can see that the ConfigureServices can be used to define predefined services like Mvc, Routing, Database, Authentication and so on and can also be used to register user-defined services with varying lifetimes as the singleton, transient or scoped services.

Configure(IApplicationBuilder app):

The configure() method configures an application pipeline and defines the steps a request need to pass through, before reaching the endpoint. These stages are called middlewares and each Middleware is configured to execute a specific functionality and hold a single responsibility. The order of execution of these middlewares can be varied by the order in which they are added to the request pipeline within the configure method.

The configure method takes in a parameter of type IApplicationBuilder, and we can also pass the services which are defined in the ConfigureServices method as additional parameters. The IApplicationBuilder instance is passed on to the Configure method by the Hosting and not registered to the service container.

The components which are to be added onto the pipeline are added to the IApplicationBuilder instance using the Use<> method. The dotnetcore library also provides specific methods for adding predefined middlewares such as Routing, Https Redirection, Authentication, Authorization, Exception Handling and so on. Any middleware component added to the pipeline using the Use<>() method takes a delegate function of two parameters httpContext and Task. The consequent middlewares are invoked from one another by using the Invoke method of the RequestDelegate type, similar to a Chain of Responsibility.

Example:

// the services registered using the ConfigureServices method 
// can be injected into the Configure method
// Use this method to configure the HTTP request pipeline.
public void Configure (
      IApplicationBuilder app, 
      IWebHostEnvironment env, 
      IMyService service)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

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

    // defining a simple middleware component
    // takes two parameters of type HttpContext and a Task
    app.Use((httpContext, next) => {
    	//custom logic can come in here
    });

    app.UseMvc();
}

If a service is not injected from the parameter of Configure method, we can still request the container to provide an instance of the required service by calling GetRequiredService() method as below:

Example:

public void Configure(IApplicationBuilder app) 
{
	// the IWebHostEnvironment instance can be 
    // created ready to use without injection
	using (var scope = app.ApplicationServices.CreateScope())
    {
        var service = scope.ServiceProvider
            .GetRequiredService<IWebHostEnvironment>();
    }   
	....
}

Hooking up Startup Configurations to the BuildHost directly:

If using a startup class is not the case, then we can define configureservices and configure methods at the Host building step itself using the convenience methods provided.

Example:


public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
                webBuilder.ConfigureServices(services =>
                {
                    // register services here
                }).Configure(app =>
                {
                    // tag to request pipeline here
                });
            });
}

In this way, the Startup class in an aspnetcore application provides us with options to register services and construct the request pipeline with desired components for building powerful web applications.