ASP.NET Core platform자체는 Web Application을 만드는 기반에 해당하며 여기에 MVC나 Blazor와 같은 Framework를 사용할 수 있는 기능을 제공합니다. 이번 글에서는 ASP.NET Core platform과 관련하여 ASP.NET Core project에서 생성되는 각 file에 대한 목적과 ASP.NET Core 요청 pipeline이 HTTP요청을 처리하기 위해 어떻게 사용되는지, 그리고 이러한 것들을 어떻게 사용자정의할 수 있는지에 대해 알아봄으로써 전반적인 ASP.NET Core기능의 작동방식을 파악해 보고자 합니다.
별것 아닌 사소한 부분에 해당하는 것 같지만 사실 여기서 설명하는 것은 ASP.NET Core가 동작하는 것에 대한 기본적인 사항들로서 이러한 것들을 이해하는 것은 매일같이 사용하는 일반적인 기능을 더 잘 이해할 수 있도록 도움을 줄 뿐만 아니라 예상한대로 Application이 동작하지 않을 때 문제점들을 파악하는데도 도움이 될 수 있습니다.
1. Project 준비
예제 Project는 최소한의 ASP.NET Core 설정만을 제공하는 template을 사용하여 Platform이라는 이름으로 생성할 것입니다. 이를 위해 PowerShell에서 아래 명령을 실행합니다.
dotnet new globaljson --sdk-version 8.0.204 --output Platform dotnet new web --no-https --output Platform --framework net8.0 dotnet new sln -o Platform dotnet sln Platform add Platform |
위 명령을 실행한 후 Visual Studio를 사용한다면 Platform에서 Platform.sln을 통해 Project를 열 수 있으며, Visual Studio Code를 사용한다면 Platform folder를 열어 Project를 열 수 있습니다. 이 후 Properties folder에 있는 launchSettings.json file을 열어 사용하는 Port를 확인합니다. 예제에서는 편의성을 위해 아래와 같이 Port를 8000으로 변경하였습니다.
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:8000",
"sslPort": 0
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:8000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
(1) 예제 실행하기
예제를 실행하기 위해 Platform folder에서 아래 명령을 실행하고
dotnet run |
WebBrowser를 열어 http://localhost:8000으로 URL을 요청하여 다음과 동일한 응답이 생성되는지 확인합니다.
2. ASP.NET Core platform의 이해
ASP.NET Core는 아래 3가지 핵심적인 기능을 이해하는 것이 중요합니다.
- 요청 Pipeline
- Middleware
- Service
개별적으로 각각의 기능에 대해 상세하게 알지는 못하더라도 이들 기능이 어떻게 함께 결합하여 동작하는지를 이해하는 것만으로 ASP.NET Core Project와 ASP.NET Core platform의 형태를 이해하는데 도움이 될 수 있습니다.
(1) Middleware와 요청 Pipeline의 이해
ASP.NET Core platform의 목적은 HTTP요청을 받아 요청자에게 응답하는 것으로 ASP.NET Core는 이 역활을 Middleware Component에 위임하고 있습니다. Middleware Component는 Chain으로 배열된 형태를 이루고 있는 것으로 이를 요청 Pipeline(Request Pipeline)이라고 합니다.
HTTP 요청이 도달하면 ASP.NET Core platform은 이를 서술하는 개체와 응답으로 보낼 개체를 생성합니다. 그리고 이들 개체는 Chain에서 첫 번째 Middleware Component으로 전달되어 요청을 검사하고 응답을 수정하게 됩니다. 그런 후 요청은 다시 Chain에서 다음 Middleware Component에 전달되고 이런 식으로 각각의 Component에서는 요청을 확인하여 필요한 응답을 추가합니다. 일단 요청이 Pipeline을 통과하게 되면 ASP.NET Core platform은 응답을 보내게 되는 것입니다.
일부 Component는 요청에 대응하는 응답을 생성하기도 하지만 일부는 특정한 Data type에 대한 형식화나 Cookie를 읽고 쓰는 것과 같은 기능을 제공하기도 합니다. ASP.NET Core는 일반적인 문제를 해결하기 위한 Middleware Component를 포함하고 있는데 이들에 관한 내용과 개발자가 직접 자신만의 Middleware Component를 어떻게 만들고 적용할 수 있는지에 대해서는 추후에 자세히 알아볼 것입니다. 만약 Middleware Component에서 생성된 응답이 존재하지 않는 경우라면 ASP.NET Core는 HTTP 404 Not Found 상태 Code를 반환하게 될 것입니다.
(2) Service의 이해
Service를 정의하면 Web Application에서 특정 기능을 제공하는 개체라고 할 수 있습니다. 모든 Class는 Service로서 사용될 수 있으며 제공할 수 있는 기능에 제한은 없습니다. Service를 특별하게 만든는 것은 이들이 ASP.NET Core에 의해 관리된다는 것이며 의존성 주입(Dependency Injection)기능을 통해 Application 어디든 Middleware를 포함해 Service로 쉽게 접근할 수 있습니다.
의존성주입은 또 하나의 큰 영역으로 추후에 자세히 알아볼 것이지만 Component간 조정의 역할을 하거나 Logging 혹은 구성 Data를 Load 하는 것과 같은 공통기능의 중복을 피하기 위해 Middleware Component에서 공유할 수 있는 ASP.NET Core platform이 관리하 개체라고 할 수 있습니다.
Middleware Component는 자신들의 작업을 수행하는데 필요한 Service를 사용하는 것이며, ASP.NET Core는 특정 Application에 대한 추가적인 Service에 의해 보완될 수 있는 기본적인 Service를 일부 제공하고 있습니다.
3. .NET Core Project의 이해
Web template은 몇 가지 기본적인 Service와 Middleware Component로 ASP.NET Core runtime을 시작하기 위해 충분한 Code와 설정을 가진 Project를 생성합니다. 우리가 이전에 생성한 Platform Project 또한 Template을 통해 생성한 것이며 아래와 같은 file을 Project에 추가해 줍니다.
Visual Studio와 Visual Studio Code는 File과 Folder를 표시하기 위한 방식이 서로 다릅니다. Visual Studio는 개발자가 일반적으로 사용하지 않는 Item은 기본적으로 숨기는 형태로 표시하며 Visual Studio Code가 모든 File을 1차원적으로 표시하는 반면 관련된 Item을 중첩시켜 표시합니다. 이런 특징으로 인해 Visual Studio는 bin과 obj를 기본적으로 숨기게 되며 appsettings.Development.json file 역시 appsettings.json file안으로 중첩해 표시합니다. Visual Studio에서 Solution Explorer창의 위에 나열된 button 중 일부는 중첩된 file과 숨겨진 file을 모두 표시하는 데 사용됩니다.
비록 예제에서는 몇개 안 되는 file들만 표시되고 있지만 이들 모두는 ASP.NET Core개발에 필요한 것들이며 아래 표에서와 같이 각각마다 고유의 역할을 가지고 있습니다.
appsettings.json | Application에서 필요한 설정을 구성하는데 사용됩니다. |
appsettings.Development.json | 개발환경에서만 적용되는 설정을 구성하는데 사용됩니다. |
bin | Compile된 Application file을 포함하며 Visual Studio에서는 기본적으로 숨김상태를 갖습니다. |
global.json | .NET Core SDK의 version을 지정하는데 사용됩니다. |
Properties/launchSettings.json obj | Application이 시작할때의 설정을 위해 사용됩니다. |
Platform.csproj | Project에 대한 전반적인 구성설정을 가진 file로 Package 의존성과 Build조건을 포함하고 있습니다. Visual Studio는 기본적으로 이 file을 숨김상태로 두지만 Solution Explorer에서 Project명을 double-click하거나 mouse오른쪽 button으로 click해 'Edit Project File'을 menu를 선택하면 해당 file을 편집할 수 있습니다. |
Platform.sln | Project를 구성하는데 사용되는 file이며 Visual Studio에서는 기본적으로 숨김상태로 둡니다. |
Program.cs | ASP.NET Core platform의 주요진입점이며 Platform을 구성하는데 사용됩니다. |
(1) Program.cs의 진입점
Program.cs에서는 Application이 시작할때 실행되는 Code를 포함하고 있으며 ASP.NET platform과 이 platform이 지원하는 개별 framework를 구성하는 데 사용됩니다. 예제에서의 Program.cs는 다음과 같습니다.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Program.cs는 top-level 구문이 적용되어 있으며 WebApplication.CreateBuilder를 호출하여 그 결과를 builder이라는 변수에 할당하고 있습니다. 이 method는 ASP.NET Core platform의 기본적인 기능을 설정하며 구성 data logging을 담당하는 service를 생성하고 HTTP요청을 전달받는 데 사용되는 Kestrel이라는 이름의 HTTP server를 설정하기도 합니다.
CreateBuilder method는 WebApplicationBuilder개체를 반환하는데 이 개체는 추가적인 service를 등록하는데 사용되는데 예제에서는 아직 필요한 service를 정의하지 않았습니다. WebApplicationBuilder class는 초기설정을 마무리하는 데 사용되는 Build method를 정의하고 있습니다.
Build method는 middleware component를 설정하는데 사용되는 WebApplication 개체를 반환합니다. 현재 예제에서는 MapGet 확장 method를 사용해 하나의 Middleware Component를 설정하고 있습니다.
MapGet 확장 method는 WebApplication class에서 구현되는 IEndpointRouteBuilder interface의 확장 method로서 특정 URL경로에 대한 HTTP request을 처리할 method를 설정합니다. 예제의 경우 Method는 /로서 표시되는 기본 URL 경로에 대한 요청에 응답하며 간단한 문자열을 응답으로 반환하고 있습니다.
대부분의 Project는 더욱 진보된 일련의 응답을 필요로 하며 Microsoft는 Web Application에서 필요로 하는 가장 일반적인 기능을 처리하는 Middleware를 ASP.NET Core의 일부로서 제공합니다. 물론 추후에 자세히 알아보겠지만 내장 기능이 Project의 요구사항에 맞지 않다면 개발자가 직접 필요한 Middleware생성할 수 있습니다.
Program.cs의 마지막에는 WebApplication class에 정의된 Run method를 호출하고 있는데 해당 method가 호출되면 그때 HTTP요청의 수신을 대기하게 됩니다.
비록 지금은 MapGet method와 함께 사용되는 method가 문자열을 반환할 뿐이지만 ASP.NET Core는 browser에서 처리될 수 있는 유요한 HTTP응답을 생성할 수 있습니다. Project를 실행시킨 상태에서 PowerShell 명령 prompt를 열고 아래 명령을 실행해 실행 중인 ASP.NET Core server로 HTTP요청을 전송합니다.
(Invoke-WebRequest http://localhost:8000).RawContent |
위 명령을 실행하면 다음과 같은 응답이 생성될 것입니다. 해당 응답은 ASP.NET Core에서 보내진 것으로 아래와 같이 HTTP status code와 일련의 기본 header를 포함하고 있습니다.
PS C:\Users\dev> (Invoke-WebRequest http://localhost:8000).RawContent HTTP/1.1 200 OK Transfer-Encoding: chunked Content-Type: text/plain; charset=utf-8 Date: Tue, 07 May 2024 12:35:52 GMT Server: Kestrel Hello World! |
(2) csproj Project file
Project file인 Platform.csproj file은 .NET Core가 Project를 build 하기 위해 사용하는 정보와 종속성을 추적하는 데 사용하는 정보를 포함합니다.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
csproj file은 Visual Studio에서는 기본적으로 숨김상태가 되지만 Solution Explorer에서 Project 명을 mouse double click 하거나 오른쪽 click 하여 'Edit Project File' menu를 선택하면 편집이 가능하도록 csproj file이 열리게 됩니다.
Project file은 Microsoft build engine인 MSBuild에게 Project를 서술하는 XML요소를 포함하고 있습니다. MSBuild는 복잡한 Build process를 생성하는데 사용되는 것으로 아래 link를 통해 더 자세한 정보를 확인할 수 있습니다.
Use the MSBuild XML schema to control builds - MSBuild | Microsoft Learn
Use the MSBuild XML schema to control builds - MSBuild
Explore how the Microsoft Build Engine (MSBuild) platform provides a project file with an XML schema to control builds with properties, items, tasks, and targets.
learn.microsoft.com
대부분의 Project에서는 해당 Project file을 직접적으로 수정해야 하는 일이 발생하지는 않습니다. File을 변경해야 하는 가장 흔한경우는 다른 .NET Package에 대한 종속성을 추가하는 것이지만 그나마도 command-line도구나 Visual Studio에 기본내장된 Interface를 통해 추가될 수 있습니다.
Command line을 통해 Package를 추가하기 위해서 PowerShell을 열고 Platform Project folder(csproj file이 있는)에서 아래 명령을 실행합니다.
dotnet add package Swashbuckle.AspNetCore |
위 명령은 Swashbuckle.AspNetCore package를 Project에 추가하는 명령으로서 자세한 사항은 추후에 살펴보겠지만 여기서 중요한것은 dotnet add package이라는 명령이 주는 효과입니다. 이 명령으로 인해 새로운 종속성이 아래와 같이 csproj file에 추가될 것입니다.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
</Project>
4. 사용자 정의 Middleware 생성
상술하였든 Microsoft는 ASP.NET Core를 위해 Web Application에서 필요로 하는 일반적인 기능을 처리하기 위한 다양한 Middleware Component를 제공하고 있습니다. 그런데 만약 원하는 적절한 Middleware가 존재하지 않는다면 이를 직접 만들어 볼 수도 있는데 Project에서 기본적인 Component만을 사용한다고 하더라도 실제 이 과정을 경험해 보는 것은 ASP.NET Core가 작동하는 방식을 이해하는데도 도움이 될 수 있습니다. Middleware를 만들기 위한 핵심 Method는 아래와 같이 Use를 사용하는 것입니다.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) => {
if (context.Request.Method == HttpMethods.Get && context.Request.Query["middle"] == "ware")
{
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync("My Middleware\n");
}
await next();
});
app.MapGet("/", () => "Hello World!");
app.Run();
Use는 Middleware Component를 등록하는 method이며, Middleware는 일반적으로 Pipeline을 통과할때 각 요청을 수신하는 Lambda함수로 표현됩니다.(Class에서 사용되는 다른 method 역시 존재합니다.)
Lambda 함수의 인수는 요청을 Pipeline을 통해 다른 Component에게 전달하도록 ASP.NET Core에 호출되는 HttpContext 개체와 Method가 됩니다.
HttpContext개체는 HTTP요청과 응답을 서술하고 요청과 관련된 User의 상세정보를 포함한 추가적인 context를 제공합니다. 아래표는 Microsoft.AspNetCore.Http Namespace에서 정의된 HttpContext class에서 가장 일반적인 Member를 나타내고 있습니다.
Connection | Local및 원격 IP주소와 Port를 포함해 HTTP요청에 대한 Network 연결정보를 제공하는 ConnectionInfo개체를 반환합니다. |
Request | 처리중인 HTTP요청을 서술하는 HttpRequest개체를 반환합니다. |
RequestServices | 요청에서 사용할 수 있는 Service로의 접근을 제공합니다. |
Response | HTTP요청에 대해 응답을 생성하는데 사용되는 HttpResponse개체를 반환합니다. |
Session | 요청에 할당된 Session data를 반환합니다. |
User | 요청에 할당된 User의 상세를 반환합니다. |
Features | 요청처리에 대한 저수준 측면으로의 접근을 허용하는 요청기능으로의 접근을 제공합니다. |
ASP.NET Core platform은 HttpRequest 개체를 생성하기 위해 HTTP 요청을 처리하는 역활을 하는 것으로 Middleware나 Component가 원시 요청 data에 관해서는 신경 쓰지 않아도 된다는 것을 의미합니다. 아래 표에서는 HttpContext class의 Request속성을 통해 가져올 수 있는 HttpRequest class의 가장 일반적인 Member를 나타내고 있습니다.
Body | 요청 Body를 읽기위해서 사용될 수 있는 stream을 반환합니다. |
ContentLength | Content-Length header의 값을 반환합니다. |
ContentType | Content-Type header의 값을 반환합니다. |
Cookies | 요청 cookie를 반환합니다. |
Form | 요청 body를 form으로 표현한것을 반환합니다. |
Headers | 요청 header를 반환합니다. |
IsHttps | 요청이 HTTPS이면 true를 반환합니다. |
Method | 흔히 HTTP method라고 하는 것으로 요청에서 사용되는 HTTP 동사를 반환합니다. |
Path | 요청 URL에 대한 Path Section을 반환합니다. |
Query | 키-값 쌍의 형태로 요청에 대한 query string section을 반환합니다. |
HttpContext class의 Response속성을 통해 가져올 수 있는 HttpResponse개체는 요청이 Pipeline을 통과할때 Client로 다시 전송되는 HTTP 응답을 서술합니다. 아래 표는 HttpResponse class에서 가장 일반적인 Member를 나타내고 있는데, ASP.NET Core platform은 가능한 한 쉽게 응답을 처리할 수 있도록 header를 자동적으로 설정하고 Client로 content를 쉽게 보낼 수 있도록 합니다.
ContentLength | Content-Length header의 값을 설정합니다. |
ContentType | Content-Type header의 값을 설정합니다. |
Cookies | Cookie를 응답에 연결할 수 있도록 합니다. |
HasStarted | ASP.NET Core가 Client에 응답 header를 전송한 이 후 상태 code가 header를 바꿀 수 없을때 true를 반환합니다. |
Headers | 응답 header를 설정합니다. |
StatusCode | 응답에 상태 Code를 설정합니다. |
WriteAsync(data) | 비동기 method로서 응답 body에 인수로 지정한 문자열을 작성합니다. |
Redirect(url) | 인수로 지정한 url로 Redirect 응답을 보냅니다. |
사용자 Middleware를 작성하는 경우 HttpContext, HttpRequest, HttpResponse 개체를 직접적으로 사용할 수 있지만 MVC Framework와 Razor Page같은 고수준 ASP.NET Core 기능을 사용하는 경우에는 일반적으로 필요하지 않습니다.
위 예제에서 작성한 Middleware기능은 HttpRequest개체를 사용해 HTTP method와 Query string을 확인하여 'ware'값과 같이 query string안에서 사용자 매개변수를 가진 GET요청인지를 식별하고 있습니다. HttpMethods class에서는 각 HTTP Method에 대한 정적 문자열을 정의하고 있습니다.
예상 Query string을 가진 GET 요청이 확인되면 Middleware는 ContentType속성을 사용하여 Content-Type header를 설정하고 WriteAsync method를 사용하여 응답 body에 문자열을 추가하고 있습니다.
Content-Type header를 설정하는 것은 다 Middleware Component가 응답 상태 Code와 Header를 설정할 수 있기 때문에 이를 방지하기 위해 중요한 부분입니다. ASP. NET Core는 항상 유효한 HTTP응답이 전송되었는지를 확인하는데 이로 인해 응답 header 또는 상태 Code가 이미 응답 body에 content가 이전 Component에 의해 작성된 후에 설정될 수 있으며 응답 body가 시작되기 전 header가 Client로 전송되어야 하기 때문에 예외를 일으킬 수 있습니다.
예제에서는 간단한 문자열만을 결과로서 browser에 전송하지만 추후에는 JSON data를 반환하는 Web service의 생성방식과 ASP.NET Core가 HTML응답을 생성할 수 있는 다양한 방식을 시도해 볼 것입니다.
Middleware로의 두번째 인수는 관례적인 next이름의 Method이며 ASP.NET Core가 요청 Pipeline에서 다음 Component에 요청을 전달할 수 있도록 합니다.
다음 Middleware Component를 호출할 때는 ASP.NET Core가 HttpContext개체와 자체 next method를 Component에 제공하므로 요청을 처리할 수 있기 때문에 인수를 필요로 하지 않습니다. next는 await keyword가 사용되었고 Lambda 함수가 async keyword로 정의되었기 때문에 비동기 Method임을 알 수 있습니다.
간혹 next() 대신 next.Invoke()를 대신 사용하는 경우가 있습니다. next()는 next.Invoke()의 단축된 형태로서 compiler가 적절히 처리해 주기 때문에 완전히 동일한 기능을 수행합니다.
아래 명령을 통해 예제를 실행하고
dotnet run |
/?middle=ware로 URL을 요청하여 새로운 Middleware component Method가 요청이 다음 Middleware component로 전달되기 전에 아래와 같이 message를 응답하는지 확인합니다.
Query string을 삭제하거나 true를 다른 값으로 변경하여 middleware component가 응답에 message를 추가하지 않고 요청을 전달하는지도 확인합니다.
(1) Class를 사용하는 Middleware정의하기
Middleware를 정의할때정의할 때 위 예제처럼 lambda 함수를 사용할 수 있지만 자칫 Program.cs에 너무 길고 복잡한 구문이 만들어질 수 있고 다른 Project에서 Middleware를 재사용하기 어려워질 수 있습니다. 때문에 별도의 Class를 만들어 Middleware를 정의할 때 사용할 수도 있습니다. Middleware class를 만들기 위해 Project에 Middleware.cs file을 아래와 같이 추가합니다.
namespace Platform;
public class QueryStringMiddleWare
{
private RequestDelegate next;
public QueryStringMiddleWare(RequestDelegate nextDelegate)
{
next = nextDelegate;
}
public async Task Invoke(HttpContext context)
{
if (context.Request.Method == HttpMethods.Get && context.Request.Query["middle"] == "ware")
{
if (!context.Response.HasStarted)
{
context.Response.ContentType = "text/plain";
}
await context.Response.WriteAsync("My Middleware Class\n");
}
await next(context);
}
}
Middleware classe는 생성자 매개변수로 RequestDelegate개체를 전달받는데 RequestDelegate는 요청을 Pipeline에서 다음 Component로 전달하는데 사용됩니다. Invoke method는 요청이 수신되면 ASP.NET Core에 의해 호출되며 lambda 함수 Middleware가 수신한 것과 동일한 Class를 사용하여 요청과 응답에 대한 접근을 제공하는 HttpContext개체가 주어집니다. 또한 RequestDelegate에서는 비동기로 동작하는 Task를 반환합니다.
Class기반 Middleware와 중요한 차이점중 하나는 RequestDelegate를 요청을 전달하기 위해 호출할 때 HttpContext개체를 인수로 사용해야 한다는 것입니다.
이렇게 만들어진 Class기반 Middleware는 type인수를 가진 UseMiddleware Method를 통해 Pipeline에 추가됩니다.
app.Use(async (context, next) => {
if (context.Request.Method == HttpMethods.Get && context.Request.Query["middle"] == "ware")
{
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync("My Middleware\n");
}
await next();
});
app.UseMiddleware<Platform.QueryStringMiddleWare>();
app.MapGet("/", () => "Hello World!");
ASP.NET Core가 시작되면 QueryStringMiddleware 개체가 생성되며(instance) Invoke method가 요청을 처리하기 위해 호출될 것입니다.
단일 Middleware가 모든 요청을 처리하는데 사용되므로 Invoke method의 Code는 반드시 thread에 안전해야 합니다.
dotnet run 명령을 통해 Project를 실행하고 /?middle=ware로 URL을 요청하여 다음과 같은 응답이 생성되는지 확인합니다.
(2) Pipeline path 반환
Middleware component는 next method가 호출되고 난 후 다음 예제와 같이 HTTPResponse개체를 변경할 수 있습니다.
app.Use(async (context, next) => {
await next();
await context.Response.WriteAsync($"\nStatus Code : {context.Response.StatusCode}");
});
app.Use(async (context, next) => {
if (context.Request.Method == HttpMethods.Get && context.Request.Query["middle"] == "ware")
{
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync("My Middleware\n");
}
await next();
});
예제에서 상위 Middleware는 먼저 next method를 호출하여 요청을 Pipeline에 따 전달하고 있으며 그다음 WriteAsync method를 통해 응답 Body에 문자열을 추가하고 있습니다. 이 것은 이상해 보일 수 있지만 Middleware가 요청 Pipeline을 따라 전달되기 전에 next method를 호출하기 전/후 필요한 문을 추가함으로써 Middleware가 응답을 변경할 수 있다는 것을 잘 보여주고 있습니다.
Middleware는 요청이 전달되기 전은 물론 요청이 다른 Component에 의해 처리된 이후라도 작동할 수 있으며 둘다 모두인 경우도 동작할 수 있습니다. 그 결과 여러 Middleware component는 응답에 일부 측면을 제공하거나 이후 Pipeline에서 사용된 기능 또는 data를 제공함으로써 생성된 응답에 기여하게 됩니다.
Project를 실행하고 localhost:8000으로 URL을 요청하여 아래와 같은 응답이 생성되는지 확인합니다.
(3) 요청 Pipeline 단락
완료응답을 생성하는 Component의 경우에는 next method를 호출하지 않음으로서 요청이 전달되지 않도록 할 수 있습니다. 요청을 전달하지 않는 Component는 요청을 단락 한다라고 말할 수 있으며 아래 예제에서의 새로운 Middleware가 /short URL에 대해서 동작하는 것이기도 합니다.
app.Use(async (context, next) => {
await next();
await context.Response.WriteAsync($"\nStatus Code : {context.Response.StatusCode}");
});
app.Use(async (context, next) => {
if (context.Request.Path == "/short")
await context.Response.WriteAsync($"Request Short Circuited");
else
await next();
});
위 예제의 Middleware는 HttpRequest개체의 Path속성을 확인하여 요청이 /short URL인지 아닌지를 확인한뒤 결과가 true가 되면 next method의 호출 없이 WriteAsync method만을 호출하도록 합니다. 새로운 Middleware의 동작을 확인하기 위해 예제를 실행하고 /short?middle=ware로 URL을 요청하여 아래와 같은 결과가 생성되는지 확인합니다.
비록 URL이 Pipeline에서 다음 Component에 해당하는 query string 매개변수를 가지고 있었지만 요청은 더이상 전달되지 않았고 다음 Component는 동작하지 않았습니다. 하지만 Pipeline에서 이전 Component가 응답에 Message가 추가함에 따라 위와 같은 결과가 만들어지게 되었는데 이를 통해 우리는 단락이 적용된 시점의 Middleware부터 Pipeline을 따 그 이후의 Component에서만 단락이 적용된다는 것을 알 수 있습니다.
(4) Pipeline 분기 생성
특정한 URL에서만 요청을 처리해야 하는 Pipeline영역을 생성하고 그 안에 별도의 Middleware component 열을 만들려면 Map method를 사용합니다.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Map("/branch", branch => {
branch.UseMiddleware<Platform.QueryStringMiddleWare>();
branch.Use(async (HttpContext context, Func<Task> next) => {
await context.Response.WriteAsync($"Separate Branch Middleware");
});
});
app.UseMiddleware<Platform.QueryStringMiddleWare>();
app.MapGet("/", () => "Hello World!");
app.Run();
Map method의 첫번째 인수로는 처리할 대상이 될 URL을 지정하며 두 번째 인수로 Use 및 UseMiddleware method로 Middleware를 추가할 Pipeline의 branch를 지정합니다.
위 예제에서는 /branch로 시작하는 URL에 사용될 branch를 생성하고 있으며 이전 예제에서 정의한 QueryStringMiddleware class와 응답에 문자열을 추가하는 Middleware lambda 표현식으로 요청을 전달하고 있습니다. 아래 그림은 요청 Pipeline상에서 branch가 동작하는 방식을 나타내고 있습니다.
URL이 Map method에 지정한 URL과 일치하게 되면 branch로 요청이 흘러가게 됩니다. 예제를 보면 branch안애 마지막 Component는 next method를 호출하지 않고 있습니다. 이는 요청이 Pipeline을 통과하는 주요 경로상에서 Middleware component를 통과하지 않는다는 것을 의미합니다.
예제에서 볼 수 있는 것처럼 QueryStringMiddleWare class가 Pipeline의 주요경로에서 그리고 branch에서 사용되고 있습니다. 같은 Middleware가 Pipeline내부의 다른 영역에서 사용될 수 있음을 알 수 있습니다.
요청이 다르게 처리되는 상황을 확인하기 위해 Project를 실행하고 /?middle=ware로 URL을 요청합니다. 그러면 요청은 Pipeline이 주요 경로를 통해 전달되어 다음과 같은 결과를 나타낼 것입니다.
이번에는 /branch/?middle=ware로 URL을 요청합니다. 그러면 요청은 branch내부의 Middleware로 전달되어 다음과 같은 결과를 나타낼 것입니다.
조건을 통한 분기
ASP.NET Core에서는 MapWhen method를 통해 술어를 사용하여 요청을 일치시킬 수 있으며 이를 통해 단지 URL에서만이 아니라 조건에 따라 Pipeline branch에 요청이 선택될 수 있습니다.
MapWhen method의 인수로는 HttpContext 수신하고 branch로 흘러가야 할 요청에 대해 true를 반환하는 조검 함수와 IApplicationBuilder개체를 수신하여 Middleware 추가되는 pipeline branch를 표현할 함수가 사용됩니다.
app.MapWhen(context => context.Request.Query.Keys.Contains("branch"), branch => { //여기에 Middleware Component를 추가함 } );
상기 예제의 조건 함수는 Query string이 branch라는 문자열을 포함하는 요청에 대한 branch로 true를 반환합니다. IApplicationBuilder interface에 대한 cast는 필요하지 않은데 이는 MapWhen method가 하나만 정의되었기 때문입니다.
(5) Terminal Middleware
Terminal Middleware는 요청을 다른 component로 전달하지 않으며 요청 Pipeline의 끝을 표시하는 것으로 예를 들어 Program.cs에서 아래와 같이 정의할 수 있습니다.
branch.Use(async (context, next) => {
await context.Response.WriteAsync($"Branch Middleware");
});
ASP.NET Core은 Terminal Middleware를 생성하기 위해 Run method를 사용할 수 있으며 이를 통해 Middleware component가 요청을 요청을 전달하지 않으며 고의적으로라도 next method를 사용할 수 없게 됩니다. 아래 예제는 Pipeline branch안에서 Terminal Middleware를 위해 Rum method를 사용한 예를 나타내고 있습니다.
app.Map("/branch", branch => {
branch.UseMiddleware<Platform.QueryStringMiddleWare>();
branch.Run(async (context) => {
await context.Response.WriteAsync($"Separate Branch Middleware");
});
});
Rum method에 전달된 Middleware는 HttpContext 개체만들 전달받고 있으며 때문에 사용되지 않는 다른 매개변수를 정의할 필요가 없습니다. 내부적으로 Run method는 Use method를 통해 구현되며 편의상 제공되는 기능입니다.
Terminal Component이후에 Pipeline에 추가된 Middleware는 절대 요청을 수신할 수 없습니다. Pipeline 끝단 이전에 Terminal Component를 추가하면 ASP.NET Core는 이에 대한 경고를 하지 않습니다.
Class기반 Component에서도 역시 아래와 같이 일반적인 Component와 Terminal Component를 모두 사용할 수 있습니다.
namespace Platform;
public class QueryStringMiddleWare
{
private RequestDelegate? next;
public QueryStringMiddleWare()
{
//next = nextDelegate;
}
public QueryStringMiddleWare(RequestDelegate nextDelegate)
{
next = nextDelegate;
}
public async Task Invoke(HttpContext context)
{
if (context.Request.Method == HttpMethods.Get && context.Request.Query["middle"] == "ware")
{
if (!context.Response.HasStarted)
{
context.Response.ContentType = "text/plain";
}
await context.Response.WriteAsync("My Middleware Class\n");
}
//await next(context);
if (next != null)
await next(context);
}
}
예제에서 Component는 nextDelegate매개변수에 대한 비 null값을 가진 생성자가 제공되는 경우에만 요청을 전달할 것입니다. 아래예제는 표준과 Terminal형 모두에서 Component의 적용을 나타내고 있습니다.
..생략
var app = builder.Build();
app.Map("/branch", branch => {
branch.Run(new Platform.QueryStringMiddleWare().Invoke);
});
app.UseMiddleware<Platform.QueryStringMiddleWare>();
..생략
Terminal Middleware에 대한 동일한 UseMiddleware method는 존재하지 않습니다. 따라서 Run method는 Middleware class의 새로운 instance를 생성하고 Invoke method를 사용해야 합니다. Run method의 사용은 Middleware로부터의 출력을 변경하지 않습니다. 이는 예제를 실행하고 /branch?middle=ware로 URL을 요청함으로써 확인할 수 있습니다.
5. Middleware 구성하기
Middleware를 구성하기 위한 가장 일반적인 pattern으로 Options pattern이 있으며 추후에 설명할 일부 내장 Middleware Component에 의해 사용됩니다.
가장먼저 필요한 것은 Middleware component에 대한 option 구성을 포함하는 Class를 정의하는 것입니다. Project folder에 MiddlewareOptions.cs라는 이름이 file을 아래와 같이 추가합니다.
namespace Platform;
public class MiddlewareOptions
{
public string Category { get; set; } = "Packages";
public string Transportation { get; set; } = "Air";
}
위 예제는 운반할 종류의 Category와 운송수단인 Transportation이라는 속성을 정의하고 있는 Class file입니다. 이를 Class를 통해 아래와 같이 해당 Class와 관련된 사용자 정의 Middleware Component를 options pattern을 통해 생성할 수 있습니다. 참고로 간결함을 위해 이전 예제에서 추가된 일부 Middleware Component는 제거하였습니다.
using Platform;
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<MiddlewareOptions>(options => {
options.Transportation = "Truck";
});
var app = builder.Build();
app.MapGet("/trans", async (HttpContext context,
IOptions<MiddlewareOptions> msgOpts) => {
Platform.MiddlewareOptions opts = msgOpts.Value;
await context.Response.WriteAsync($"{opts.Category}, " + opts.Transportation);
});
app.MapGet("/", () => "Hello World!");
app.Run();
Option은 WebApplicationBuilder class에 정의되어 있는 Services.Configure method를 통해 Generic 매개변수를 사용하여 정의됩니다.
예제에서는 MiddlewareOptions class를 사용하여 Option을 생성하고 있으며 Transportation의 값을 변경하고 있습니다. Application이 시작되면 ASP.NET Core platform은 MiddlewareOptions class에 대한 새로운 intance를 생성하고 이를 Configure method의 인수로 제공된 method에 전달하여 Option값을 바꿀 수 있도록 합니다.
Option은 Service로서도 사용할 수 있는데 이때는 해당 구문이 예제에서 처럼 Build method가 호출되기 전에 들어가야 합니다.
Middleware Component는 요청을 처리할 method의 매개변수로 정의함으로써 Option설정에 접근할 수 있습니다.
app.MapGet("/trans", async (HttpContext context,
IOptions<MiddlewareOptions> msgOpts) => {
Middleware Component를 등록하기 위해 사용되는 일부 확장 method는 요청을 처리하는 모든 method를 허용합니다. 요청이 처리되면 ASP.NET Core platform은 Method를 확인하여 service가 필요한 매개변수를 찾고 Middleware가 생성하는 응답에서 구성 option을 사용할 수 있도록 합니다.
Platform.MiddlewareOptions opts = msgOpts.Value;
await context.Response.WriteAsync($"{opts.Category}, " + opts.Transportation);
이는 추후설명할 의존성 주입에 대한 하나의 예가 될 수 있습니다. Project를 실행하여 /trans로 URL을 요청하여 아래와 같은 응답이 생성되는지 확인합니다. 이를 통해 우리는 Middleware component가 Options pattern을 사용하는 방식을 확인해 볼 수 있습니다.
(1). Class기반 Middleware에 Options pattern사용하기
Options pattern은 Class기반 Middleware에서도 사용될 수 있으며 비슷한 방법으로 적용할 수 있습니다. Middleware.cs file에 아래와 같이 구성에 MiddlewareOptions를 사용하는 Class기반 Middleware component를 정의합니다.
..생략
public class QueryStringMiddleWare
{
..생략
}
public class TransMiddleware {
private RequestDelegate next;
private MiddlewareOptions options;
public TransMiddleware(RequestDelegate nextDelegate, IOptions<MiddlewareOptions> opts) {
next = nextDelegate;
options = opts.Value;
}
public async Task Invoke(HttpContext context) {
if (context.Request.Path == "/trans")
await context.Response.WriteAsync($"{options.Category}, " + options.Transportation);
else
await next(context);
}
}
..생략
TransMiddleware는 IOptions<MiddlewareOptions> 생성자 매개변수를 정의하여 Invoke method에서 Options설정에 접근할 수 있도록 하고 있습니다. 이를 통해 Program.cs에서는 다음과 같이 요청 Pipeline을 lambda 함수 middleware component로 바꿀 수 있습니다.
..생략
var app = builder.Build();
app.UseMiddleware<TransMiddleware>();
app.MapGet("/", () => "Hello World!");
..생략
UseMiddleware문이 샐행될때 MiddlewareOptions 생성자를 확인하게 되면서 Services.Configure method에서 생성된 개체를 사용해 IOptions<MiddlewareOptions> 매개변수가 Resolve 될 것입니다. 이는 의존성 주입기능을 사용해 수행되지만 그것보다 Options pattern이 Class기반 middleware를 쉽게 구성하는 데 사용될 수 있다는 것을 알 수 있습니다.
예제를 실행하고 /trans로 URL을 요청하여 위에서 추가한 Middleware가 아래와 같은 응답을 생성하는지 확인합니다.
'.NET > ASP.NET' 카테고리의 다른 글
[ASP.NET Core] - 13. 의존성 주입 (0) | 2024.07.09 |
---|---|
[ASP.NET Core] - 12. URL Routing (0) | 2024.06.11 |
[ASP.NET Core] - 10. Shopping mall project 만들기 - 5 (2nd) (0) | 2024.05.04 |
[ASP.NET Core] - 9. Shopping mall project 만들기 - 4 (2nd) (0) | 2024.04.22 |
[ASP.NET Core] - 8. Shopping mall project 만들기 - 3 (2nd) (0) | 2024.04.19 |