URL Routing기능을 사용하면 요청 URL의 Matching과 처리를 통합하여 응답을 좀 더 쉽게 생성할 수 있습니다. URL Routing기능을 이해하기 위해 ASP.NET Core platform이 URL Routing을 지원하는 방식을 알아보고 직접 사용해 볼 것입니다. 또한 이를 통해 Middleware component의 생성을 어떻게 대체할 수 있는지도 알아볼 것입니다
1. Project 준비하기
예제 Project는 이전에 사용하던 Platform project를 계속 사용할 것이며 추가로 Trans.cs이름의 file을 아래와 같이 추가합니다.
namespace Platform;
public class Trans {
private RequestDelegate? next;
public Trans() { }
public Trans(RequestDelegate nextDelegate)
{
next = nextDelegate;
}
public async Task Invoke(HttpContext context)
{
string[] paths = context.Request.Path.ToString().Split("/", StringSplitOptions.RemoveEmptyEntries);
if (paths.Length == 2 && paths[0] == "trans") {
string vehicle = paths[1];
int? cost = null;
switch (vehicle.ToLower()) {
case "air":
cost = 1_500_000;
break;
case "truck":
cost = 500_000;
break;
case "sedan":
cost = 300_000;
break;
}
if (cost.HasValue) {
await context.Response.WriteAsync($"Vehicle: {vehicle}, Cost: {cost}");
return;
}
}
if (next != null) {
await next(context);
}
}
}
위 예제의 Middleware component는 /trans/(vehicle) 형태의 요청에 응답하며 vehicle에는 air, truck, sedan이 올 수 있습니다. 예제는 URL을 나누어 처리해야할 값의 길이를 확인하고 switch문을 사용해 요청에 응답할 수 있는 곳을 결정합니다. 응답은 Middleware가 찾고 있는 URL pattern과 일치하는 경우 생성되고 그렇지 않으면 요청은 Pipeline을 따라 넘어가게 됩니다.
계속해서 Vehicle.cs이름의 file을 아래와 같이 추가합니다.
namespace Platform;
public class Vehicle
{
private RequestDelegate? next;
public Vehicle() { }
public Vehicle(RequestDelegate nextDelegate)
{
next = nextDelegate;
}
public async Task Invoke(HttpContext context)
{
string[] paths = context.Request.Path.ToString().Split("/", StringSplitOptions.RemoveEmptyEntries);
if (paths.Length == 2 && paths[0] == "vehicle") {
string? type = null;
string vehicle = paths[1];
switch (vehicle.ToLower())
{
case "air":
type = "People and cargo";
break;
case "truck":
type = "cargo";
break;
case "sedan":
context.Response.Redirect($"/trans/{vehicle}");
return;
}
if (type != null) {
await context.Response.WriteAsync($"{vehicle} Transportable items : {type}");
return;
}
}
if (next != null) {
await next(context);
}
}
}
위 예제는 /vehicle/(vehicle)에 대한 요청에 응답하며 vehicle에는 역시 air, truck, sedan이 올 수 있습니다. 이때 air와 truck에 대해서는 brand를 표시하지만 sedan은 /trans/sedan으로 Redirect를 수행하게 됩니다.
Program.cs에서는 이전의 Middleware component를 제거하고 위 새로운 Middleware component를 Pipeline에 추가합니다.
using Platform;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseMiddleware<Trans>();
app.UseMiddleware<Vehicle>();
app.Run(async (context) => {
await context.Response.WriteAsync("Middleware terminal");
});
app.Run();
예제를 실행하여 /trans/air로 URL을 요청하여 아래와 같은 응답이 생성되는지 확인합니다.
또한 /vehicle/air URL을 통해서도 응답을 확인합니다.
(1) URL Routing
각각의 Middleware component는 Pipeline으로 전달된 요청에 따라 동작할지의 여부를 결정합니다. 일부 Component의 경우 특정 header나 query string의 값을 찾는 경우도 있지만 대부분의 경우, 특히 Terminal과 단락 Component는 URL자체를 확인하는 경우가 많습니다.
각각의 Middleware component는 요청이 Pipeline을 따라 진행함에 따라 동일한 절차를 반복해야 합니다. 이것은 위 예제를 통해서도 확인할 수 있는데 예제로 작성한 2개의 Middleware component는 모두 URL을 나누고 URL의 각 부분에 대한 수를 확인한 뒤 특정한 부분의 값을 확인하는 동일한 처리를 진행하고 있습니다.
이러한 접근방식은 각각의 Middleware component에서 URL을 처리하기 위해 동일한 동작을 반복하고 있기 때문에 그다지 효휼적이라고 할 수는 없으며 각 Component에서 처리해야 할 URL이 Code안에 숨겨져 있기 때문에 유지관리하기에도 어렵고 여러 곳에서 신중하게 변경작업이 이루어져야 하기 때문에 쉽게 문제를 일으킬 수 있습니다. 예를 들어 Vehicle component는 Trans Component에서 처리되는 /trans으로 Redirect를 수행하는 부분이 있는데, 만약 Trans Component가 /Transpotation으로 URL이 처리되도록 변경된다면 이 변경사항은 Vehicle Component에도 그대로 반영되어야 합니다. 실제 Application에서도 복잡한 일련의 URL을 지원하도록 개발되는 경우, 각각의 개별 Component에서 이러한 부분이 완벽하게 반영되도록 하는 것은 매우 어려운 작업에 속합니다.
URL Routing은 요청 URL을 연결하는 middleware를 도입하여 Endpoint라고 하는 Component가 응답에 집중할 수 있게 함으로서 이러한 문제를 해결할 수 있습니다. Endpoint와 그들이 필요로 하는 URL간의 연결은 Route로 표현할 수 있으며 Routing middleware는 URL을 처리하고 Route설정을 확인하여 요청을 처리할 Endpoint를 찾게 되는데 이를 Routing이라고 합니다.
(2) Routing middleware와 Endpoint 정의하기
Routing middleware는 2가지 method를 통해 추가되는데 하나는 UseRouting이고 다른 하는 UseEndPoint입니다. UseRouting은 Pipeline으로의 요청을 처리할 Middleware를 추가하는 것이고 UseEndPoint는 Endpoint와 연결되는 URL인 Route를 정의하기 위해 사용됩니다. 여기서 URL은 요청 URL의 경로를 비교하는 Pattern을 사용해 연결되며 각 Route는 URL Pattern과 Endpoint를 1:1로 관계로 설정합니다. 아래 예제는 Routing middleware와 하나의 간단한 Route를 포함하는 경우를 나타내고 있습니다.
..생략
app.UseMiddleware<Vehicle>();
app.UseRouting();
app.UseEndpoints(endpoints => {
endpoints.MapGet("routing", async context => {
await context.Response.WriteAsync("Request Was Routed");
});
});
app.Run(async (context) => {
await context.Response.WriteAsync("Middleware terminal");
});
..생략
UseRouting method는 인수가 없으며 UseEndpoints는 IEndpointRouteBuilder개체를 전달받아 이를 사용해 아래 표의 확장 method를 사용하는 route를 생성하고 있습니다.
위 예제는 실제 Code Editor상에서 UseEndpoints부분에 경고가 발생할 수 있습니다. 또한 MVC Framework와 같이 ASP.NET Core의 다른 부분에 대한 Endpoint를 설정하는 확장 method도 존재하는데 이들에 대한 것은 곧 별도로 알아볼 것입니다.
MapGet(pattern, endpoint) | 해당 method는 URL Pattern과 일치하는 HTTP GET 요청을 Endpoint로 route합니다. |
MapPost(pattern, endpoint) | 해당 method는 URL Pattern과 일치하는 HTTP POST 요청을 Endpoint로 route합니다. |
MapPut(pattern, endpoint) | 해당 method는 URL Pattern과 일치하는 HTTP PUT 요청을 Endpoint로 route합니다. |
MapDelete(pattern, endpoint) | 해당 method는 URL Pattern과 일치하는 HTTP DELETE 요청을 Endpoint로 route합니다. |
MapMethods(pattern, methods, endpoint) | 해당 method는 URL Pattern과 일치하는 이정된 HTTP Method중 하나를 사용하여 이루어진 요청을 Endpoint로 route합니다. |
Map(pattern, endpoint) | 해당 method는 URL Pattern과 일치하는 모든 HTTP 요청을 Endpoint로 route합니다. |
Endpoint는 기존 Middleware에서 사용된 동일한 delegate인 RequestDelegate를 사용해 정의되므로 HttpContext 개체를 수신하는 비동기 method가 되며 이를 응답을 생성하는 데 사용하게 됩니다. 즉 아래 글에서 설명한 Middleware component에 대한 기능이 Endpoint에서도 사용될 수 있다는 것입니다.
예제를 실행하고 /routing으로 URL을 요청하여 위 예제의 route가 제대로 작동하는지 확인합니다.
요청이 일치하게 되면 Routing Middleware는 route의 URL pattern을 URL 경로에 적용하며 이때 경로는 아래와 같이 /문자를 통해 hostname과 분리됩니다.
http://localhost:8000/routing |
그리고 해당 URL경로는 아래와 같이 route에 지정된 pattern과 일치하게 됩니다.
endpoints.MapGet("routing", async context => {
이때 URL pattern은 관례적으로 시작하는 /문자는 제외하고 표현되며 이 부분은 URL경로의 일부로 취급되지 않습니다. 요청 URL경로가 URL Pattern과 일치하게 되면 요청은 Endpoint로 전달되며 위 화면과 같은 응답을 생성하게 됩니다.
Routing Middleware는 route가 URL과 일치할때 Pipeline을 단락 시키게 되므로 응답은 Route의 Endpoint에서만 생성되며 요청은 요청 Pipeline의 후반부에 나타나는 다른 Endpoint나 Middleware component로 전달되지 않습니다.
만약 요청 URL이 어떠한 route와도 일치하지 않는다면 Routing Middleware는 요청을 요청 Pipeline의 다음 Middleware component로 전달합니다. 이 동작을 확인해 보기 위해 예제를 실행하고 /route URL로 요청을 시도합니다. 해당 URL은 위 예제에서 어떠한 Route의 어떠한 Pattern에도 일치하지 않습니다.
Routing Middleware는 URL경로를 route와 일치시킬 수 없으므로 요청을 밑에 있는 단말 Middleware로 전달하여 아래와 같은 응답을 표시할 것입니다.
Endpoint는 Middleware component와 같은 방법으로 응답을 생성합니다. HttpRequest와 HttpResponse 개체를 통해 요청 및 응답에 대한 접근을 제공하는 HttpContext 개체를 수신합니다. 바꿔 말하면 모든 Middleware Component는 Endpoint로서 사용될 수 있습니다. 아래 예제는 이전에 사용된 Trans와 Vehicle Component를 Endpoint로서 사용하는 route를 어떻게 추가할 수 있는지를 보여주고 있습니다.
// app.UseMiddleware<Trans>();
// app.UseMiddleware<Vehicle>();
app.UseRouting();
app.UseEndpoints(endpoints => {
endpoints.MapGet("routing", async context => {
await context.Response.WriteAsync("Request Was Routed");
});
endpoints.MapGet("trans/air", new Trans().Invoke);
endpoints.MapGet("vehicle/truck", new Vehicle().Invoke);
});
위와 같이 Middleware Component를 사용하는 것은 Endpoint로서 Class의 새로운 intance를 생성하고 Invoke method가 호출되도록 해야하기 때문에 어색할 수 있습니다. Route에서 사용하는 URL Pattern은 Middleware component에서 지원하는 URL의 일부만 지원합니다. 예제를 실행하고 /trans/air와 /vehicle/truck URL을 요청하여 아래와 같은 응답이 생성되는지 확인합니다.
(3) Pipeline 구성 간소화 하기
이전 예제를 통해 UseRouting와 UseEndpoints method를 사용해본것은 표준 Pipeline 기능상에서 그리고 일반적인 Middleware Component를 통해 Routing이 구현되는 것을 알아보기 위한 목적이었습니다.
하지만 Microsoft는 ASP.NET Core Application의 구성을 간소화하기 위해 자동적으로 UseRouting와 UseEndpoints method를 Pipeline에 적용합니다. 다시 말해 위에서 나타낸 표에 해당하는 method들은 WebApplication CreateBuilder method에 의해 반환되는 WebApplication 개체상에서 직접적으로 사용될 수 있습니다.
또한 이전 예제에서 UseEndpoints method를 호출할때 C# code 분석기는 Program.cs file의 top level에서 route등록을 제안하는 경고를 생성했는데, 아래와 같은 구현에서는 이러한 경고 역시 나타나지 않습니다.
// app.UseEndpoints(endpoints => {
// endpoints.MapGet("routing", async context => {
// await context.Response.WriteAsync("Request Was Routed");
// });
// endpoints.MapGet("trans/air", new Trans().Invoke);
// endpoints.MapGet("vehicle/truck", new Vehicle().Invoke);
// });
app.MapGet("routing", async context => {
await context.Response.WriteAsync("Request Was Routed");
});
app.MapGet("trans/air", new Trans().Invoke);
app.MapGet("vehicle/truck", new Vehicle().Invoke);
// app.Run(async (context) => {
// await context.Response.WriteAsync("Middleware terminal");
// });
WebApplication class는 IEndpointRouteBuilder interface를 구현하는데 이를 통해 Endpoint는 더욱 간결하게 생성될 수 있습니다. 이면에서 Routing Middleware는 여전히 요청을 연결하고 Route를 선택하는 동작이 가능합니다.
예제를 실행하고 /trans/air와 /vehicle/truck URL을 요청하여 이전과 동일한 응답이 생성되는지 확인합니다.
직접 Route를 등록할때의 위험성
상기예제를 보면 Pipeline에서 Terminal Middleware Component를 제거했음을 확인할 수 있습니다. Pipeline에 명시적으로 추가한 Routing Middleware는 일치하는 Route가 존재하지 않을 때 요청을 Pipeline을 따라 전달할 수도 있습니다. 위 예제처럼 Route를 직접적으로 등록하게 되면 이러한 동작을 바꾸게 되고 결국 요청이 어떤 경우든 전달하도록 만들게 됨으로써 Terminal Middleware가 모든 요청에 응답하는 상황이 발생할 수 있습니다.
추후 Terminal Middleware에 대한 대안을 볼 기회가 있겠지만, 지금은 간소화된 Pipeline구성이 단지 Program.cs에서 Code의 양을 줄여주는 것 뿐만이 아니라 요청이 처리되는 방식을 변경할 수도 있다는 점을 이해하는 것이 중요합니다.
(4) URL Pattern
Endpoint로 Middleware component를 사용하게 되면 URL Routing이 표준 ASP.NET Core platform 기능에 의해 구축된다는 것을 알 수 있습니다. 물론 Route를 확인해 보면 Application에서 처리하는 모든 URL을 확인할 수 있지만 Trans나 Vehicle class로 들어가는 모든 URL이 Route 되는 것은 아니며 Routing middleware가 Route를 선택하기 위해 URL을 한 번은 처리하고 Trans와 Vehicle class가 필요한 data값을 추출하기 위해 다시 한번 처리하기 때문에 그다지 효율성이 좋다고는 할 수 없습니다.
이 문제를 개선하려면 어떻게 URL Pattern이 사용되는지에 대해 충분한 이해가 필요합니다. 요청이 도달하면 Routing Middleware는 경로(Path)로 부터 segment값을 추출하기 위해 URL을 처리하게 됩니다. 이때 segment는 전체 경로에서 '/'문자로 분리된 영역을 의미합니다.
http://localhost:8000 | /trans | /air |
첫번째 segment | 두번째 segment |
Routing Middleware에서는 또한 URL Routing Pattern으로 부터 아래와 같이 segment를 추출합니다.
app.MapGet("trans/air", new Trans().Invoke);
요청을 Route하기 위해 URL Pattern에서 segment를 요청과 비교하여 연결여부를 확인합니다. 경로가 동일한 segment 수를 포함하고 있고 각 segment가 URL Pattern과 동일한 content를 가진다면 요청은 Endpoint로 Route 됩니다.(아래 표 참고)
/trans | 연결불가 | Segment 수가 부족함 |
/trans/air/car | 연결불가 | Segment 수가 너무 많음 |
/sedan/trans | 연결불가 | 첫번째 Segment와 일치하는 Route가 없음 |
/trans/air | 연결 | 일치함 |
(5) URL Pattern에서 segment 변수 사용하기
위 예제에서 사용된 URL Pattern은 실제 값 그대로를 사용한 것으로 정적 segment라고도 하며 요청을 고정된 문자열 값으로 연결합니다. 예를 들어 Pattern에서 첫번째 segment trans 경로에 대한 요청과 일치할 것이며 두 번째 segment는 두 번째 segment가 air인 요청과 일치할 것입니다. 따라서 이 경로를 함께 놓고 보면 /trans/air인 요청과 일치됨을 알 수 있습니다.
Segment변수는 route 매개변수라고도 하며 pattern segment가 일치할 경로 segment의 범위를 확장하여 좀더 유연성 있는 routing을 가능하게 합니다. Segment변수는 아래와 같이 변수의 이름과 함께 중괄호를 사용해 표현됩니다.
..생략
var app = builder.Build();
app.MapGet("{first}/{second}/{third}", async context => {
await context.Response.WriteAsync("Request Was Routed\n");
foreach (var kvp in context.Request.RouteValues) {
await context.Response.WriteAsync($"{kvp.Key}: {kvp.Value}\n");
}
});
app.MapGet("trans/air", new Trans().Invoke);
app.MapGet("vehicle/truck", new Vehicle().Invoke);
..생략
예제의 URL pattern인 {first}/{second}/{third}는 3개의 segment를 포함하는 URL하고만 일치됩니다. 예제의 Routing Middleware Component는 일치된 URL 경로 segment와 함께 endpoint의 content와 함께 제공하고 있고 그 안에서 Segment변수를 사용하고 있습니다. Content는 RouteValuesDictionary 개체를 반환하는 HttpRequest .RouteValues속성을 통해 사용할 수 있습니다. 아래 표에서는 RouteValuesDictionary개체를 통해 사용할 수 있는 유용한 몇 가지 Member들을 소개학 있습니다.
Segment변수의 이름으로 action, area, controller, handler, page는 사용할 수 없습니다.
[key] | Key로 값을 검색할 수 있는 indexer를 정의합니다. |
Keys | Segment 변수 이름에 대한 Collection을 반환하는 속성입니다. |
Values | Segment 변수 값에 대한 Collection을 반환하는 속성입니다. |
Count | Segment 변수의 수를 반환하는 속성입니다. |
ContainsKey(key) | Route data가 특정 key에 대한 값을 포함하는 경우 true를 반환합니다. |
RouteValuesDictionary class는 열거형이므로 foreach loop등의 문을 통하여 Segment변수의 이름과 요청 URL로부터 추출된 값에 해당하는 KeyValuePair<string, object>개체의 배열을 생성할 수 있습니다. 예제의 Endpoint는 HttpRequest.RouteValues속성을 열거하고 있는데 이를 통해 URL pattern과 일치하는 Segment변수의 값과 이름에 해당하는 응답을 생성하고 있습니다.
예제에서 사용된 Segment 변수의 이름은 first, second, third입니다. 예제를 실행하여 /apple/banana/mango과 같이 URL을 요청하면 해당 URL로 부터 추출된 값을 아래와 같이 직접 확인할 수 있습니다.
Route 선택방식
요청이 처리될때 Middleware는 요청과 일치되는 모든 Route를 찾아 이들에게 점수를 부여하고 가장 낮은 점수를 가진 Route를 선택하게 됩니다. 점수를 부여하는 과정은 복잡하지만 결과적으로 가장 구체적인 Route가 요청을 수신하게 됩니다. 즉, 실제 Segment가 Segment변수보다 우선권을 가지며 제약 조을 가진 Segment변수가 그렇지 않은 것보다 우선권을 가지게 됩니다.(제약 조건에 대해서는 잠시 후 알아볼 것입니다.) 이러한 점수 부여 Sytem은 의외의 결과를 만들어낼 수 있으므로 사용하고자 하는 URL이 예상한 Route와 정학하게 일치하는지를 확인하는 것이 좋습니다.
2개의 Route가 만약 같은 점수를 가지게 된다면 요청을 어디로 Route해야 할지를 알 수 없기 때문에 이때는 모호한 Route와 관련된 예외가 발생하게 됩니다.
● Endpoint로 Middleware 재작성 하기
Endpoint는 일반적으로 모든 Segment변수를 열거하는 대신 Middleware Component를 통해 특정 Segment 변수를 제공합니다. 아래 예제는 이러한 방식으로 URL Pattern에 의존해 특정 값을 제공하도록 Route Data에 따라 Trans와 Vehicle class를 재작성한 것입니다.
namespace Platform;
public class Trans {
public static async Task Endpoint(HttpContext context)
{
string? trans = context.Request.RouteValues["trans"] as string;
int? cost = null;
switch ((trans ?? string.Empty).ToLower()) {
case "air":
cost = 1_500_000;
break;
case "truck":
cost = 500_000;
break;
case "sedan":
cost = 300_000;
break;
}
if (cost.HasValue) {
await context.Response.WriteAsync($"Vehicle: {trans}, Cost: {cost}");
}
else
{
context.Response.StatusCode = StatusCodes.Status404NotFound;
}
}
}
예제에서는 RouteValuesDictionary class에 정의된 indexer를 통해 vehicle이름의 Segment변숫값을 가져오고 있습니다. 이때 Indexer는 object를 반환하기 때문에 이를 as keyword를 사용해 string으로 cast 해야 합니다. 또한 Endpoint를 대신하여 처리하는 Routing Middleware인 Pipeline을 따라 요청을 전달하는 구문이 제거되었습니다.
그리고 Segment변수를 사용하는 경우 Endpoint가 필요로 하지 않는 값을 가진 요청이 Endpoint로 Route 될 수 있기 때문에 이에 대한 대비로 404가 상태 code값을 반환하도록 하였습니다.
마지막으로 생성자를 제거하고 Invoke Instance method를 Endpoint라는 이름의 정적 method로 변경하였는데 이는 Endpoint가 Route에서 사용되기에 적합한 방식이라고 할 수 있습니다. 아래 예제는 Vehicle class에도 위와 동일한 변경을 시도한 것으로 표준 Middleware Component를 Routing Middleware에 따라 URL을 처리하도록 하는 Endpoint로 변환한 것입니다.
namespace Platform;
public class Vehicle
{
public static async Task Endpoint(HttpContext context)
{
string? type = null;
string? vehicle = context.Request.RouteValues["vehicle"] as string;
switch ((vehicle ?? string.Empty).ToLower())
{
case "air":
type = "People and cargo";
break;
case "truck":
type = "cargo";
break;
case "sedan":
context.Response.Redirect($"/trans/{vehicle}");
return;
}
if (type != null)
await context.Response.WriteAsync($"{vehicle} Transportable items : {type}");
else
context.Response.StatusCode = StatusCodes.Status404NotFound;
}
}
위와 같은 정적 method의 변경으로 인해 Route를 정의할 때 아래와 같이 Endpoint를 좀 더 간결하게 사용할 수 있게 되었습니다.
..생략
app.MapGet("{first}/{second}/{third}", async context => {
await context.Response.WriteAsync("Request Was Routed\n");
foreach (var kvp in context.Request.RouteValues) {
await context.Response.WriteAsync($"{kvp.Key}: {kvp.Value}\n");
}
});
app.MapGet("trans/{trans}", Trans.Endpoint);
app.MapGet("vehicle/{vehicle}", Vehicle.Endpoint);
app.Run();
위 예제의 route는 2개의 Segment를 가진 URL과 일치하며 첫 번째가 trans와 vehicle가 됩니다. 그리고 두 번째 Segment에서 trans와 vehicle라는 이름의 Segment변수에 값이 할당됨으로써 URL을 온전히 처리할 수 있습니다. 예제의 route를 확인해 보기 위해 예제를 실행하고 /trans/air과 vehicle/truck로 URL을 요청합니다.
위와 같은 변경사항은 여러 Component가 아닌 Routing Middleware에서만 처리되므로 효율성이 향상되며 URL Pattern에서 어떻게 요청이 연결될 수 있는지를 확인할 수 있습니다.
(6) Route에서 URL 생성하기
위 예제에서 Vehicle endpoint를 보면 여전히 Vehicle에서 지원하는 URL일부가 Trans endpoint에 밀접하게 연결되어 있는 부분을 확인할 수 있습니다.
context.Response.Redirect($"/trans/{vehicle}");
Routing System은 Segment 변수에 Data 값을 제공하여 URL을 생성하는 기능을 지원하고 있습니다. 이 문제를 해결하기 위 우선은 생성된 URL의 대상이 될 Route에 아래와 같이 이름을 할당해 줍니다.
..생략
app.MapGet("trans/{trans}", Trans.Endpoint).WithMetadata(new RouteNameMetadata("trans"));
app.MapGet("vehicle/{vehicle}", Vehicle.Endpoint);
..생략
WithMetadata Method는 MapGet Method의 결과에 적용되며 Route에 Metadata를 할당합니다. URL을 생성하기 위해 필요한 metadata는 RouteNameMetadata 개체를 통해 전달된 이름뿐인데 이 이름은 Route를 지정하기 위해 사용되는 것으로 생성자의 매개변수를 통해 지정됩니다.
Route의 이름을 사용하면 전혀 다른 Route를 대상으로 한 Link가 생성되는 걸 방지할 수 있습니다. 하지만 무시할 수 있으며 이렇게 하면 Routing System은 가장 적합한 Route를 검색할 것입니다.
아래 예제는 Vehicle endpoint를 개선한 것으로 trans에 대한 직접적인 URL의 의존성을 제거하고 URL을 생성하는 Routing기능을 사용하도록 하였습니다.
..생략
switch ((vehicle ?? string.Empty).ToLower())
{
case "air":
type = "People and cargo";
break;
case "truck":
type = "cargo";
break;
case "sedan":
//context.Response.Redirect($"/trans/{vehicle}");
LinkGenerator? generator = context.RequestServices.GetService<LinkGenerator>();
string? url = generator?.GetPathByRouteValues(context, "trans", new { trans = vehicle });
if (url != null) {
context.Response.Redirect(url);
}
return;
}
..생략
URL을 생성할 때는 LinkGenerator class를 사용합니다. 이때는 의존성 주입기능을 사용해 LinkGenerator class의 instance를 가져오는 것으로 직접적으로는 instance를 생성할 수 없습니다. 의존성 주입에 관해서는 추후에 자세히 알아볼 것입니다.
LinkGenerator class는 GetPathByRouteValues method를 통해 URL을 생성하게 되는데 예제에서는 이를 활용해 Redirect를 수행하고 있습니다.
GetPathByRouteValues method의 인수로는 endpoint의 HttpContext개체와 Link를 생성할때 사용될 Route의 이름, 그리고 Segment변수에 값을 제공하기 위해 사용될 개체를 전달합니다. 이렇게 하면 GetPathByRouteValues method는 trans endpoint로 route 될 URL을 반환합니다. 예제의 동작을 확인해 보기 위해 Application을 실행하고 /vehicle/sedan으로 URL을 요청합니다. 그러면 요청은 vehicle endpoint로 route 되고 URL을 생성하여 Redirect를 수행한 뒤 아래와 같은 결과를 표시할 것입니다.
위와 같은 방식은 명명된 Route의 URL Pattern을 통해 URL이 생성되도록 함으로써 Endpoint의 URL Pattern이 변경되는 경우 그 변경사항을 그대로 생성된 URL에 반영시킬 수 있습니다. 예를 들어 위의 예제에 따라 trans Endpoint의 URL Pattern이 아래와 같이 변경되는 경우
..생략
app.MapGet("logistice/{trans}", Trans.Endpoint).WithMetadata(new RouteNameMetadata("trans"));
app.MapGet("vehicle/{vehicle}", Vehicle.Endpoint);
..생략
해당 Route에 할당된 이름이 바뀌지만 않으면 생성된 URL은 항상 동일한 Endpoint와 연결될 것입니다. 예제를 다시 실행하고 /vehicle/sedan으로 다시 URL을 요청합니다. 이렇게 하면 변경된 URL Pattern과 연결되는 URL로 Redirection을 수행할 것입니다. 이 기능을 잘 활용하면 Application에서 사용되는 URL을 쉽게 변경할 수 있습니다.
URL Routing과 Areas
URL routing system은 Areas라는 기능을 통해 Application의 영역을 분리하여 각자 자신만의 Controller, View 그리고 Razor page를 가질 수 있습니다. 그러나 이 기능은 잘 사용되지 않을뿐더러 사용하게 되면 해결해야 할 여러 문제점들을 유발할 수 있습니다. Application이 어떻게든 분리되어야 한다면 Areas기능을 사용하기보다 별도의 Project를 만들어볼 것을 권장합니다.
2. URL 연결관리
지금까지는 기본적인 URL Routing기능만을 사용해 왔지만 대부분의 Application에서는 URL이 정확하게 Route 되고 Route와 연결되는 URL의 범위를 증가 혹은 제한하기 위한 더 많은 요구사항이 발생합니다. 이에 따라 URL Pattern을 통해 연결 Process를 조정할 수 있는 다양한 방법에 관해 알아볼 것입니다.
(1) 단일 URL Segment에서 다수의 값 연결하기
대부분의 Segment변수는 URL경로의 Segment와 직접적으로 연결되지만 Routing Middleware는 여기서 그치지 않고 단일 Segment를 변수에 연결함과 동시에 필요하지 않은 문자를 버릴 수도 있습니다. 아래 예제에서는 이를 활용해 URL Segment부분만을 변수와 연결하는 Route를 정의하고 있습니다.
//app.MapGet("{first}/{second}/{third}", async context => {
app.MapGet("file/{filename}.{ext}", async context => {
await context.Response.WriteAsync("Request Was Routed\n");
foreach (var kvp in context.Request.RouteValues) {
await context.Response.WriteAsync($"{kvp.Key}: {kvp.Value}\n");
}
});
URL Pattern은 정적문자열로 구분되는 한 원하는 만큼의 Segment변수를 포함시킬 수 있습니다. 정적 분리자가 필요한 이유는 Routing Middleware가 하나의 변수가 끝나는 위치를 그리고 다음이 시작하는 위치를 알기 위해서입니다. 위 예제의 Pattern은 마침표로 구분된 filename과 ext라는 이름의 Segment변수와 일치하는데 변수의 이름에서도 유추할 수 있듯이 File명을 처리하기 위한 Pattern입니다. 실제 Pattern이 URL와 연결되는 방식을 확인해 보기 위해 예제를 실행하고 /file/test.txt로 URL을 요청하여 아래와 같은 응답이 생성되는지 확인합니다.
Pattern을 너무 복잡하게 만들지 않는 것이 좋습니다.
위 예제의 실행결과를 보면 Segment변수의 순서를 확인할 수 있습니다. 다수의 변수를 포함하는 Pattern의 경우 오른쪽에서 왼쪽의 순서로 일치됩니다. 물론 Endpoint는 특정한 key순서에 의존하지 않기 때문에 대부분의 경우 이 문제는 별로 중요한 사항은 아니지만 Pattern이 복잡해지만 원하는 연결이 제대로 이루어지지 않을 수 있습니다.
사실 Matching Process는 매우 어렵기 때문에 예기치 않은 연결 실패가 발생할 수 있습니다. 특정한 오류사항은 Matching Process가 문제를 해결하기 위해 조정되면서 ASP.NET Core가 Release 될 때마다 조치되었지만 그럼에도 불구하고 이에 따른 새로운 문제를 일으키는 경우도 있었습니다. 예를 들어 첫 번째 변수와 일치되어야 하며, Segment의 시작부분이 실제문자열로 표시된 다음과 같은 URL Pattern의 경우에도 예상치 못한 문제가 존재합니다.
app.MapGet("file/name{filename}", async context => {
위 Pattern의 경우 name라는 실제 문자열로 시작하고 있으면서 filename이라는 Segment변수를 갖고 있습니다. 따라서 Routing Middleware는 /file/nametest URL과 정확히 연결될 것이며 filename이라는 Routing변수의 값은 test가 될 것입니다. 그러나 만약 URL이 /file/namenametest와 같이 되어 버리면 Matching Process는 filename변수에 할당되어야 하는 첫번째 부분과 실제 Content의 부분을 혼동하기 때문에 URL과 연결되지 못합니다. 이러한 문제는 추후에 개선될 수도 있겠지만 가능한 한 URL Pattern을 단순하게 유지하는 것이 좋습니다.
(2) Segment변수의 기본값 사용하기
Pattern을 정의할 때는 URL이 Segment에 해당하는 값을 포함하고 있지 않을 때 대신 사용할 수 있는 기본값을 사용할 수 있고 이를 통해 Route가 일치할 수 있는 URL의 범위를 늘릴 수 있습니다. 아래 예제는 Pattern을 통해서 기본값을 어떻게 사용할 수 있는지를 나타내고 있습니다.
app.MapGet("vehicle/{vehicle=air}", Vehicle.Endpoint);
기본값은 '=[사용값]' 형태로 정의됩니다. 따라서 예제에서는 URL에 두 번째 Segment가 존재하지 않는다면 air이라는 값을 사용할 것입니다. 이로서 Route에 일치할 수 있는 URL의 범위가 늘어나게 됩니다. 아래 표에서는 위 예제에 대해 일치할 수 있는 URL의 목록을 확인할 수 있습니다.
/ | 일치하지 않음(Segment가 너무 적음) |
/vhc | 일치하지 않음(첫번째 Segment는 vehicle이어야 함) |
/vehicle | 일치함(vehicle의 값은 air가 됨) |
/vehicle/truck | 일치함(vehicle의 값은 truck이 됨) |
/vehicle/air/truck | 일치하지 않음(Segment수가 너무 많음) |
위 예제의 동작을 확인해 보기 위해 예제를 실행하고 /vehicle로 URL을 요청해 아래와 같은 결과가 생성되는지 확인합니다.
(3) URL Pattern에서 선택적 Segment사용하기
기본값은 더 적은 Segment를 가진 URL을 일치시킬 수 있지만 Endpint를 불확실하게 만들 수 있습니다. 일부 Endpoint는 들어오는 Segment를 무시하고 자체적으로 URL을 처리하도록 정의될 수도 있습니다. 아래 예제는 Trans Endpoint를 변경한 것으로 Routing data에서 가능한 trans값이 존재하지 않을 때 지정한 기본값을 사용하도록 하고 있습니다.
..생략
string trans = context.Request.RouteValues["trans"] as string ?? "air";
int? cost = null;
switch (trans.ToLower()) {
case "air":
cost = 1_500_000;
break;
case "truck":
cost = 500_000;
break;
case "sedan":
cost = 300_000;
break;
}
..생략
위와 같은 변경으로 인해 Program.cs에서 trans에 대한 Route를 지정할 때 두 번째 Segment를 아래와 같이 선택적으로 지정할 수 있습니다.
app.MapGet("logistice/{trans?}", Trans.Endpoint).WithMetadata(new RouteNameMetadata("trans"));
선택적 Segment는 변수이름뒤에 ? 문자로 표시되며 해당 경로의 Segment를 가지지 않은 URL과도 연결될 수 있습니다.
/ | 일치하지 않음(Segment가 너무 적음) |
/log | 일치하지 않음(첫번째 Segment는 logistice이어야 함) |
/logistice | 일치함(trans의 값은 air가 됨) |
/logistice/truck | 일치함(trans의 값은 truck가 됨) |
/logistice/air/truck | 일치하지 않음(Segment수가 너무 많음) |
선택적 Segment의 동작을 확인하기 위해 예제를 실행하고 /logistice로 URL을 요청하여 아래와 같은 응답이 생성되는지 확인합니다.
(4) Catchall Segment 변수 사용하기
선택적 Segment를 사용하면 더 간소화된 URL에도 Pattern을 일치시킬 수 있습니다. 그런데 Catchall Segment는 이와는 반대로 Route를 Pattern보다 더 많은 Segment를 가진 URL에 일치시킬 수 있도록 합니다. Catchall segment는 아래와 같이 변수명 앞에 *를 사용하여 표시합니다.
..생략
app.MapGet("{first}/{second}/{*third}", async context => {
await context.Response.WriteAsync("Request Was Routed\n");
foreach (var kvp in context.Request.RouteValues) {
await context.Response.WriteAsync($"{kvp.Key}: {kvp.Value}\n");
}
});
..생략
예제의 Pattern에서는 2개의 Segment변수와 하나의 Catchall(third)을 포함하고 있습니다. 이로서 Route는 2개 또는 그 이상의 Segment를 가진 모든 URL과 일치하게 될 것입니다. 즉 해당 Route와 일치할 수 있는 URL Pattern에서 Segment의 수에는 제한이 없으며 추가된 Segment의 Content는 third라는 Segment변수에 할당됩니다. 예제를 실행하고 /first/second/third/fourth로 URL을 요청하여 아래와 같은 응답이 생성되는지 확인합니다.
Catchall을 통해서 할당된 Segment의 Content는 segment/segment/segment 형태로 표현됩니다. 따라서 Endpoint에서 해당 문자열을 적절히 분리하여 사용해야 합니다.
(5) Segment 제약 조건
기본값이나 선택적 Segment, Catchall Segment는 Route와 일치할 수 있는 URL의 범위를 증가시킵니다. 하지만 지금 설명할 Segment 제약 조건은 이와는 반대의 효과를 가지며 일치할 수 있는 URL의 범위를 제한합니다. Segment 제한은 Endpoint가 특정한 Segment Content를 가지는 경우에만 처리되어야 하는 경우 유용하게 사용될 수 있습니다. 사용방법은 Colon(:) 문자를 Segment변수이름 뒤에 붙이고 그다음 제한하고자 하는 type을 지정하면 됩니다.
..생략
//app.MapGet("{first}/{second}/{third}", async context => {
//app.MapGet("file/name{filename}", async context => {
//app.MapGet("{first}/{second}/{*third}", async context => {
app.MapGet("{first:int}/{second:bool}", async context => {
await context.Response.WriteAsync("Request Was Routed\n");
foreach (var kvp in context.Request.RouteValues) {
await context.Response.WriteAsync($"{kvp.Key}: {kvp.Value}\n");
}
});
..생략
예제에서 첫 번째 first Segment는 int type으로 판정될 수 있는 값에서만, 두 번째 second Segment는 bool type을 판정될 수 있는 값에서만 일치하도록 제한하고 있습니다. 따라서 제한된 것에 부합하지 않는 값은 Route에 연결되지 않을 것입니다. 아래 표에서는 URL Pattern에서 사용할 수 있는 다양한 제한 type을 나타내고 있습니다.
일부 제한은 지역에 기반하여 달라질 수 있는 형식(대한민국 와 미국에서의 시간표현 방식이 달라지는 등의)의 type을 다룹니다. Routing Middleware는 지역화된 형식은 처리하지 않으며 오로지 문화권 형식에 변하지 않는 형태로 표현되는 값에서만 일치합니다.
alpha | 대소문자 구별없이 a부터 z까지의 문자에 일치합니다. bool |
bool | 대소문자 구별없이 true와 false만 일치합니다. |
datetime | 비지역화된 불변의 문화권 형식으로 표현되는 DateTime값에만 일치합니다. |
decimal | 비지역화된 불변의 문화권으로 형식화된 decimal값에만 일치합니다. |
double | 비지역화된 불변의 문화권으로 형식화된 double값에만 일치합니다. |
file | File명.확장자 형태의 File이름을 나타내는 Segment와 일치합니다. File의 확장자 유효성은 검증하지 않습니다. |
float | 비지역화된 불변의 문화권으로 형식화된 float값에만 일치합니다. |
guid | GUID값에 일치합니다. |
int | int값에 일치합니다. |
length(len) | URL에서 특정한 수의 문자를 가진 Segment와 일치합니다. |
length(min, max) | min과 max사이의 길이를 가진 Segment와 일치합니다. |
long | long값에 일치합니다. |
max(val) | 지정한 값에 비해 더 적거나 동일한 int값에 일치합니다. |
maxlength(len) | 지정한 값에 비해 더 적거나 동일한 길이의 Segment와 일치합니다. |
min(val) | 지정한 값에 비해 더 많거나 동일한 int값에 일치합니다. |
minlength(len) | 지정한 값에 비해 더 많거나 동일한 길이의 Segment와 일치합니다. |
nonfile | File이름을 나타내지 않는, 즉 file 제약조건과 일치하지 않는 값과 일치합니다. |
range(min, max) | min과 max사이의 int값과 일치합니다. |
regex(expression) | 경로 Segmemt과 일치할 수 있는 정규표현식을 제약조건을 사용합니다. |
예제를 확인해 보기 위해 Project를 실행하고 위 예제의 제약조건에 부합하는 /100/true로 URL을 요청하여 아래와 같은 결과가 생성되는지 확인합니다.
이번에는 /first/second로 URL을 요청합니다. 해당 URL은 Segment의 수는 같지만 제약조건에는 부합하지 않으므로 Terminal Middleware로 전달된 요청은 그 어떤 Route와도 연결되지 않으므로 아래와 같은 결과를 생성할 것입니다.
제약조건은 결합이 가능하므로 아래와 같이 일치조건을 제한할 수도 있습니다.
app.MapGet("{first:int:max(3)}/{second:bool}", async context => {
위에서 처럼 제약조건이 결합되면 이들을 모두 만족하는 경로 Segment에만 일치됩니다. 따라서 위 예제는 첫 번째 Segment가 최대 3까지의 int type숫자이면서 두 번째 Segment가 true/false인 URL Pattern에만 일치할 것입니다. 이를 확인하기 위해 예제를 실행하고 /3/true로 URL을 요청하여 아래와 같은 응답이 생성되는지 확인합니다. /4/false에서 4는 max(3)의 제약조건에 부합하지 않으므로 원하는 결과를 얻을 수 없습니다.
● 다중 값을 사용한 제약조건 사용하기
Regex 제약조건은 정규표현식을 사용해 제약조건을 적용하는 방법이며 이를 통해 특정한 여러 가지 값에만 일치하는 제약조건을 생성할 수 있습니다. 아래 예제는 Regex 제약조건을 Vehicle Endpoint에 적용한 것으로 2개의 값에 대한 요청만 수신하게 될 것입니다.
..생략
app.MapGet("vehicle/{vehicle:regex(^air|truck$)}", Vehicle.Endpoint);
..생략
예제에서 첫 번째 Segment는 vehicle이 되어야 하며 두 번째 Segment는 air 또는 truck만이 되어야 합니다. 참고로 정규표현식에서 지정한 값은 대소문자를 구분하지 않으므로 /vehicle/AIR처럼 요청을 수행할 수 있습니다.
(6) Fallback route 정의하기
Fallback route는 요청이 어떤 Route와도 연결되지 않을 경우 지정한 Endpoint로 요청을 연결합니다. 이렇게 하면 Routing system이 항상 응답을 생성한다는 것을 보장하게 되므로 요청이 어떤 Route와도 연결되지 않는 상황을 방지할 수 있습니다.
...생략
app.MapGet("logistice/{trans?}", Trans.Endpoint).WithMetadata(new RouteNameMetadata("trans"));
app.MapGet("vehicle/{vehicle:regex(^air|truck$)}", Vehicle.Endpoint);
app.MapFallback(async context => {
await context.Response.WriteAsync("fallback endpoint response.");
});
app.Run();
MapFallback method는 마지막 수단으로 사용될 Route를 생성하는 것으로 여기까지 도달하게 되는 모든 요청에 응답하게 될 것입니다. 아래 표는 Fallback Route를 생성하는 데 사용되는 Method를 정리한 것이며 기타 다른 method도 존재하나 이 부분에 대해서는 추후에 다시 다뤄볼 것입니다.
MapFallback(endpoint) | Endpoint fallback을 생성합니다. |
MapFallbackToFile(file 경로) | File fallback을 생성합니다. |
위 예제에서 추가된 Route로 인해 상단에 정의한 정규표현식 Route와 일치하지 않는 요청을 포함하여 위에서 연결되지 않는 모든 요청에 응답하게 될 것입니다. 예제를 실행하고 /abc처럼 어떤 Route와도 일치하지 않는 요청을 수행하고 아래와 같은 응답이 생성되는지 확인합니다.
3. 고급 Routing 기능
지금까지 설명한 Routing 기능은 MVC Framework와 같은 고수준 기능을 통해 사용되는 것으로 대부분 Project에서 필요한 사항을 해결할 수 있습니다. 하지만 일반적이지 않은 독특한 Routing기능이 필요한 경우도 있는데 이런 경우는 어떻게 대응할 수 있는지 알아보도록 하겠습니다.
(1) 사용자 정의 제약사항 생성하기
지금까지 알아본 제약조건기능만으로 충분하지 않다면 IRouteConstraint interface를 구현하여 직접 사용자 정의 제약조건을 만들 수 있습니다. 이를 확인해 보기 위해 ToDestRouteConstraint.cs file을 Platform folder에 아래와 같이 추가합니다.
namespace Platform;
public class UseVehicleRouteConstraint : IRouteConstraint
{
private static string[] vc = { "air", "truck", "sedan" };
public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) {
string segments = values[routeKey] as string ?? string.Empty;
return Array.IndexOf(vc, segments.ToLower()) > -1;
}
}
IRouteConstraint interface는 Match method를 정의하고 있으며 이를 통해 요청에 Route와 연결될 수 있는 제약조건을 설정할 수 있습니다. Match안에서는 요청과 관련된 HttpContext, Route와 Segment이름 및 URL로부터 가져온 Segment 변수를 매개변수로 제공하고 있으며, 요청을 들어오는 URL에서 확인할지 또는 나가는 URL에서 확인할지 여부를 결정할 수 있습니다. Match Method는 제약조건에 요청에 만족하는 경우라면 Match는 true를 그렇지 않으면 false를 반환하게 되는데 예제에서의 제약조건은 3개의 문자열을 통한 배열을 정의하여 이를 Segment값과 비교하고 정의한 문자열을 Segment가 포함하고 있는지 확인하고 있습니다. 위 예제와 같이 정의된 사용자 정의 제약조건은 다음과 같이 Option pattern을 통해 설정할 수 있습니다.(Option pattern에 관해서는 추후에 상세히 알아볼 것입니다.)
using Platform;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<RouteOptions>(opts => {
opts.ConstraintMap.Add("vehicles",
typeof(UseVehicleRouteConstraint));
});
var app = builder.Build();
app.MapGet("vehicle/{vehicle:vehicles}", Vehicle.Endpoint);
..생략
Option pattern은 ConstraintMap속성을 정의하는 RouteOptions class에 적용됩니다. 각 제약조건은 URL Pattern에 적용할 Key와 함께 등록되는데 예제에서 UseVehicleRouteConstraint class에 사용된 Key는 vehicle이고 이를 통해 다음과 같이 Route에 제약조건을 설정할 수 있습니다.
app.MapGet("vehicle/{vehicle:vehicles}", Vehicle.Endpoint);
예제에 따라 URL의 첫 번째 Segment가 vehicle이고 두 번째 Segment가 정의된 vc배열 중 하나일 때만 요청은 Route와 연결될 것입니다.
(2) 모호한 Route 예외 피하기
요청을 Route 할 때 Routing Middleware는 각 Route에 접수를 할당하게 됩니다. 이때 더 구체적인 Route에 더 우선권이 주어지게 되며 물론 Application이 지원하는 URL을 제대로 Test 하지 않으면 가끔씩 문제를 일으킬 수 있지만 대부분 Route선택은 예측가능할 정도의 단순한 process입니다.
2개의 Route가 같은 점수를 가지게 되면 Route System은 이들 Route를 선택할 수 없게 되고 결국 Route가 모호하다는 예외를 발생시키게 됩니다. 대부분의 경우 가장 좋은 방법은 모호한 Route에 기준이 될 Segment나 제약사항을 증가시킴으로써 더 구체화시키는 것이 좋습니다. 실제 이러한 상황은 언제든지 발생할 수 있고 그러면 Routing System이 의도한 대로 작동할 수 있게끔 하는 추가적인 작업이 필요합니다. 아래 예제는 이전 예제에서 Route를 변경하여 일부 요청에 한하여 모호함이 생길 수 있는 2개의 Route가 만들어지도록 하였습니다.
..생략
var app = builder.Build();
app.Map("{num:int}", async context => {
await context.Response.WriteAsync("Routed to the int endpoint");
});
app.Map("{num:double}", async context => {
await context.Response.WriteAsync("Routed to the double endpoint");
});
app.MapFallback(async context => {
await context.Response.WriteAsync("fallback endpoint response.");
});
..생략
위 예제의 Route는 특정한 유형의 값에만 해당하는 모호함이 존재하는데 첫 번째 Segment로 double 유형이 사용된 요청에만 두번째 Route에 연결되도록 하는 의도를 나타내고 있지만 첫번째 Segment가 int 유형이라면 2개의 Route모두와 연결될 수 있습니다. 문제점을 확인해 보기 위해 예제를 실행하고 /11.22로 URL을 요청합니다. 11.22는 double로 인식할 수 있으므로 아마도 아래와 같은 응답을 생성할 것입니다.
하지만 /123과 같이 URL을 요청하면 예외를 일으키게 됩니다. 해당 Segment는 int와 double유형 모두에 해당할 수 있기 때문이며 이때 Routing system은 요청을 처리할 수 있는 Route가 무엇인지를 식별할 수 없게 됩니다.
이러한 상황에서는 아래 예제와 같이 다른 연결가능한 Route와 상대적인 순서를 정의함으로써 특정 Route에 우선순위를 부여할 수 있습니다.
..생략
app.Map("{num:int}", async context => {
await context.Response.WriteAsync("Routed to the int endpoint");
}).Add(n => ((RouteEndpointBuilder)n).Order = 1);
app.Map("{num:double}", async context => {
await context.Response.WriteAsync("Routed to the double endpoint");
}).Add(n => ((RouteEndpointBuilder)n).Order = 2);
..생략
예제에서는 Add method를 호출하여 RouteEndpointBuiler로 형변환을 수행한 뒤 Order 속성을 통해 값을 설정하고 있습니다. 이에 따라 우선순위는 더 낮은 Order값을 가진 Route에 부여됩니다. 따라서 예제에서는 두 개의 모든 Route가 처리할 수 있는 첫 번째 Segment를 가진 URL요청이면 첫 번째 Route가 동작하게 됩니다. 예제를 다시 실행하고 /123 URL을 요청하여 아래와 같은 응답이 생성되는지 확인합니다.
(3) Middleware Component에서 Endpoint에 접근하기
잘 알겠지만 모든 Middleware가 응답을 생성하는 것은 아닙니다. 일부 Component는 Session Middleware와 같이 요청 Pipeline이전에 사용되는 기능을 제공하거나 상태 Code Middleware와 같이 응답을 변형하는 것과 같은 기능을 제공합니다.
일반적인 요청 Pipeline의 한계 중 하나는 Pipeline이 시작할 때의 Middleware Component에서는 이후에 어떤 Component에서 응답을 생성할지 알 수 없다는 것입니다. Routing은 UseRouting과 UseEndpoints method를 명시적으로 혹은 ASP.NET Core Platform에 의존하여 Application이 시작할때 호출함으로써 설정합니다.
물론 UseEndpoints method에서 Route가 등록되어 있지만 Route의 선택은 UseRouting method에서 이루어지며 UseEndpoints method안에서 EndPoint가 실행되어 응답을 생성하게 됩니다. 하지만 UseRouting과 UseEndpoints method사이에 요청 Pipeline에서 추가된 모든 Middleware Component는 응답이 생성되기 전에 어떤 Endpoint가 선택되었는지를 알 수 있으며 이에 대한 동작을 적절히 바꿀 수 있습니다.
아래 예제에서는 요청을 처리하기 위해 선택된 Route에 따라 다른 Message를 응답에 추가하는 Middleware Component를 추가한 것입니다.
..생략
var app = builder.Build();
app.Use(async (context, next) => {
Endpoint? end = context.GetEndpoint();
if (end != null)
await context.Response.WriteAsync($"{end.DisplayName} Selected\n");
else
await context.Response.WriteAsync("No Endpoint Selected\n");
await next();
});
app.Map("{num:int}", async context => {
await context.Response.WriteAsync("Routed to the int endpoint");
}).WithDisplayName("num int endpoint").Add(n => ((RouteEndpointBuilder)n).Order = 1);
app.Map("{num:double}", async context => {
await context.Response.WriteAsync("Routed to the double endpoint");
}).WithDisplayName("num double endpoint").Add(n => ((RouteEndpointBuilder)n).Order = 2);
..생략
HttpContext class상의 GetEndpoint 확장 method에서는 요청을 처리하기 위해 선택된 EndPoint를 반환합니다.
응답이 생성되기 전 Routing Middleware에 의해 선택된 EndPoint를 변경할 수 있는 SetEndpoint Method도 존재합니다. 그러나 신중하게 그리고 일반적인 Route 선택 Process에 개입해야 할 중대한 이유가 있을 경우에만 사용되어야 합니다.
아래 Table에서는 EndPoint class에서 정의된 유용한 속성을 확인할 수 있습니다.
DisplayName | 해당 속성은 EndPoint에 할당된 Display name을 반환하며 Route를 생성할때 WithDisplayName method를 사용해 설정할 수 있습니다. |
Metadata | 해당 속성은 EndPoint에 할당된 Metadata collection을 반환합니다. |
RequestDelegate | 해당 속성은 응답을 생성할때 사용될 Delegate를 반환합니다. |
위 예제에서는 Routing Middleware가 선택한 Endpoint를 쉽게 식별하기 위해 WithDisplayName method를 사용하여 위 예제의 Route에 이름을 할당하였습니다. 예제의 새로운 Middleware Component는 선택된 Endpoint의 응답에 지정한 Message를 추가할 것입니다. 예제를 실행하고 /123으로 URL을 요청하여 요청 Pipeline으로 Routing Middleware를 추가하는 2개의 Method 중 어떤 EndPoint가 선택되었는지를 나타내는 Middleware의 응답을 확인합니다.
'.NET > ASP.NET' 카테고리의 다른 글
[ASP.NET Core] - 13. 의존성 주입 (0) | 2024.07.09 |
---|---|
[ASP.NET Core] - 11. ASP.NET Core platform (0) | 2024.05.15 |
[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 |