미들웨어 직접 작성하기
파이프라인에 내 코드를 끼웁니다.
파이프라인에 내 코드를 끼웁니다
1.3에서 app.Use(…) 로 요청이 지나는 길에 코드를
놓았습니다. 그 자리에 놓을 것이 길어지면 클래스로 옮깁니다.
모든 요청이 지나는 자리라 기록·인증·오류 처리 같은 것을 여기에 둡니다.
| 방법 | 언제 |
|---|---|
| app.Use(…) | 짧을 때입니다. Program.cs 에 그대로 적습니다. |
| 클래스 + UseMiddleware | 길어질 때입니다. 가장 흔한 방법입니다. |
| IMiddleware 구현 | 요청마다 새 인스턴스가 필요할 때입니다. |
가운데 방법에는 물려받을 것도 구현할 인터페이스도 없습니다. 정해진 모양만 갖추면 됩니다.
처리 시간을 재는 미들웨어
public class TimingMiddleware { private readonly RequestDelegate _next; public TimingMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { var 시작 = Stopwatch.GetTimestamp(); await _next(context); var 걸린 = Stopwatch.GetElapsedTime(시작); Console.WriteLine($"{context.Request.Path} {context.Response.StatusCode} {걸린.TotalMilliseconds:F1}ms"); } } // Program.cs — Build() 뒤, 가장 바깥에 두려면 가장 먼저 적습니다 app.UseMiddleware<TimingMiddleware>();
이 코드는 서버가 있어야 하므로 브라우저에서 실행할 수 없습니다. 파이프라인의 순서만 보려면 ASP.NET Core 1.3 의 예제를 실행해 보십시오.
갖출 모양은 둘입니다.
- 생성자가
RequestDelegate를 첫 매개 변수로 받습니다. 그것이 다음 처리기입니다. InvokeAsync또는Invoke라는 이름의 메서드가 있고Task를 돌려줍니다.
이름이 어긋나도 컴파일은 됩니다. 반사로 찾기 때문입니다. 대신 기동할 때 예외가 발생합니다.
인스턴스는 하나뿐입니다
만들어지는 횟수와 처리하는 횟수를 함께 찍어 보았습니다.
[미들웨어] 만들어짐 1번째 [미들웨어] / 200 5.0ms (처리 1번째) [미들웨어] / 200 0.1ms (처리 2번째) [미들웨어] / 200 0.0ms (처리 3번째) [미들웨어] /admin/x 200 0.5ms (처리 4번째) [미들웨어] /count 200 2.0ms (처리 5번째) [미들웨어] /count 200 0.4ms (처리 6번째)
여기서 함정이 나옵니다. 인스턴스가 하나이므로 필드에 요청별 값을 담으면 안 됩니다. 여러 요청이 동시에 지나며 서로의 값을 덮습니다.
더 자주 겪는 것은 생성자로 서비스를 받는 것입니다. 3.2에서 본
DbContext 는 Scoped 인데, 미들웨어는 기동할 때 한 번
만들어지므로 그 자리에 요청 범위가 없습니다.
public TimingMiddleware( RequestDelegate next, TodoContext db) { … } // 기동할 때 // Application startup exception // InvalidOperationException: // Cannot resolve scoped service // 'TodoContext' from root provider.
public async Task InvokeAsync( HttpContext context, TodoContext db) { … } // 요청마다 그 범위의 것을 // 넣어 줍니다.
더 조심할 것이 있습니다. 왼쪽의 예외는 Development 에서만 납니다. 범위 검사가 그때만 켜지기 때문입니다. 운영에서는 오류 없이 뜨고, 미들웨어가 맨 처음 만들어진 하나를 끝까지 붙들고 모든 요청에 사용합니다.
그러면 3.2에서 본 일이 그대로 생깁니다. 지켜보는 기억이 쌓이고, 여러 요청이
동시에 건드립니다. 개발에서 잡히지 않으면 운영에서 이따금 터지는
모양이 되므로, 미들웨어에서 서비스가 필요하면
InvokeAsync 의 매개 변수로 받으십시오.
[미들웨어] 만들어짐 1번째 [미들웨어] / 200 4.2ms 범위 ff8a491d (처리 1번째) [미들웨어] / 200 0.1ms 범위 bbdae442 (처리 2번째) [미들웨어] / 200 0.0ms 범위 d08c9f9d (처리 3번째) [미들웨어] /count 200 0.2ms 범위 92738ecd (처리 4번째)
일부 요청에만 붙이기
모든 요청에 붙일 것이 아니라면 조건을 걸어 갈라 붙입니다.
app.UseWhen(
c => c.Request.Path.StartsWithSegments("/admin"),
b => b.Use(async (c, next) => {
Console.WriteLine(" [admin 전용] 지납니다");
await next();
}));
| 메서드 | 하는 일 |
|---|---|
| UseWhen | 조건에 맞으면 곁길을 지난 뒤 본래 길로 돌아옵니다. |
| MapWhen | 조건에 맞으면 곁길로 가서 돌아오지 않습니다. |
| Map | 주소가 맞으면 곁길로 갑니다. 돌아오지 않습니다. |
UseXxx 로 감싸기
app.UseMiddleware<TimingMiddleware>() 는 형식
이름이 드러나 읽기 나쁩니다. 확장 메서드로 감싸는 것이 관례입니다.
UseStaticFiles·UseAuthentication
이 모두 이렇게 되어 있습니다.
public static class TimingMiddlewareExtensions { public static IApplicationBuilder UseTiming(this IApplicationBuilder app) => app.UseMiddleware<TimingMiddleware>(); } // Program.cs app.UseTiming();
요청마다 새로 만들려면
IMiddleware 를 구현하면 요청마다 새 인스턴스를
받습니다. 다만 직접 등록해야 합니다 —
builder.Services.AddScoped<MyMiddleware>()
를 빠뜨리면 기동할 때 예외가 발생합니다.
대개는 필요하지 않습니다. 관례 기반으로 두고 필요한 서비스를
InvokeAsync 의 매개 변수로 받으면 같은 일이
되고, 인스턴스를 매번 만들지 않아 가볍습니다.
직접 해보기
2.5에서 본 traceId 처럼, 모든 응답에 X-Request-Id 머리글을 붙이는 미들웨어를 적어보세요.
모든 요청을 찍으면 기록이 너무 많습니다. 500ms 를 넘는 것만 찍도록 고치고, 그 기준을 설정에서 읽어 오도록 해보세요.
동료가 미들웨어 생성자로 DbContext 를 받도록 적었고,
개발 기계에서는 앱이 뜨지 않는다고 합니다. 그런데 운영에는 이미
올라가 잘 돌고 있습니다. 무슨 일이 벌어지고 있는지 적어보세요.
- 생성자가
RequestDelegate를 받고InvokeAsync가 있으면 미들웨어입니다. 물려받을 것이 없습니다. - 인스턴스는 하나입니다. 필드에 요청별 값을 담지 마십시오.
- Scoped 서비스는
InvokeAsync의 매개 변수로 받습니다. 생성자로 받으면 개발에서는 기동이 막히고 운영에서는 조용히 잘못 동작합니다. - 일부 요청에만 붙이려면
UseWhen·MapWhen·Map을 사용합니다. UseXxx확장 메서드로 감싸는 것이 관례입니다.