Add minimal APIs to the Startup class

In the previous article of this series we discussed integrating ASP.NET Core Identity with JWT and minimal APIs. Minimal APIs are introduced as a part of ASP.NET Core 6.0. All the new project templates in Visual Studio 2022 use the new way of application startup. However, you might be migrating an older project to ASP.NET Core 6.0 and you may want to continue using the Startup class based application initialization. What if you want to create minimal APIs in such cases? Can they be defined in the Startup class? That's what we are going to discuss in this part of this multipart article series.

Before we go into the details of creating minimal APIs in the Startup class, let's understand the difference between the app startup in older project templates and in the new project templates. Consider the following code from an ASP.NET Core 5 project's Program.cs.

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();
            });
}

And also take a look at the new Program.cs below (comments and code not related to our discussion has been removed for clarity):

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseHttpsRedirection();
var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", 
    "Mild", "Warm", "Balmy", "Hot", 
    "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () => {...});
app.Run();

We won't focus much on C# language features such as top-level statements here. More important for us is to note the difference between the namespaces, classes, and the interfaces used in the earlier templates and the new project templates.

The application initialization code before ASP.NET Core 6.0 uses classes and interfaces residing in the Microsoft.Extensions.Hosting namespace. The CreateDefaultBuilder() method of Host class returns an IHostBuilder object. The ConfigureWebHostDefaults() method specifies the startup class to be used. The Build() method called on the IHostBuilder returns an IHost object. Finally, the Run() method starts the web application.

On the other hand, the new project templates use classes residing in the Microsoft.AspNetCore.Builder namespace. The CreateBuilder() method of the WebApplication class returns a WebApplicationBuilder object. The Build() method called on the WebApplicationBuilder returns a WebApplication object. Finally, Run() is called on the WebApplication to start the web application.

When you want to migrate a project build using an older version (say 5.0) to ASP.NET Core 6, you can either stick to the older way of app initialization or use the new Startup-less way of app initialization. If you are trying to quickly migrate the app, chances are that you will stick with the Startup class based initialization. If you want to add some functionality to the app post migration, you might be curious to know whether it can can be done through minimal APIs. Luckily, you can easily create minimal APIs in the Startup class also. That's what we are going to do in the remainder of this article.

Open the same project that we have created in the previous parts of this article series. Then add a new class called Startup.cs in the project root folder. You can either add a plain C# class or you can use Startup class template from the Add New Item dialog as shown below: 

Before using the Startup class for app initialization, we will move the code from Program.cs into the three methods of the Startup class - constructor, ConfigureServices(), and Configure(). Here is  the skeleton of the Startup class for your understanding.

namespace MinimalAPI;

public class Startup
{
    public Startup(IConfiguration config)
    {
    }

    public void ConfigureServices(IServiceCollection services)
    {
    }

    public void Configure(IApplicationBuilder app, 
                          IWebHostEnvironment env)
    {
    }
}

Now let's add code that will help us read the application configuration. Modify the Startup() constructor like this:

private readonly IConfiguration config;

public Startup(IConfiguration config)
{
    this.config = config;
}

The IConfiguration object will be used to read connection string and JWT settings.

Next, we will move all the "Add" methods to the ConfigureServices() method. The following code shows the completed CofigureServices() for your convenience.

public void ConfigureServices(IServiceCollection services)
{
    var connectionString = config.GetConnectionString("AppDb");

    services.AddDbContext<AppDbContext>(o => 
o.UseSqlServer(connectionString));

    services.AddDbContext<AppIdentityDbContext>
(o => o.UseSqlServer(connectionString));

    var contact = new OpenApiContact()
    {
        Name = "FirstName LastName",
        Email = "user@example.com",
        Url = new Uri("http://www.example.com")
    };

    var license = new OpenApiLicense()
    {
        Name = "My License",
        Url = new Uri("http://www.example.com")
    };

    var info = new OpenApiInfo()
    {
        Version = "v1",
        Title = "Swagger Demo Minimal API",
        Description = "Swagger Demo Minimal API Description",
        TermsOfService = new Uri("http://www.example.com"),
        Contact = contact,
        License = license
    };


    var securityScheme = new OpenApiSecurityScheme()
    {
        Name = "Authorization",
        Type = SecuritySchemeType.ApiKey,
        Scheme = "Bearer",
        BearerFormat = "JWT",
        In = ParameterLocation.Header,
        Description = "JSON Web Token based security",
    };

    var securityReq = new OpenApiSecurityRequirement()
            {
                {
                        new OpenApiSecurityScheme
                        {
                            Reference = new OpenApiReference
                            {
                                Type = ReferenceType.SecurityScheme,
                                Id = "Bearer"
                            }
                        },
                        new string[] {}

                }
            };

    services.AddEndpointsApiExplorer();

    services.AddSwaggerGen(o =>
    {
        o.SwaggerDoc("v1", info);
        o.AddSecurityDefinition("Bearer", securityScheme);
        o.AddSecurityRequirement(securityReq);
    });


    services.AddIdentity<IdentityUser, IdentityRole>()
                    .AddEntityFrameworkStores<AppIdentityDbContext>()
                    .AddDefaultTokenProviders();

    services.AddAuthentication(o =>
    {
        o.DefaultAuthenticateScheme = 
JwtBearerDefaults.AuthenticationScheme;
        o.DefaultChallengeScheme = 
JwtBearerDefaults.AuthenticationScheme;
        o.DefaultScheme = 
JwtBearerDefaults.AuthenticationScheme;
    }).AddJwtBearer(o =>
    {
        o.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = false,
            ValidateIssuerSigningKey = true,
            ValidIssuer = config["Jwt:Issuer"],
            ValidAudience = config["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey
(Encoding.UTF8.GetBytes(config["Jwt:Key"]))
        };
    });
    services.AddAuthorization();
}

We have already discussed these methods in the previous parts of  this article series and hence I am not going to discuss them again.

Finally, this is your Configure() method with various "Use" method calls.

public void Configure(IApplicationBuilder app, 
IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json",
        "Swagger Demo Minimal API v1");
    });
}

Now comes the important step. We will add various "Map" method calls to the Startup class.

You might be wondering where to add them? That's because the IApplicationBuilder parameter of Configure() doesn't have any MapGet() or MapPost() methods. The MapGet(), MapPost(), MapPut(), and MapDelete() methods are defined in the EndpointRouteBuilderExtensions class from the Microsoft.AspNetCore.Builder namespace. To use these methods you need to define endpoints using the UseEndpoints() call in the Configure() method.

The following code shows how these methods can be written in the UseEndpoints() call.

app.UseEndpoints(e => {

    e.MapGet("/minimalapi/employees", 
[Authorize](AppDbContext db) =>
    {
        return Results.Ok(db.Employees.ToList());
    });

    e.MapGet("/minimalapi/employees/{id}", 
[Authorize](AppDbContext db, int id) =>
    {
        return Results.Ok(db.Employees.Find(id));
    });

    e.MapPost("/minimalapi/employees", 
[Authorize](AppDbContext db, Employee emp) =>
    {
        db.Employees.Add(emp);
        db.SaveChanges();
        return Results.Created($"/minimalapi/
employees/{emp.EmployeeID}", emp);
    });

    e.MapPut("/minimalapi/employees/{id}", 
[Authorize](AppDbContext db, int id, Employee emp) =>
    {
        db.Employees.Update(emp);
        db.SaveChanges();
        return Results.NoContent();
    });

    e.MapDelete("/minimalapi/employees/{id}", 
[Authorize](AppDbContext db, int id) =>
    {
        var emp = db.Employees.Find(id);
        db.Remove(emp);
        db.SaveChanges();
        return Results.NoContent();
    });


    e.MapPost("/minimalapi/security/getToken", 
[AllowAnonymous]async (UserManager<IdentityUser> userMgr, 
User user) =>
    {
        var identityUsr = await userMgr.FindByNameAsync
(user.UserName);

        if (await userMgr.CheckPasswordAsync
(identityUsr, user.Password))
        {
            var issuer = config["Jwt:Issuer"];
            var audience = config["Jwt:Audience"];
            var securityKey = new SymmetricSecurityKey
        (Encoding.UTF8.GetBytes(config["Jwt:Key"]));
            var credentials = new SigningCredentials
(securityKey, SecurityAlgorithms.HmacSha256);

            var token = new JwtSecurityToken(issuer: issuer,
                audience: audience,
                signingCredentials: credentials);

            var tokenHandler = new JwtSecurityTokenHandler();
            var stringToken = tokenHandler.WriteToken(token);

            return Results.Ok(stringToken);
        }
        else
        {
            return Results.Unauthorized();
        }
    });


    e.MapPost("/minimalapi/security/createUser", 
[AllowAnonymous] async (UserManager<IdentityUser> userMgr, 
User user) =>
    {
        var identityUser = new IdentityUser()
        {
            UserName = user.UserName,
            Email = user.UserName + "@example.com"
        };

        var result = await userMgr.CreateAsync
(identityUser, user.Password);

        if (result.Succeeded)
        {
            return Results.Ok();
        }
        else
        {
            return Results.BadRequest();
        }
    });
});

As you can see, we use IEndpointRouteBuilder to define various endpoints using MapGet(), MapPost(), MapPut(), and MapDelete() methods. These methods are identical to what we developed in the previous parts.

This completes the Startup class. Since we moved the code from Program.cs to Startup.cs, at this stage the Program.cs will contain only entity classes and DbContext classes (Employee, User, AppDbContext, and AppIdentityDbContext).

Now add the following code at the top of the Program.cs.

var builder = Host.CreateDefaultBuilder(args);
builder.ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
var app = builder.Build();
app.Run();

Since we want to use Startup class based initialization, we make use of Host, IHostBuilder, and IHost. Notice how the Startup class is specified using the ConfigureWebHostDefaults() method.

Run the application by hitting F5.

The following figure shows a successful run of the application with Startup class in place.

You can now test CRUD functionality and JWT authentication as before.

So far in in this article series we have added all the "Map" calls either in the Program.cs or in the Startup.cs at one place. If we have dozens of endpoint definitions at one place it can be tedious to work with them. Can we organize them in a better way? Can we club them based on their purpose? We will discuss some possible approaches to organizing minimal APIs in the next part of this article series.

That's it for now! Keep coding!!


Bipin Joshi is an independent software consultant and trainer by profession specializing in Microsoft web development technologies. Having embraced the Yoga way of life he is also a yoga mentor, meditation teacher, and spiritual guide to his students. He is a prolific author and writes regularly about software development and yoga on his websites. He is programming, meditating, writing, and teaching for over 27 years. To know more about his private online courses on ASP.NET and meditation go here and here.

Posted On : 22 December 2021


Tags : ASP.NET ASP.NET Core MVC .NET Framework C# Visual Studio