MS DOC를 참고하거나 발췌하였습니다. 각 개념이 처음 등장할 때 문서의 링크로 연결했습니다.
ASP.NET Core 기본 사항 개요를 읽고 다음으로 넘어가는 것을 추천합니다.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
// Do work that can write to the Response.
await next.Invoke();
// Do logging or other work that doesn't write to the Response.
});
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from 2nd delegate.");
});
app.Run();
Program.cs
파일에 추가되는 순서로 요청과 응답에 따른 순서가 정의됩니다.var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Map("/map1", HandleMapTest1);
app.Map("/map2", HandleMapTest2);
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});
app.Run();
static void HandleMapTest1(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 1");
});
}
static void HandleMapTest2(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 2");
});
}
Map확장은 파이프라인을 분기하는 규약으로 사용됩니다.
Map은 주어진 요청 경로와 일치하는 것을 기반으로 요청 파이프라인을 분기합니다.
app.Map("/level1", level1App => {
level1App.Map("/level2a", level2AApp => {
// "/level1/level2a" processing
});
level1App.Map("/level2b", level2BApp => {
// "/level1/level2b" processing
});
});
app.Map("/map1/seg1", HandleMultiSeg);
MapWhen, UseWhen으로 주어진 조건자의 결과에 따라 요청 파이프라인을 분기할 수 있습니다.
인라인 미들웨어로 만드는 것도 가능하지만 별도의 클래스를 만들어서 사용하는 방법을 보겠습니다.
미들웨어 클래스는 다음을 포함해야 합니다.
RequestDelegate
타입의 매개변수가 있는 public 생성자Invoke
또는 InvokeAsync
public 메서드Task
를 반환하고 HttpContext
타입의 첫 번째 매개변수를 받아야 합니다.Invoke
의 추가 매개 변수는 뒷절에서 나오는 DI
로 채워집니다.public class RequestCultureMiddleware
{
private readonly RequestDelegate _next;
public RequestCultureMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
// Call the next delegate/middleware in the pipeline.
await _next(context);
}
}
미들웨어 클래스를 만들었으면 확장 메서드로 노출시킵니다.
public static class RequestCultureMiddlewareExtensions
{
public static IApplicationBuilder UseRequestCulture(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestCultureMiddleware>();
}
}
다음 코드를 호출할 때 this IApplicationBuilder builder
에 app
객체가 할당 됩니다.
app.UseRequestCulture()
라우팅을 구성하는 방법: ASP.NET Core의 컨트롤러 작업에 라우팅
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
//...
app.MapControllers();
app.Run();
MapControllers
가 호출되어야 어트리뷰트 라우팅된 컨트롤러에 매핑할 수 있습니다.다음 코드는 conventional route
와의 차이를 볼 수 있는 코드입니다.
public class HomeController : Controller
{
[Route("")]
[Route("Home")]
[Route("Home/Index")]
[Route("Home/Index/{id?}")]
public IActionResult Index(int? id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
[Route("Home/About")]
[Route("Home/About/{id?}")]
public IActionResult About(int? id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}
conventional route
보다 더 많은 입력이 필요합니다. 다음 코드에서 [controller]는 test2와 대응됩니다.
[Route("api/[controller]")]
[ApiController]
public class Test2Controller : ControllerBase
{
[HttpGet] // GET /api/test2
public IActionResult ListProducts()
{
return ControllerContext.MyDisplayRouteInfo();
}
[HttpGet("{id}")] // GET /api/test2/xyz
public IActionResult GetProduct(string id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
[HttpGet("int/{id:int}")] // GET /api/test2/int/3
public IActionResult GetIntProduct(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
[HttpGet("int2/{id}")] // GET /api/test2/int2/3
public IActionResult GetInt2Product(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}
public interface IMyDependency
{
void WriteMessage(string message);
}
public class MyDependency : IMyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
}
}
IMyDependency
인터페이스를 요청하는 모든 클래스에 인스턴스를 제공합니다.using Microsoft.Extensions.DependencyInjection;
using YourNamespace.Interfaces;
using YourNamespace.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddScoped<IMyDependency, MyDependency>();
var app = builder.Build();
public class IndexModel : PageModel
{
private readonly IMyDependency _myDependency;
public IndexModel(IMyDependency myDependency)
{
_myDependency = myDependency;
}
public void OnGet()
{
_myDependency.WriteMessage("IndexModel.OnGet");
}
}
앞선 코드에서는 AddScoped
를 사용했지만 ASP.NET Core에서는 수명에 따라 다음 세 가지가 존재합니다.
WebApplication.CreateBuilde는 미리 구성된 기본 값을 사용하여 클래스의 새 인스턴스를 초기화 합니다.
뒤에서 살펴볼 코드는 JSON 구성 공급자를 사용합니다.
ASP.NET Core는 옵션 패턴을 제공합니다. 구성이 별도의 클래스로 분리되면 두 가지 소프트웨어 공학 원칙을 따르게 됩니다.
옵션 클래스는 다음과 같아야 합니다.
public class DbConfig
{
public const string name = "DbConfig";
public String MasterDb { get; set; }
public String AccountDb { get; set; }
public String GameDb { get; set; }
public String Memcached { get; set; }
}
var builder = WebApplication.CreateBuilder(args);
IConfiguration configuration = builder.Configuration;
builder.Services.Configure<DbConfig>(configuration.GetSection(nameof(DbConfig)));
//또는 configuration.GetSection(DbConfig.name)
//...
첫 번째 행에서 WebApplicatoin은 HTTP 파이프라인과 routes를 구성하는데 사용됩니다.
IConfiguration configuration = builder.Configuration;
코드를 통해 구성 정보에 접근할 수 있는 IConfiguration
인스턴스를 가져옵니다.
세 번째 문장은 Configure
함수를 통해 구성 데이터를 서비스 컨테이너에 등록합니다. 등록된 데이터는 DI를 통해 다른 클래스에서 사용될 수 있습니다.
IOptions
를 사용합니다.IOptions
는 앱 시작 후의 JSON 구성 파일의 변경사항은 읽지 않습니다.IOptionsSnapshot
을 사용해야 합니다.public class MyClass
{
private readonly DbConfig _dbConfig;
public MyClass(IOptions<DbConfig> dbConfig)
{
_dbConfig = dbConfig.Value;
}
// ...
}
GetSection
은 IConfigurationSection 반환합니다.
configuration.GetSection("DbConfig")
키로 임의 접근을 하면 문자열을 반환합니다.
configuration["logdir"]
json 예시
{
"logdir": "./log/",
"DbConfig": {
"Redis": "127.0.0.1",
...
}
}
로깅은 실행 동안 발생한 이벤트를 기록하고 추적할 수 있는 매커니즘입니다. 성능, 문제 해결 및 보안에 큰 도움이 됩니다.
로그를 표시하는 Console
을 제외하면 로깅 공급자는 로그를 저장합니다.
일반적으로 WebApplicartion.CreateBuilder
를 호출하면 다음 공급자를 추가합니다.
로그도 마찬가지로 의존성 주입(DI)을 통해 만들어질 수 있습니다.
public class AboutModel : PageModel
{
private readonly ILogger _logger;
public AboutModel(ILogger<AboutModel> logger)
{
_logger = logger;
}
public void OnGet()
{
_logger.LogInformation("About page visited at {DT}",
DateTime.UtcNow.ToLongTimeString());
}
}
ILogger<TCategoryName>
객체를 사용합니다.ILogger
에 DI 하기 위해 Program.cs
에서 직접 함수를 호출할 필요가 없습니다.로그 카테고리는 각 로그와 연결된 문자열입니다. 다음 방법으로 정할 수 있습니다.
해당 문자열은 ILogger<T>
의 T
에 해당하는 클래스의 이름
_logger = logger.CreateLogger("AboutModel");
logger
는 ILoggerFactory
객체 입니다.로그 카테고리는 JSON 파일에서 로깅을 구성할 때 사용될 수 있습니다. 로깅 구성은 일반적으로 appsettings.{ENVIRONMENT}.json 파일의 Logging
섹션에서 제공됩니다.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"MyLogingLogger":"Trace"
}
}
}
로그 레벨은 로그 이벤트의 심각성을 나타냅니다. 로그 레벨은 Trace, Debug, Information, Warning, Error, Critical 및 None으로 구성되어 있으며, 각각 0부터 6까지 할당됩니다.
json 파일에서 공급자별로 로그 레벨을 설정할 수 있습니다.
Logging.{PROVIDER NAME}.LogLevel
{
"Logging": {
"LogLevel": { // All providers, LogLevel applies to all the enabled providers.
"Default": "Error", // Default logging, Error and higher.
"Microsoft": "Warning" // All Microsoft* categories, Warning and higher.
},
"Debug": { // Debug provider.
"LogLevel": {
"Default": "Information", // Overrides preceding LogLevel:Default setting.
"Microsoft.Hosting": "Trace" // Debug:Microsoft.Hosting category.
}
},
"EventSource": { // EventSource provider
"LogLevel": {
"Default": "Warning" // All categories of EventSource provider.
}
}
}
}
//Program.cs 파일
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddLogging(); // 로그 추가
builder.Services.AddControllers();
// 로그 설정
builder.Logging.ClearProviders(); // 공급자 제거
builder.Logging.AddConsole(); // 콘솔 공급자 추가
//...
//LoginController.cs 파일
[ApiController]
[Route("[controller]")]
public class Login : ControllerBase
{
private readonly ILogger Logger;
//DI를 위한 생성자
public Login(ILogger<Login> logger)
{
Logger = logger;
}
//...
[HttpPost]
public async Task<PkLoginResponse> Post(PkLoginRequest request)
{
//...
//로그 출력
Logger.LogInformation($"[Request Login] Email:{request.Email}, request.Password:{request.Password}, saltValue:{userInfo.SaltValue}, hashingPassword:{hashingPassword}");
}
//...
WebAPI를 단계 별로 만들어 보겠습니다.
//Program.cs
var builder = WebApplication.CreateBuilder(args);
IConfiguration configuration = builder.Configuration;
builder.Services.AddControllers();
var app = builder.Build();
app.UseRouting();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
app.Run(configuration["ServerAddress"]);
DI
를 위해 인터페이스를 작성하고 이를 상속 받습니다.ILogger
와 IOptions
매개변수를 갖는 생성자를 작성합니다.IDisposable
을 구현한 객체가 소멸할 때 Dispose
가 호출 됩니다.//AccountDatabase.cs
namespace firstAPI.Services
{
public interface IAccountDatabase : IDisposable
{
}
public class AccountDatabase : IAccountDatabase
{
private readonly IOptions<DatabaseConfiguration> _configurationOptions;
private readonly ILogger<AccountDatabase> _logger;
private IDbConnection _databaseConnection;
QueryFactory _queryFactory;
public AccountDatabase(ILogger<AccountDatabase> logger, IOptions<DatabaseConfiguration> configurationOptions)
{
_configurationOptions= configurationOptions;
_logger= logger;
_databaseConnection = new MySqlConnection(configurationOptions.Value.AccountDatabase);
_databaseConnection.Open();
var compiler = new MySqlCompiler();
_queryFactory = new QueryFactory(_databaseConnection, compiler);
}
public void Dispose()
{
_databaseConnection.Dispose();
//_queryFactory.Dispose();
}
}
}
public class DatabaseConfiguration
{
public String AccountDatabase { get; set; }
public String GameDb { get; set; }
public String Redis { get; set; }
}
CreateAccountController
를 작성합니다.IAccountDatabase
변수에 DI
하기 위해 Program.cs
에서 명시적인 함수 호출이 필요합니다.http://localhost:11500/CreateAccount
Post 요청을 받으면 콘솔 창에 출력 합니다.[Route("[controller]")]
[ApiController]
public class CreateAccountController : ControllerBase
{
private readonly IAccountDatabase _accountDatabase;
private readonly ILogger<CreateAccountController> _logger;
public CreateAccountController(IAccountDatabase accountDatabase, ILogger<CreateAccountController> logger)
{
_accountDatabase = accountDatabase;
_logger = logger;
}
[HttpPost]
public async Task<PacketTest> Post(PacketTest packet)
{
Console.WriteLine(packet.Email);
Console.WriteLine(packet.Password);
return packet;
}
}
public class PacketTest
{
public String Email { get; set; }
public String Password { get; set; }
}
Program.cs
에 DI를 위한 코드를 작성합니다.DatabaseConfiguration
섹션의 정보를 가져와 DI 컨테이너에 의존성 등록합니다.IAccountDatabase
를 구현하는 AccountDatabase
클래스에 대한 의존성을 등록합니다.IConfiguration configuration = builder.Configuration;
builder.Services.Configure<DatabaseConfiguration>(configuration.GetSection(nameof(DatabaseConfiguration)));
builder.Services.AddTransient<IAccountDatabase, AccountDatabase>();
데이터 베이스 연결과 요청 처리가 잘 되었음을 볼 수 있다.
Postman 요청 및 응답
콘솔 출력
AccountDatabase
클래스에 CreateAccountAsync
함수를 만듭니다.Insert
합니다.public async Task<ErrorCode> CreateAccountAsync(String email, String password)
{
try
{
//임시 값
var saltValue = 991;
var hashingPassword = 119;
Console.WriteLine($"[CreateAccount] Email: {email}, Password: {password}");
var count = await _queryFactory.Query("account")
.InsertAsync(new { Email = email, SaltValue = saltValue, HashedPassword = hashingPassword });
if (count != 1)
{
return ErrorCode.CreateAccountFailInsert;
}
return ErrorCode.None;
}
catch (Exception e)
{
Console.WriteLine(e);
return ErrorCode.CreateAccountFailException;
}
}
CreateAccountController.Post
함수를 수정한다. public class PkCreateAccountResponse
{
public ErrorCode Result { get; set; }
}
[HttpPost]
public async Task<PkCreateAccountResponse> Post(PkCreateAccountRequest packet)
{
var response = new PkCreateAccountResponse();
var errorCode= await _accountDatabase.CreateAccountAsync(packet.Email, packet.Password);
response.Result = errorCode;
if (errorCode != ErrorCode.None)
{
return response;
}
Console.WriteLine("Account is Created!");
return response;
}
LoginController
를 생성하고 함수를 생성합니다.[Route("[controller]")]
[ApiController]
public class LoginController : ControllerBase
{
private readonly IAccountDatabase _accountDatabase;
private readonly IMemoryDatabase _memoryDatabase;
public LoginController(ILogger<LoginController> logger, IAccountDatabase accountDb, IMemoryDatabase memoryDb)
{
_accountDatabase = accountDb;
_memoryDatabase = memoryDb;
}
[HttpPost]
public async Task<PkLoginResponse> Post(PkLoginRequest request)
{
var response = new PkLoginResponse();
//유저 정보 확인
var (errorCode, accountId) = await _accountDatabase.VerifyAccount(request.Email, request.Password);
if (errorCode != ErrorCode.None)
{
response.Result = errorCode;
return response;
}
//토큰 발행 후 추가
var tempToken = "999";
errorCode = await _memoryDatabase.RegisterUserAsync(request.Email, tempToken,accountId);
if (errorCode != ErrorCode.None)
{
response.Result = errorCode;
return response;
}
response.AuthToken = tempToken;
return response;
}
}
public class PkLoginRequest
{
public String Email { get; set; }
public String Password { get; set; }
}
public class PkLoginResponse
{
public ErrorCode Result { get; set; }
public string AuthToken { get; set; }
}
AccountDatabase
클래스에 VerifyAccount
함수를 만듭니다.public async Task<Tuple<ErrorCode, Int64>> VerifyAccount(string email, string password)
{
try
{
var accountInformation =
await _queryFactory.Query("account").Where("Email", email).FirstOrDefaultAsync<Account>();
if (accountInformation == null || accountInformation.AccountId ==0)
{
return new Tuple<ErrorCode, Int64>(ErrorCode.LoginFailUserNotExist, 0);
}
return new Tuple<ErrorCode, Int64>(ErrorCode.None, accountInformation.AccountId);
}
catch (Exception e)
{
return new Tuple<ErrorCode, Int64>(ErrorCode.LoginFailException, 0);
}
}
public class Account
{
public Int64 AccountId { get; set; }
public String Email { get; set; }
public String HashedPassword { get; set; }
public String SaltValue { get; set; }
}
IMemoryDatabase
인터페이스와 이를 상속한 RedisDatabase
클래스를 만듭니다.RegisterUser
키와 1번에서 임시로 만든 토큰을 저장합니다.public interface IMemoryDatabase
{
Task<ErrorCode> RegisterUserAsync(string id, string authToken, Int64 accountID);
}
public class RedisDatabase : IMemoryDatabase
{
public RedisConnection _redisConnection;
public RedisDatabase(IOptions<DatabaseConfiguration> configuration)
{
var config = new RedisConfig("default", configuration.Value.Redis);
_redisConnection = new RedisConnection(config);
}
public async Task<ErrorCode> RegisterUserAsync(string email, string authToken, Int64 accountId)
{
var tempKey = "RegisterUser";
var result = ErrorCode.None;
try
{
var redis = new RedisString<string>(_redisConnection, tempKey, TimeSpan.FromMinutes(15));
redis.SetAsync(authToken);
}
catch (Exception e)
{
result = ErrorCode.LoginFailRegisterToRedis;
return result;
}
return result;
}
}