Monday, April 18, 2016

How does startup.cs in ASP.NET Core actually work under the hood


All about conventions

The startup class is the entrypoint to your ASP.NET application. But what does actually happen when you run your code. If you break down a typical Startup.cs file the layout will be something similar to the code below. Initially the Startup object is created (constructor). Then ConfigureServices is called by the runtime (if the method exist). After the ConfigureServices method is completed, the Configure method is executed before the application is finally started with the Main() method. The reason for using “public static void main()” method is due to cross-plattform compatibility. All in all this is pretty straightforward, but then there is dependency injection. 

namespace MFAEvent
{
    public class Startup
    {
        /* Constructor for the Startup class w/arguments */
        public Startup(IHostingEnvironment env, IApplicationEnvironment appEnv)
        {...}

        /* This gets called by the runtime. Adds services to the container.*/
        public void ConfigureServices(IServiceCollection services)
        {...}

        /* Configure is called after ConfigureServices is called.*/
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {...}

        /* This is the entrypoint for the application. */
        public static void Main(string[] args) => WebApplication.Run<Startup>(args);
    }
}

ConfigureServices is optional

The runtime will expect to find the Startup class, but the ConfigureServices method is optional. If you don't include this method the runtime will just carry on with Configure using the default set of services. Another important fact is that ConfigureServices will only handle the IServiceCollection parameter while Configure can take a whole range of different parameters. 


An example of Dependency Injection 

Injecting Swagger

As an example of how dependency injection is implemented in Configure and ConfigureServices we will see how Swagger is added to the pipeline. 

ASP.NET Core uses the notation of dependency injection. DI is a software design pattern common in software engineering. A Wiki description is available here. To implement a dependency in ASP.NET Core you basically need to perform three actions.

  • Add the package where the service exist.
  • Configure the dependency in ConfigureServices method.
  • Use the dependency in the Configure method.


Let’s say we would like to add Swagger (add the Nuget packages below) to the dependency pipeline.

Note: Swagger is only used to illustrate the example with something else than Mvc or Identity commonly explained in several posts. Swagger is a package that will automatically create an interface to test and describe your API methods when accessing /site/swagger.



Add the Swashbuckle Nuget packages

First of all you need to add the Nuget packages for Swagger.

  "Swashbuckle.SwaggerUi": "6.0.0-rc1-final",
    "Swashbuckle.SwaggerGen": "6.0.0-rc1-final",



Add Swagger to the DI pipeline in ConfigureServices

Secondly you need to add Swagger to the DI pipeline in the ConfigureServices method. Use the services object passed to the method and Intellisense should be able to provide you a list of the available services. If AddSwaggerGen is not there you should verify that you have actually included the Swagger Nuget package to your project. 




/* Adds Swagger to the Pipeline. Swagger adds Rest API functionality. */
services.AddSwaggerGen();



Add Swagger to the app in the Confihire method

Finally you add Swagger to the app object in the Configure method.



/* Adds Swagger for automatic API descriptions. */
app.UseSwaggerGen();
app.UseSwaggerUi();


Now, if you have some Web APIs in your application you should be able to go to http://<app>/swagger and see a list of all your API methods and also be able to test them. 





What is going on in the constructor for the Startup class?



/* Constructor for the Startup class w/arguments */

public Startup(IHostingEnvironment env, IApplicationEnvironment appEnv)

{

       /* Setup configuration sources.*/

       var builder = new ConfigurationBuilder()

           .SetBasePath(appEnv.ApplicationBasePath)

           .AddJsonFile("appSettings.json")

           .AddEnvironmentVariables();

       if (env.IsDevelopment())
       {
         /* Read the configuration keys from the secret store. Details can be found here.
               http://go.microsoft.com/fwlink/?LinkID=532709 */
            builder.AddUserSecrets();
       }
       builder.AddEnvironmentVariables();

       /* Assign key/value config pairs to the public Configuration object */
       Configuration = builder.Build();
}



Some references on the topic





The entire Startup.cs example

The sample below is an extraction from the event handling system used throughout this blog. This means that several of the features in the code below requires additional coding to be usefull. 


using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.Data.Entity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.PlatformAbstractions;
using Microsoft.Extensions.Logging;
using MFAEvent.Models;
using MFAEvent.Services;
using System.Security.Claims;

namespace MFAEvent
{
    /* This class should correspond to the values in appSettings.json */
    public class AppSettings
    {
        public int DefaultPageRefresh { get; set; } = 60;
        public string Title { get; set; } = "MFAevent";
       
    }


    public class Startup
    {

        public IConfiguration Configuration { get; set; }

        /* Constructor for the Startup class w/arguments */
        public Startup(IHostingEnvironment env, IApplicationEnvironment appEnv)
        {
            /* Setup configuration sources.*/
            var builder = new ConfigurationBuilder()
                .SetBasePath(appEnv.ApplicationBasePath)
                .AddJsonFile("appSettings.json")
                .AddEnvironmentVariables();

            if (env.IsDevelopment())
            {
                /* Read the configuration keys from the secret store. Details can be found here.
                   http://go.microsoft.com/fwlink/?LinkID=532709 */
                builder.AddUserSecrets();
            }
            builder.AddEnvironmentVariables();

            /* Assign key/value config pairs to the public Configuration object */
            Configuration = builder.Build();
        }


        /* This method gets called by the runtime. It is used to add services to the container.*/
        public void ConfigureServices(IServiceCollection services)
        {

            /* Add Entity Framework to Pipeline. DbContext and SQL services. */
            services.AddEntityFramework()
                .AddSqlServer()
                .AddDbContext<ApplicationDbContext>(options =>
                    options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]));

            /* Add Identity services to the Pipeline. */
            services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();
               

            /* Add ApplicationSettings to the application */
            services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));

            /* Add MVC services to the Pipeline. Also handles routing */
            services.AddMvc();

            /* Adds Swagger to the Pipeline. Swagger adds Rest API functionality. */
            services.AddSwaggerGen();

            /* Register application services. */
            services.AddTransient<IEmailSender, AuthMessageSender>();
            services.AddTransient<ISmsSender, AuthMessageSender>();

            /* NOT VERIFIED - Adds SendGrid Package and functionality. */
            services.Configure<AuthMessageSenderOptions>(Configuration);
        }


        /* Configure is called after ConfigureServices is called.*/
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            /* Configures logging by reading settings from appSettings.json */
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));

            /* Add the following to the HTTP request pipeline only in development.*/
            if (env.IsDevelopment())
            {
                app.UseBrowserLink();
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage(options =>
                {
                    options.EnableAll();
                });
            }
            else {
                /* Add Error handling middleware which catches all application specific errors and sends the request to the following path or controller action.*/
                app.UseExceptionHandler("/Home/Error");

                /* For details on creating database during deployment see
                   http://go.microsoft.com/fwlink/?LinkID=615859 */
                try
                {
                    using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>()
                        .CreateScope())
                    {
                        serviceScope.ServiceProvider.GetService<ApplicationDbContext>()
                             .Database.Migrate();
                    }
                }
                catch { }
            }

            // NEW
            app.UseIISPlatformHandler(options => options.AuthenticationDescriptions.Clear());

            /* Add static files to the request pipeline.*/
            app.UseStaticFiles();

            /* Add authentication services to the request pipeline.*/
            app.UseIdentity();

            /* Add Facebook authentication to the pipeline */
            app.UseFacebookAuthentication(options =>
            {
                //these should be managed with user-secret cmd (moved away from here)
            });

            /* Add MVC and RouteMaps to the request pipeline.*/
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });

            /* Adds Swagger for automatic API descriptions. */
           app.UseSwaggerGen();
           app.UseSwaggerUi();

            //await CreateRoles(serviceProvider);

            /* Adds some data to an empty database. */
            SampleData.Initialize(app.ApplicationServices);

            /* Create a catch-all response */
            app.Run(async (context) =>
            {
                var logger = loggerFactory.CreateLogger("Catchall Endpoint");
                logger.LogInformation("No endpoint found for request {path}", context.Request.Path);
                await context.Response.WriteAsync("No endpoint found - try /Home.");
            });
        }

        /* This is the entrypoint for the application. */
        public static void Main(string[] args) => WebApplication.Run<Startup>(args);

    }

}