Prologue

The inspiration of this post comes from this article.

I’ve been always looking forward to develop backend with ASP.NET Core, but when I’m actually doing this, I found the start up being a little tough. So I decide just write things down as a memo.


1. Setup Project

1.1 Create Project

Well, of course, we use Visual Studio as our IDE. Create a project using “ASP.NET Core Web API” template. If you want a frontend, just use “React and ASP.NET Core (Preview)”. Although in preview, I think it is quite awesome.

image-20230706150158036

Then, for additional information, just leave it default is OK. Or you can just select another framework.

image-20230706150917625

After creation, you will get a structure like this. But this structure is apparently not what we like.

image-20230706151441899

1.2 Essential NuGet Packages

Well, you can download these NuGet packages later, but a quick preview of them may help you know what package you can utilize.

image-20230706151848530

Well, Microsoft.EntityFrameworkCore packages enables the powerful Entity Framework Core, which is an excellent ORM framework. And then MySql.EntityFrameworkCore add support for MySQL database connection. Then, if you got CORS problem, you’ll need Microsoft.AspNetCore.Cors package. At last, AutoMapper is an extraordinary convenient tool for you to map entity to Data Transfer Object (DTO).


2. Refactor Project Structure

2.1 Application Entry

In .NET 6, the Startup.cs is removed, and replaced by Program.cs alone. But, personally, I don’t prefer this, as it looks messy.

So first, we refactor Program.cs. Now it looks better.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}

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

Then, we create Startup.cs next to Program.cs. An brief overview of this file is like this. And you can expand the complete version.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Startup
{
public IConfiguration Configuration { get; }

public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public void ConfigureServices(IServiceCollection services)
{
// ...
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ...
}
}
Startup.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class Startup
{
public IConfiguration Configuration { get; }

// Dependency Injection
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public void ConfigureServices(IServiceCollection services)
{
services.AddCors();

ConfigureDatabase<BadgeContext>(services);
services.AddUnitOfWork<BadgeContext>();
services.RegisterModules();

// Controllers
services.AddControllers();

// Swagger service
services.AddSwaggerGen(c => {
c.SwaggerDoc("v1", new OpenApiInfo() { Title = "SimpleToDo.Api", Version = "v1" });
});

// AutoMapper
var autoMapperConfig = new MapperConfiguration(config => {
config.AddProfile(new AutoMapperProfile());
});
services.AddSingleton(autoMapperConfig.CreateMapper());
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Demo.Api v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();

// Must be placed between UseRouting and UseEndpoints
app.UseCors(policy => {
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});

app.UseEndpoints(endpoints => {
endpoints.MapControllers();
endpoints.MapSwagger();
});
}

private void ConfigureDatabase<TContext>(IServiceCollection services) where TContext : DbContext
{
var profile = Configuration["Profile"];
var database = Configuration.GetConnectionString("Database");
var connection = Configuration.GetConnectionString("DefaultConnection");

switch (database) {
case "MySQL":
services.AddDbContext<TContext>(option => {
option.UseMySQL(connection);
});
break;
case "SQLite":
services.AddDbContext<TContext>(option => {
option.UseSqlite(connection);
});
break;
default:
throw new Exception($"Invalid database: {database}");
}
}
}

2.2 Application Configuration

The configuration for the project is in appsettings.json, and it can be overwritten depends on the environment. So there are actually three configuration files.

image-20230706153839776

You may need to create appsettings.Production.json manually.

The content of the configuration can be like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"Profile": "Development",
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Database": "SQLite",
"DefaultConnection": "DataSource=Data\\BadgeBoard.db;Cache=Shared"
}
}

And if you use MySQL, the connection string for MySQL could be like this.

1
"server={ip};database={database};user={user};password={password}"

If you’re not using the default 3306 port, then the connection may look like this.

1
"server={ip};port={port};database={database};user={user};password={password}"

Which appsettings.json is used is based on the environment value ASPNETCORE_ENVIRONMENT. That is to say, the application will load appsettings.{ASPNETCORE_ENVIRONMENT}.json. If not set, will load appsettings.json.

image-20231214234026705

There’s another configuration file called launchSettings.json, which mainly configures the port of the API.

image-20230706154636480

2.3 Project Structure

2.3.1 Overview

Well, according to Maybe it’s time to rethink our project structure with .NET 6, we can use module to manage our project. And based on the idea in this article, we can refactor our project like this.

image-20230706155559379

Let me have a brief elaboration. Data\ is for the SQLite database file, and Migrations\ is for the migration files created by EF Core. Extensions\ holds all common utils or base classes (I’ll explain this later). And Modules\ is where all our business module are placed. For Templates\, I adopted this concept from Django, where template HTML files are saved.

2.3.2 Extensions

Well first, an overall glance. The most important and fundamental packages are Module and UnitOfWork.

image-20230706162245071

2.3.2.1 Module

Module holds all base classes and interfaces required for our modules. For example, the IModule interface.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface IModule
{
IServiceCollection RegisterModule(IServiceCollection services);
IEndpointRouteBuilder MapEndpoints(IEndpointRouteBuilder endpoints);
}

public class BaseModule : IModule
{
public virtual IEndpointRouteBuilder MapEndpoints(IEndpointRouteBuilder endpoints)
{
return endpoints;
}

public virtual IServiceCollection RegisterModule(IServiceCollection services)
{
return services;
}
}

Then, for web API, we often use MVC pattern, and usually relay actual logic from Controller to Service class. Here is the main place where Dependency Injection (DI) is used.

For more information about Dependency Injection in ASP.NET, see:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class BaseController<TController> : Controller where TController : Controller
{
protected readonly ILogger<TController> _logger;

public BaseController(ILogger<TController> logger)
{
_logger = logger;
}
}

public class BaseService
{
protected readonly IServiceProvider _provider;
protected readonly IUnitOfWork _unitOfWork;
protected readonly IMapper _mapper;

protected BaseService(IServiceProvider provider, IUnitOfWork unitOfWork, IMapper mapper)
{
_provider = provider;
_unitOfWork = unitOfWork;
_mapper = mapper;
}
}

2.3.2.2 Unit Of Work

Another important part is Unit of Work Pattern, which often comes along with Repository Pattern. For detailed information, you can refer to Microsoft official documentation.

Here, I use the implementation provided by Arch.

GitHub: Arch/UnitOfWork

2.3.2.3 Response

You may also want to wrap up your responses. Here I offer a simple but practical way.

1
2
3
4
5
6
7
public class ApiResponse : JsonResult
{
protected ApiResponse(int code, object? value) : base(value)
{
StatusCode = code; // code is StatusCodes
}
}

With this, you can extend other responses, for example,

1
2
3
4
5
6
7
8
9
10
11
12
13
public class GoodResponse : ApiResponse
{
public GoodResponse(object? value) : base(StatusCodes.Status200OK, value)
{
}
}

public class BadRequestResponse : ApiResponse
{
public BadRequestResponse(object? value) : base(StatusCodes.Status400BadRequest, value)
{
}
}

Then, for the value, it can be this, which will later be wrapped as DTO.

1
2
3
4
5
6
7
8
9
10
11
public class ApiResponseData
{
public ApiResponseMeta Meta { get; set; }
public object? Data { get; set; }
}

public class ApiResponseMeta
{
public int Status { get; set; }
public string? Message { get; set; }
}

The DTO is extended can be like this.

1
2
3
4
5
6
public class GoodDto : ApiResponseData
{
public GoodDto(string message = "Nice request", object? data = null) : base(0, message, data)
{
}
}

2.3.3 Modules

Again, first an overview. We put all our modules under Modules/ packages, and each module has its own models, controllers, services and else.

image-20230706162649477

2.3.3.1 Database Context

First, is two file outside all modules. There must be a context for database support, as we have already seen above in Startup.ConfigureServices.

The constructor here is not always empty, though. If you use Fluent API to define properties of your entities, you will have to do that. But I prefer data annotation. Read more in my other post.

1
2
3
4
5
6
7
8
9
public class BadgeContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<FavoriteUser> FavoriteUsers { get; set; }

public BadgeContext(DbContextOptions<BadgeContext> options) : base(options)
{
}
}

2.3.3.2 Auto Mapper Profile

Another global configuration file is AutoMapperProfile.cs. This one defines all mapper from entity to DTO. This is really convenient, since it easily support nested mapping. ReverseMap makes reverse mapping available.

1
2
3
4
5
6
7
8
9
public class AutoMapperProfile : MapperConfigurationExpression
{
public AutoMapperProfile()
{
CreateMap<User, UserDto>().ReverseMap();
CreateMap<UserPreference, UserPreferenceDto>().ReverseMap();
CreateMap<UserInfo, UserInfoDto>().ReverseMap();
}
}

2.3.3.3 Business Module

Now, for our actual modules. Notice that, each module must have a main module file to load corresponding module, as mentioned above. For example, it looks like this. Its main purpose is to link entities with their repositories, and use IoC to prepare services to be injected by DI.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserModule : BaseModule
{
public override IServiceCollection RegisterModule(IServiceCollection services)
{
services.AddCustomRepository<User, UserRepository>()
.AddCustomRepository<UserPreference, UserPreferenceRepository>()
.AddCustomRepository<UserInfo, UserInfoRepository>()
.AddCustomRepository<FavoriteUser, FavoriteUserRepository>();

services.AddTransient<ILoginService, LoginService>()
.AddTransient<IRegisterService, RegisterService>();

return services;
}
}

Then, in controller, just relay logic to services.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[ApiController]
[Route("api/[controller]")]
public class RegisterController : BaseController<RegisterController>
{
private readonly IRegisterService _service;

public RegisterController(ILogger<RegisterController> logger, IRegisterService service)
: base(logger)
{
_service = service;
}

[HttpPost]
[Route("[action]")]
public async Task<ApiResponse> Register([FromBody] RegisterDto dto)
{
return await _service.Register(dto);
}
}

Above controller will have a route called api/Register/Register (with upper case). However, you can use custom string instead of [ ].


Epilogue

Well, I’m still a noob, and it is as far as I can describe. So… let it be.