0,0
рейтинг
10 апреля 2015 в 12:47

Разработка → ASP.NET 5 Middleware или куда же пропал мой HTTP модуль? перевод tutorial

Новая версия ASP.NET 5 переписана почти с нуля и включает в себя существенные изменения по сравнению с предыдущими версиями. Одно из самых больших изменений — это конвейер обработки (HTTP Pipeline). В этой статье описано, как эти изменения влияют на проектирование и внедрение компонентов, которые раньше были представлены как Http модули.

Поведение HTTP модулей раньше было схожим с поведением фильтров запросов, вплоть до ASP.NET 5. Это функционал, который можно внедрить в конвейер запросов и описать некоторую задачу для выполнения, например, отреагировать на событие в приложении. Модули используют для аутентификации, глобальной обработки ошибок и логгирования. Также их часто используют для перехвата и изменения серверного ответа, например, удаления пробелов или компрессии. Они реализуют интерфейс IHttpModule, который определен в сборке System.Web, которая, в свою очередь, не является частью нового ASP.NET.

В основном регистрация HttpModule-я в коде добавляется как обработчик события в Global.asax или же создается сборка для регистрации в web.config.

Что такое Middleware?


Определение «Middleware» сильно варьируется, но в контексте ASP.NET 5, пожалуй, самое точное определение дано в спецификации Owin:

Сквозные компоненты, которые образуют конвейер между сервером и приложением, которое выполняет инспекцию, маршрутизацию или изменении запроса и ответа с определенной целью.

Оригинал:
Pass through components that form a pipeline between a server and application to inspect, route, or modify request and response messages for a specific purpose

Очень похоже на описание традиционного HTTP модуля или обработчика (handler-a).

Доступ к конвейеру запросов в приложении ASP.NET 5 предоставляется в классе Startup (файл Startup.cs), он же является точкой входа в само приложение. Класс Startup включает в себя метод Configure, который принимает IApplicationBuilder в качестве параметра. Интерфейс IApplicationBuilder (параметр app) предоставляет ряд extension-методов, с помощью которых различные компоненты могут быть подключены к конвейеру запросов. Основное различие между этой и предыдущей реализацией HTTP конвейера является то, что прежний подход базировался на событиях, а сейчас он был заменен на композитную модель (компоненты добавляются один за другим). Также, в новом ASP.NET важен порядок, в котором добавляют эти компоненты. В базовом шаблоне приложения MVC уже имеются некоторые компоненты, и по комментариям можно понять их назначение:
// Add static files to the request pipeline.
app.UseStaticFiles();

// Add cookie-based authentication to the request pipeline.
app.UseIdentity();

// Add MVC to the request pipeline.
app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller}/{action}/{id?}",
        defaults: new { controller = "Home", action = "Index" });

    // Uncomment the following line to add a route for porting Web API 2 controllers.
    // routes.MapWebApiRoute("DefaultApi", "api/{controller}/{id?}");
});


Как мы видим, методы, с помощью которых добавлены Identity, MVC и статические файлы — extension-методы IApplicationBuilder. При добавлении нового промежуточного обработчика, который будет описан в этой статье, я также создам extension-метод для добавления его к конвейеру.

Пример Middleware


Наш пример промежуточного обработчика будет делать 2 вещи: измерять время обработки запроса, а также добавлять это значения в исходящий HTML и заголовки ответа. Это даже целые 3 вещи. В любом случае, этот пример будет иллюстрировать 2 самых распространенных сценария, что описаны во множестве статей типа «Как написать свой HTTP модуль» — изменение ответа сервера и добавление заголовков. Код обработчика:
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Http;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;

namespace WebApplication1.MiddleWare
{
    public class MyMiddleware
    {
        RequestDelegate _next;

        public MyMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            var sw = new Stopwatch();
            sw.Start();

            using (var memoryStream = new MemoryStream())
            {
                var bodyStream = context.Response.Body;
                context.Response.Body = memoryStream;
  
                await _next(context);

                var isHtml = context.Response.ContentType?.ToLower().Contains("text/html");
                if (context.Response.StatusCode == 200 && isHtml.GetValueOrDefault())
                {
                    {
                        memoryStream.Seek(0, SeekOrigin.Begin);
                        using (var streamReader = new StreamReader(memoryStream))
                        {
                            var responseBody = await streamReader.ReadToEndAsync();
                            var newFooter = @"<footer><div id=""process"">Page processed in {0} milliseconds.</div>";
                            responseBody = responseBody.Replace("<footer>", string.Format(newFooter, sw.ElapsedMilliseconds));
                            context.Response.Headers.Add("X-ElapsedTime", new[] { sw.ElapsedMilliseconds.ToString() });
                            using (var amendedBody = new MemoryStream())
                            using (var streamWriter = new StreamWriter(amendedBody))
                            {
                                streamWriter.Write(responseBody);
                                amendedBody.Seek(0, SeekOrigin.Begin);
                                await amendedBody.CopyToAsync(bodyStream);
                            }
                        }
                    }
                }
            }
        }
    }
}


У нас есть приватное поле типа RequestDelegate, в котором хранится делегат, что передан конструктору. Сам тип RequestDelegate инкапсулирует метод, которые принимает HttpContext и возвращает Task:
public delegate Task RequestDelegate(HttpContext context);


Объект HttpContext похож на такой же из предыдущих версий ASP.NET тем, что он обеспечивает доступ к запросу, ответа и т.д., но это — совсем другой зверь и он намного легче.

В ASP.NET 5 middleware это экземпляр Func <RequestDelegate, RequestDelegate> — делегат, который принимает RequestDelegate в качестве параметра и возвращает RequestDelegate. И, как описано выше, RequestDelegate — это функция, которая возвращает функцию, и это принцип, по которому построен конвейер обработки — каждая часть промежуточного слоя, будучи прикованной к другой части, отвечает за передачу на обработку следующему в цепи (при необходимости).

Метод Invoke будет вызываться во время выполнения, базируясь на основе конвенции. Это и есть место, где происходит обработка в вашей части middleware, и место, где вы отдаете контроль следующему компоненту в конвейере в случае необходимости. Возможен случай, когда не нужно передавать контроль дальше, а вместо этого нужно остановить выполнение, например, если это компонент аутентификации, который определяет, что текущий пользователь не имеет соответствующих прав. Управление передается следующему компоненту путем вызова await _next(context). Код, который вы размещаете до этого и будет выполняется, например, в нашем случае, мы создаем секундомер и запускаем его. Кроме того, у нас есть доступ к ответу, который реализуется в виде потока данных (stream). Затем вызывается следующий компонент, который, в свою очередь, вызывает следующий и так далее. Затем управление передается обратно по цепочке компонентов и выполняется код, который был добавлен после вызова await _next(context). Именно в этом блоке кода в нашем примере и изменяется тело ответа, чтобы включить HTML с истекшим временем в footer страницы. И тогда же добавляется заголовок с этим же значением.

Вклинивание в конвейер


Следующий шаг — это включить компонент в конвейер обработки. Как было описано выше, это можно сделать с помощью extesion-метода, например:

namespace ASPNET5Test.MiddleWare
{
    public static class BuilderExtensions
    {
        public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder app)
        {
            return app.UseMiddleware<MyMiddleware>();
        }
    }
}


Это простой пример обёртки вокруг стандартного метода UseMiddleware, который сейчас можно найти в Microsoft.AspNet.Builder.
Ну, и последний шаг — вызов нашего метода в классе Startup.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerfactory)
{
    app.UseMyMiddleware();

    // ...


При первом запуске сайта уходит довольно много времени — почти 2 секунды, что можно увидеть внизу страницы:


Ну, и с помощью, например, Developer Tools в Chrome можно посмотреть заголовки:


Заключение


В этой статье была представлена замена традиционным HTTP модулям и показано, как создать и внедрить в приложение экземпляр такого обработчика. Эта статья базируется на версии ASP.NET 5 Beta 3 и концепции, что здесь проиллюстрированы, могут измениться.
Перевод: Mike Brind
Виктор Коцюбан @Gbdrm
карма
53,0
рейтинг 0,0
.
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (20)

  • –2
    Мидлвары бесполезны как концепция, сужу по опыту Node.js и express. Т.к. для разных роутов нужна своя комбинация мидлвар, через которые будет проходить запрос, плюс в чем именно роль middleware? Добавлять к объекту запроса дополнительные методы, модифицировать данные внутри объекта запроса и т.д.? с этим на мой взгляд лучше справляется Dependency Injection и инъекция сервисов в обработчики вместо самого объекта запроса.
    • 0
      И в чем проблема указать для разных зон сайта разные middleware? Кстати, Autofac.Owin тоже через Middleware работает.
    • +2
      Для пре- и постпроцессинга крайне полезны. Обработку всяких X-HTTP-Request-Override и разного хлама от reverse-proxy (X-Forwarded-From, X-Scheme и т. п.) оптимально закинуть в них. Логгинг ошибок — в них. Аутентификацию запросов — тоже в них.
    • 0
      Нужно для:
      — аутентификации
      — логгирования
      — преобразования риквеста/респонса (что в этом примере и есть)
    • 0
      в express становится удобной работа с сессиями пользователя, где за операции различного рода отвечают разные middleware.
      • лог
      • таргетинг, например гео
      • наложение своих заголовков поверх статики, например Cache-Control
      • отдача статики

      очень много возможностей контроля над запросами появляется
  • +2
    Путь через компоненты


    Думаю pass through components — это всё же «сквозные» компоненты)
    • 0
      Согласен. Сначала написал «промежуточные компоненты», не понравилось. Потому попробовал перефразировать в путь… тоже не то. Сквозные, наверное, будет лучшим вариантом.
      • 0
        Сквозные компоненты, которые образуют конвейер между сервером и приложением, которое выполняет инспекцию, маршрутизацию или изменении запроса и ответа с определенной целью.

        Здесь скорее: «Сквозные компоненты, которые образуют конвейер между сервером и приложением и выполняющие инспекцию, маршрутизацию или изменении запроса и ответа с определенной целью.»
  • 0
    Один вопрос вы записываете footer после тега html? По тексту функции я не вижу где вы парсите выходной html, что бы внедрить footer в правильное место. Или я что то не понимаю?
    • 0

      Вот тут:


      responseBody = responseBody.Replace("<footer>",

      То есть в исходной разметке ищется подстрока <footer>, которая дальше заменяется на сгенерированную.

      • 0
        Хитро :). И еще вопрос я все сделал как у вас и мне в мидлвар обработчик приходит запрос favicon.ico и я не знаю что с ним делать ))). В HTTP модуле я просто не вызывал app.CompleteRequest(); И тогда IIS отдавал этот файл… Раз уж пошла такая пьянка, хочу спросить еще один вопрос, что если я полностью выключу MVC платформу не начнет плющить?
        • 0

          Чтобы передать запрос дальше, надо вызвать await _next(context);, если речь идет об асинхронном методе. Или return _next(context), если речь идет о синхронном.

          • 0
                        if (context.Request.Path == "favicon.ico")
                        {
                            return _next(context);
                        }
            


            Ругается…
          • 0
            Похоже что подводных камней в Core не мало. При попытке прочитать context.Request.Path я получаю исключение:
            System.ArgumentException: The path in 'value' must start with '/'.
            

            там был «index.html» не понимаю по какой причине путь должен начинаться с '/' и как заставить браузер его посылать )), бред какой то.
            • 0

              И когда уже программисты научатся к сообщениям об ошибках прикладывать стектрейсы?..

              • –1
                Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 DEBUG http://localhost:57252/  0
                Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 GET http://localhost:57252/index.html?action=test  
                Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 17084.406ms 200 
                Microsoft.AspNetCore.Server.Kestrel:Error: Connection id "0HKVOC28KEBCG": An unhandled exception was thrown by the application.
                
                System.ArgumentException: The path in 'value' must start with '/'.
                Parameter name: value
                   at Microsoft.AspNetCore.Http.PathString..ctor(String value)
                   at Microsoft.AspNetCore.Http.PathString.op_Implicit(String s)
                   at WebApplication1.MyMiddleware.<Invoke>d__2.MoveNext() in C:\Users\Макс\Documents\Visual Studio 2015\Projects\WebApplication1\src\WebApplication1\MyMiddleware.cs:line 23
                --- End of stack trace from previous location where exception was thrown ---
                   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
                   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
                   at Microsoft.AspNetCore.Server.IISIntegration.IISMiddleware.<Invoke>d__8.MoveNext()
                --- End of stack trace from previous location where exception was thrown ---
                   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
                   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
                   at Microsoft.AspNetCore.Hosting.Internal.RequestServicesContainerMiddleware.<Invoke>d__3.MoveNext()
                --- End of stack trace from previous location where exception was thrown ---
                   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
                   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
                   at Microsoft.AspNetCore.Server.Kestrel.Internal.Http.Frame`1.<RequestProcessingAsync>d__2.MoveNext()
                Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 39294.2338ms 200 
                


                using System;
                using System.Collections.Generic;
                using System.IO;
                using System.Linq;
                using System.Threading.Tasks;
                using Microsoft.AspNetCore.Http;
                
                namespace WebApplication1
                {
                    public class MyMiddleware
                    {
                        RequestDelegate _next;
                
                        public MyMiddleware(RequestDelegate next)
                        {
                            _next = next;
                        }
                
                        public async Task Invoke(HttpContext context)
                        {
                            var act = context.Request.Query["action"];
                
                            if (context.Request.Path == "favicon.ico")//Здесь исключение
                            {
                                return;// _next(context);
                            }
                
                            var wr = new StreamWriter(context.Response.Body);
                
                            var str = ("<html><body>" + wr + "</body></html>").ToCharArray();
                
                            wr.Write(str);
                        }
                    }
                }
                


                using System;
                using System.Collections.Generic;
                using System.Linq;
                using System.Threading.Tasks;
                using Microsoft.AspNetCore.Builder;
                using Microsoft.AspNetCore.Hosting;
                using Microsoft.Extensions.Configuration;
                using Microsoft.Extensions.DependencyInjection;
                using Microsoft.Extensions.Logging;
                
                namespace WebApplication1
                {
                    public class Startup
                    {
                        public Startup(IHostingEnvironment env)
                        {
                            var builder = new ConfigurationBuilder()
                                .SetBasePath(env.ContentRootPath)
                                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                                .AddEnvironmentVariables();
                            Configuration = builder.Build();
                        }
                
                        public IConfigurationRoot Configuration { get; }
                
                        // This method gets called by the runtime. Use this method to add services to the container.
                        public void ConfigureServices(IServiceCollection services)
                        {
                            // Add framework services.
                            //services.AddMvc();
                        }
                
                        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
                        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
                        {
                            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
                            loggerFactory.AddDebug();
                
                            //app.UseMvc();
                            //app.UseStaticFiles();
                
                            app.UseMiddleware<MyMiddleware>();
                        }
                    }
                }
                
                


                Скриншот в отладке и настроек

                http://prntscr.com/cwf48q
                http://prntscr.com/cwf3p4
                • 0

                  В следующий раз не забывайте спойлеры ставить.


                  И читайте описание ошибок внимательнее! У вас ошибка не при чтении Path — а при преобразовании строкового литерала в PathString.

                  • 0
                    Тут ошибка не у меня, а ошибка где то в нутрях Core. Поскольку это не я присваивал Path это значение. Да и вобще получить исключение при чтении свойства это нонсенс. Я так понимаю это еще бетта версии? И в продакшене это еще нельзя использовать?
                    • 0

                      Простите, но вы читать умеете?


                      У вас ошибка не при чтении Path — а при преобразовании строкового литерала в PathString.

                      У вас ошибка не при чтении Path — а при преобразовании строкового литерала в PathString.

                      Что еще непонятно?

                      • 0
                        Тфьфу блин :)). Пора идти спать :)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.