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!!