Geek Culture
Published in

Geek Culture

End to End Project With Minimal API In ASP.NET Core 6.0

Hi, Today we will talk about the new Minimal API which can help us to improve our code development quality and speed in a simpler way. We will create End to End Project and try to use the benefits of minimal API and .Net 6.0

Let’s Setup Environment:

.Net 6.0 and Visual Studio 2022 are not fully released yet but you can download the final version on 8 November 2021. We will use DB First for this project so we need the “dotnet-ef” for creating DB Context and Entities from the existing SQL DB (Abys-Prod) to our project.

“Make things as simple as possible but no simpler.”

-Albert Einstein

We will create an empty web application as below:

dotnet new web -o DevNot21

After creating an empty web application we will see this simple code page. There is no Controller or any namespace nor import files.

var builder = WebApplication.CreateBuilder(args);var app = builder.Build();app.MapGet("/", () => "Hello World!");app.Run();

When I first saw the minimal API, I thought Welcome to the NodeJS Express :) It is much more readable and the whole thing is in one place. Minimal API is a console application. But there is no “Main()” method. There is no extra ceremony required by placing your program’s entry point in a static method in a class. When you build it, Vm IIS is up and running from the default 3000 port. You can change the default port from

  • launchSettings.json/profiles/”projectName”/applicationUrl or
  • just add “app.Urls.Add(“https://localhost:1923");” to Program.cs. But when we select the second choice, the console application does not open the chrome browser Automatically. You have to do that manually.

First New Test Service:

Model/Role: This is Role Record. We will return “List<Role>” from service. And you can see, “namespace” doesn’t need ”{“ “}” anymore :) Records are immutable new types. Records can take constructor properties like methods. We call it Positional records. These parameters are default declare init. This means when we set the properties, they can’t change after all. Records came with .Net 5.0 one year ago. This test dummy service will return, List of Role model results.

namespace DevNot21.Model;public record Role(int RoleID, string RoleName, int GroupID)
{
public bool HasRole(int access, int roleID)
{
return roleID == (access & roleID);
}
}

Program.cs/GetRoles: We will return dummy dataList<Role>” like below.

app.MapGet("/GetRoles", (Func<List<Role>>)(() => new()
{
new(1, "Admin", 1),
new(2, "User", 1),
new(3, "Worker", 1)
}));

Swagger Integration:

We will add below package from the Nuget to the project.

Add Swagger Configuration to Program.cs as below. We will add the Authentication section and Bearer Token. The important part is if you describe type as HTTP “Type = SecuritySchemeType.Http”, every time you will not have to write Bearer at the beginning of the Token on swagger.

Program.cs/Swagger:

And don’t forget to Add “UseSwagger() and UseSwaggerUI” to the app.

if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Api v1"));
}

This is the swagger form url “https://localhost:1923/swagger

Create DBLayer, Add DBContext and Entities

We will use DB First for this project. For creating DBContext and Entities, you have to download the below packages.

dotnet add package Microsoft.EntityFrameworkCore.SqlServer.Design;
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools

Declare DBConnection string to the appsettings.json as below

appsettings.json

"ConnectionStrings": {
"SQLDBConnection": "Data Source=.;initial
catalog=ABYS_PROD;Trusted_Connection=True;"
},

If you call “dotnet ef dbcontext scaffold ” command as below, DevNotContext and all Entities will be created under the Entities folder.

dotnet ef dbcontext scaffold "Server=.;Database=ABYS_PROD;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context-dir "Entities\DbContexts" --no-pluralize -c DevNotContext -f

Program.cs: We call AddDBContext() method in program.cs. So we will create DevNotContext as a static, while the project is starting up. We call it “Dependency Injection.” After all, we can directly inject DevNotContext in the constructor of any class.

program.cs/AddDbContext:

  • “builder.Configuration.GetConnectionString” : We got “SQLDBConnection” from the appsettings.json
  • “builder.Service.AddDbContext()”: We create static DevNotContext for the project.
var connectionString = builder.Configuration.GetConnectionString("SQLDBConnection");builder.Services.AddDbContext<DevNotContext>(x => x.UseSqlServer(connectionString));

GlobalUsing.cs:

In .Net 6.0, we can declare all import Libraries in one class with the “global” declare keyword. So we can manage all libraries in one place.

global using AutoMapper;
global using DevNot2021.Entities.DbContexts;
global using DevNot2021.Model;
global using DevNot2021.Services;
global using DevNot21.Model;
global using Microsoft.AspNetCore.DataProtection;
global using Microsoft.EntityFrameworkCore;
global using Microsoft.OpenApi.Models;
global using AutoMapper;
global using DevNot2021.Entities;
global using Microsoft.AspNetCore.DataProtection;
global using Microsoft.AspNetCore.Authentication.JwtBearer;
global using Microsoft.AspNetCore.Authorization;
global using Microsoft.AspNetCore.Mvc;
global using Microsoft.IdentityModel.Tokens;
global using System.Text;

Enumerable.Zip():

This feature is coming with .Net 6.0. Example scenario, there are three SQL queries below. We can combine all queries by using View or SQL Procedure. But I don’t want to write business into the DB. And also if we take these queries as a service from a third-party company, we have to combine all three queries manually and put them together into the one View Model. We will use “Enumerable.Zip”.

Service/IZipService:

We will create this ZipService. It takes three List<string> parameters. These are our three Sql Queries result lists. ZipService returns tuple “<string name, string role, long? action>”

  • name” comes from first query also first list.
  • role” comes from second query also second list.
  • action” comes from third query also third list.

We used the “yield” keyword. We returned every item one by one. So we don’t need to create any List<string> object to fill and return it.

Enumerable.Zip(list1, list2, list3)”: Combine every item in these three lists and return tuple(string, string, long?) value.

namespace DevNot2021.Services;public interface IZipService
{
IEnumerable<string> ZipResult(List<string> list1, List<string> list2, List<long?> list3);
}
public class ZipService : IZipService
{
public IEnumerable<string> ZipResult(List<string> list1, List<string> list2, List<long?> list3)
{
foreach ((string name, string role, long? action) in
Enumerable.Zip(list1, list2, list3))
{
yield return $"{name} - {role} - {action}";
}
}
}

Let’s implement ZipService to our project: We added IZipService with “Scoped”. In every new request, we will create a new one and dispose of it. If we created it as a singleton, it could be very dangerous because of the 3 list parameters. Every client could use the same 3 lists :)

builder.Services.AddScoped<IZipService, ZipService>();

“Reduce the complexity of life by eliminating the needless wants of life, and the labors of life reduce themselves.”

-Edwin Way Teale

Program.cs/GetTop5UserPermisions:

Let’s test our IZipService by writing these three SQL Queries by LINQ and combine the results with IZipService and finally return it.

app.MapGet("/GetTop5UserPermisions", async (DevNotContext context, IZipService service) =>
{
var userList = await context.DbUser.Where(du => du.IdSecurityRole != null).Select(u => $"{u.Name} {u.LastName}").Take(5).ToListAsync();
var roleList = await (from role in context.DbSecurityRole join user in context.DbUser on role.IdSecurityRole equals user.IdSecurityRole select role.SecurityRoleName).Take(5).ToListAsync(); var IdUserList = await context.DbUser.Where(du => du.IdSecurityRole != null).Select(db => db.IdUser.ToString()).Take(5).ToListAsync(); var actionList = await context.DbSecurityUserAction.Where(a => a.IdSecurityController == 2 &&
IdUserList.Contains(a.IdUser.ToString())).Select(u =>
u.ActionNumberTotal).Take(5).ToListAsync();
return service.ZipResult(userList, roleList, actionList);});
  • (DevNotContext context, IZipService service) => We added DBContext and IZipService by using Dependency Injection.
  • userList : This is top 5 User FullName List.
  • roleList: This is top 5 user’s Role List.
  • actionList: This is top 5 user’s Authorization List.
  • return service.ZipResult(userList, roleList, actionList)”: Finally, we will combine these three lists into the one result list.

AutoMapper Integration:

We donwloaded below packages from the Nuget Package manager.

We will protect the “PasswordHash” property by using AutoMapper. So We will download DataProtection Library from the NuGet Package manager as seen below.

We added “AddDataProtection()” to the builder service. We used Services.BuildServiceProvider() for creating provider on runtime. Minimal API is more pratic than the .Net 5.0 for creating objects on runtime. So later we can give the “protector” object as a parameter to the constructor of the Automapper class.

Program.cs/AddDataProtection:

builder.Services.AddDataProtection();
builder.Services.AddCors();
var serviceProvider = builder.Services.BuildServiceProvider();
var _provider = serviceProvider.GetService<IDataProtectionProvider>();
var protector =
_provider.CreateProtector(builder.Configuration["Protector_Key"]);

appsettings.json: This is the secret key of the protection class. For security, if you will encode the Protector_Key could be much better.

"Protector_Key": "DevNot2021.Model.v1",

User ViewModel: We will get “Name + LastName” as a FullName to the User ViewModel form the DBUser. Actually, we will create a record. Records are immutable types. They look like classes but have more properties.

Model/User:

namespace DevNot2021.Model;
public record User
{
public int? IdSecurityRole { get; set; }
public string Name { get; set; }
public string LastName { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public string PasswordHash { get; set; }
public string Email { get; set; }
public string Gsm { get; set; }
public long? TotalCompanyNumber { get; set; }
public bool IsDeleted { get; set; }
public bool? IsAdmin { get; set; }
public string FullName { get; set; }
}

Let’s create AutoMapper class for matching and encoding properties.

Model/DevNotMapper.cs:

  • “public DevNotMapper(IDataProtector provider)” : We got IDataProtector class in the constructor for encrypting to Password by Dependency Injection.
  • CreateMap<DbUser, User>()”: We matched “DbUser” entity to “User” ViewModel => FullName = (Name + LastName)
  • CreateMap<DbUser, User>()”: PasswordHash = provider.Unprotect(u2.PasswordHash) => We decrypted the PasswordHash property when we read data from the DBUser by using provider.Unprotect() method.
  • “CreateMap<User, DbUser>()”: From User to DBUser, actually, when we insert or update the User, we will encrypt to the “PasswordHash” property by using the provider.Protect() method.
namespace DevNot2021.Model;public class DevNotMapper : Profile
{
public DevNotMapper(IDataProtector provider)
{
CreateMap<DbUser, User>()
.ForMember(u => u.FullName, opt => opt.MapFrom(u2 => u2.Name + " " + u2.LastName))
.ForMember(u => u.PasswordHash, opt => opt.MapFrom(u2 => provider.Unprotect(u2.PasswordHash)));
CreateMap<User, DbUser>().ForMember(u => u.PasswordHash, opt => opt.MapFrom(u2 => provider.Protect(u2.Password)));
}
}

Simplicity is the ultimate sophistication.”

-Leonardo da Vinci

Let’s add AutoMapper to this builder.Services().

  • We added DevNotMapper config class with the “MapperConfiguration()” method.
  • DevNotMapper(protector)”: We added the protector class as a parameter into the DevNotMapper constructor. This is very easy on .Net 6.0. But it is not easy on .Net Core 5.0, 3.1, or older version. Because of runtime creation problem. You have to avoid Startup service injection before .Net 6.0 or you can create an Object on the program.cs before.
  • We created an autoMapper class with CreateMapper() method. After all, we can use the “autoMapper” object whenever we want in the program.cs
  • We created autoMapper as a singleton for injecting to the other class with Dependency Injection.
var mappingConfig = new MapperConfiguration(mc =>
{
mc.AddProfile(new DevNotMapper(protector));
});
IMapper autoMapper = mappingConfig.CreateMapper();
builder.Services.AddSingleton(autoMapper);

Let’s Insert a User and Check The Database:

We injected DevNotContext and IMapper with Dependency Injection. And the user is a parameter.

Not: In this post method, we don’t have use [FromBody] attribute before the user parameter.

  • “//var model = autoMapper.Map<DbUser>(user)” : We could use the “autoMapper” class, which we created at the beginning of the program.cs directly too. So if you want, you don’t have to inject IMapper.
  • “var model = mapper.Map<DbUser>(user)”: We mapped from User to DbUser with AutoMapper so the PasswordHash property must be saved as encrypted by using the “DataProtector” class.
  • “context.DbUser.Add(model)”: We added a new user to the DbUser by using Entity.
app.MapPost("/InsertUser", async (DevNotContext context, User user,IMapper mapper) =>{    //var model = autoMapper.Map<DbUser>(user);
var model = mapper.Map<DbUser>(user);
context.DbUser.Add(model);

await context.SaveChangesAsync();
return new OkResult();
});

After inserting a new user, this is the result of NewUser data on the MsSql Server. As seen below, the PasswordHash property is encrypted and secure. On AutoMapper class from User to DBUser, we protected the password property to the passwordhash property.

Let’s get User by name service:

program.cs/GetAllUsersByName: We will get the user list by containing the name from DBUser to => User view model this time.

  • “context.DbUser.Where”: We will get user list by name async.
  • “var result = autoMapper.Map<List<User>>(model)”: We will get User data from DBUser to the User view model. While getting data from DBuser, we decrypt PasswordHash and return the User ViewModel.
app.MapGet("/GetAllUsersByName/{name}", async (HttpContext http, DevNotContext context, string name) =>
{
var model = await context.DbUser.Where(u =>
u.UserName.Contains(name)).ToListAsync();
var result = autoMapper.Map<List<User>>(model);
return result;
});

This is the result page of GetAllUsersByName() method. You could see PasswordHash is decrypted.

JWT Token Integration :

Firstly We have to download Authentication.JwtBearer Library from the Nuget Package manager as seen below.

As seen below, we add Authentication config with JwtBearer. Personally, I don’t prefer Jwt because of could not change the expiration time of the token. You have to call AddAuthorization() method before declaring add Jwt. “Issuer”, “Audience” and “SigningKey” are the changeable config parameters for creating own unique TokenKey.

builder.Services.AddAuthorization();builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateActor = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Issuer"],
ValidAudience = builder.Configuration["Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["SigningKey"]))
};
});

appsettings.json: We have to declare these parameters at the appsettings.json. These config parameters are used by the JWT.

"Issuer": "https://login.vbt.com.tr/bkasmer",
"Audience": "borakasmer",
"SigningKey": "Cut The Night With The Light"

Let’s Create Token Service With JWT:

Service/ITokenService: After we login, we will get a user and use it as a parameter to generate a token for the current user. (GetToken())

namespace DevNot2021.Services;
public interface ITokenService
{
string GetToken(DbUser user);
}

Service/TokenService: In this service, we will create JwtToken for a limited time for logging user.

  • Firstly we will create a Claim by using the “User” parameter.
  • issuer, audience, and SymmetricSecurityKey are gets from config.
  • “expires“ time is sixty days :) I know it is too long. And you can not expire the token before. So this is the reason, why I don’t prefer JWT.
  • “signingCredentials”: We select SecurityAlgorithms.HmacSha256.
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
public class TokenService : ITokenService
{
WebApplicationBuilder _builder;
public TokenService(WebApplicationBuilder builder)
{
_builder = builder;
}
public string GetToken(DbUser user)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName),
new Claim(JwtRegisteredClaimNames.Email, user.Email)
};
var token = new JwtSecurityToken
(
issuer: _builder.Configuration["Issuer"],
audience: _builder.Configuration["Audience"],
claims: claims,
expires: DateTime.UtcNow.AddDays(60),
notBefore: DateTime.UtcNow,
signingCredentials: new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_builder.Configuration["SigningKey"])),SecurityAlgorithms.HmacSha256)
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}

“Life is really simple, but we insist on making it complicated.”

-Confucious

We will add this ITokenService to builder as below. After all, we can inject it by using Dependency Injection.

program.cs/ITokenService:

builder.Services.AddSingleton<ITokenService>(new TokenService(builder));

program.cs: Firstly we have to add Authorization and Athentication service to the app.

app.UseAuthorization();
app.UseAuthentication();
app.UseCors(p =>
{
p.AllowAnyOrigin();
p.WithMethods("GET");
p.AllowAnyHeader();
});

Model/Login: We will write Login service. And this is our login model.

namespace DevNot2021.Model;public class Login
{
public string UserName { get; set; }
public string Password { get; set; }
}

program.cs/login:

  • “[AllowAnonymous]”: This attribute means there is no Authorization for this method.
  • We will inject DevNotContext, ITokenService and HttpContext on Constructor. And send the Login parameter for the user.
  • “_context.DbUser.Where”: We will search the user by UserName and Password
  • “if (userModel == null)”: If we don’t find any user, we will return 401 UnAuthorized error.
  • “var token = service.GetToken(userModel)”: If we find a user, we will create a Token and return it.
app.MapPost("/login", [AllowAnonymous] async (DevNotContext _context, HttpContext http, ITokenService service, Login login) =>
{
if (!string.IsNullOrEmpty(login.UserName) && !string.IsNullOrEmpty(login.Password))
{
var userModel = await _context.DbUser.Where(u => u.UserName == login.UserName && u.Password ==
login.Password).FirstOrDefaultAsync();
if (userModel == null)
{
http.Response.StatusCode = 401;
await http.Response.WriteAsJsonAsync(new { Message = "Yor Are Not Authorized!" });
return;
}
var token = service.GetToken(userModel);
await http.Response.WriteAsJsonAsync(new { token = token });
return;
}
});

This is the result of the login service. Next, we will use this JWT token for the Authorization of any web services.

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

program.cs/GetAllUsersByName: Now we added [Authorize] attribute to the existing GetAllUsersByName() method. Now, this method is secure and forbidden for anonymous users.

And also besides the [Authorize] attribute, we could add the “.RequireAuthorization()” extension method to secure this service too.

app.MapGet("/GetAllUsersByName/{name}", [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] async (HttpContext http, DevNotContext context, string name) =>
{
var model = await context.DbUser.Where(u =>
u.UserName.Contains(name)).ToListAsync();
var result = autoMapper.Map<List<User>>(model);
return result;
});//.RequireAuthorization();

If you try the GetAllUsersByName() method without the login as below, you will get a 401 UnAuthorized error.

Firstly you have to Login and get a Token. Later copy and paste it into the swagger Authorization Bearer Box. And that’s all. We complete the Authorization step.

Now, when you search user by name from “GetAllUsersByName()”, you can get the result as below.

Conclusion:

In this article, we talked about Minimal Api, .Net 6.0, and C#10 benefits and new features. Performance and simplicity are the most important things these days. .Net 6.0 and Entity 6.0 give us more performance and Minimal Api gives us more readability. In my opinion, Minimal Api is very handy for small projects like microservice. .Net 6.0 and C# 10 new features are game-changing. If you are not open to new features, if you just only upgrade Entity from 5.0 to 6.0, performance is now 70% faster on the industry-standard TechEmpower Fortunes benchmark.

I hope this article helps you to understand what is Minimal API. How can we implement environment technologies, into the Minimal API? And how can we convert an existing project to Minimal API or create a new one? See you later until the next article or video. Bye.

“If you have read so far, first of all, thank you for your patience and support. I welcome all of you to my blog for more!”

Source Code: https://github.com/borakasmer/MinimalApi

Abys_Proud Sql Script: http://borakasmer.com/projects/Abys_Prod.sql

Source:

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Bora Kaşmer

Bora Kaşmer

2.1K Followers

I have been coding since 1993. I am computer and civil engineer. Microsoft MVP. Senior Software Architect. Ride motorcycle. Gamer. Have two daughters.