OWIN и Katana. Часть 3.2 – Метод в роли модуля

Для создания небольшого обработчика не обязательно создавать класс. Иногда достаточно метода.

Давайте вместо возврата кода 404 выведем текст: "Извините, но запрошенный адрес не существует". Конечно, это не корректно для реального веб-приложения, но подойдет для тренировочной задачи.

Соглашения для создания функций-обработчика

Конечно, можно создать класс, как это было описано в прошлой части. Однако соглашения Katana позволяют задавать обработчики в виде функции Func<AppFunc, AppFunc>. Она получает делегат AppFunc следующего обработчика в цепочке (next) и должна создать и вернуть свой обработчик AppFunc. Для лучшего понимания сначала рассмотрим полный вариант записи вызова Use():

appBuilder.Use(new Func<AppFunc, AppFunc>(next => 
    new AppFunc(env => {
        /* Код обработчика */
        return next(env); /* или возврат Task и завершение обработки запроса. */ 
    })));

Благодаря возможностям C#, этот код можно записать несколько короче:

appBuilder.Use(new Func<AppFunc, AppFunc>(next =>
    env => {
        /* Код обработчика */
        return next(env); /* или возврат Task и завершение обработки запроса. */ 
    }));

Здесь env все тот же словарь с данными запроса, сервера и объектами для формирования ответа.

Рассматриваемое соглашение позволяет размещать несколько обработчиков в одном классе. Желательно чтобы при этом они были логически связанны. Выглядеть будет это так:

public class CoolModule
{
    public Task HandleMain(IDictionary<string, object> env, AppFunc next) { ... }

    public Task HandleAddon(IDictionary<string, object> env, AppFunc next) { ... }
}
...
public class Startup
{
    public void Configuration(IAppBuilder appBuilder)
    {
        var coolModule = new CoolModule();

        appBuilder.Use(new Func<AppFunc, AppFunc>(next => 
            env => coolModule.HandleMain(env, next)));

        appBuilder.Use(new Func<AppFunc, AppFunc>(next =>
            env => coolModule.HandleAddon(env, next)));
    }
}

Здесь сигнатура обработчиков несколько изменена, для передачи в них значения next. Но это возможно, т.к. напрямую они не вызываются и не попадают под действия соглашений Katana.

При разработке реального модуля создание coolModule и вызовы Use() можно разместить в методе-расширении интерфейса IAppBuilder. Тогда сам CoolModule можно не раскрывать и сделать internal. Кроме того, пользователь сможет подключать и настраивать модуль просто вызовом appBuilder.UseCoolAPI(agr):

public static class IAppBuilderExtensionsForCoolModule
{
    public static void UseCoolAPI(this IAppBuilder appBuilder, int someAgr)
    {
        var coolModule = new CoolModule();

        appBuilder.Use(new Func<AppFunc, AppFunc>(next =>
            env => coolModule.HandleStatic(env, next)));

        /* Тут может быть логика подключения разных обработчиков в зависимости от someAgr. */

        appBuilder.Use(new Func<AppFunc, AppFunc>(next =>
            env => coolModule.HandleCompression(env, next)));
    }
}

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

Но достаточно теории – перейдем к практике.

Пишем функцию обработчик

Разместим код создаваемого обработчика непосредственно в методе Configuration():

namespace OwinDemo
{
    using System;
    using System.Collections.Generic;
    using System.Text;
    using Owin;

    using AppFunc = System.Func<System.Collections.Generic.IDictionary<string, object>, System.Threading.Tasks.Task>;

    public class Startup
    {
        public void Configuration(IAppBuilder appBuilder)
        {
            appBuilder.Use(typeof(LoggerModule), "Log Module >");

            appBuilder.Use(new Func<AppFunc, AppFunc>(next =>
                env => {
                    var headers = (IDictionary<string, string[]>)env["owin.ResponseHeaders"];
                    headers.Add("Content-Type", new[] { "text/plain; charset=utf-8" });

                    var outstream = (System.IO.Stream)env["owin.ResponseBody"];
                    var buffer = Encoding.UTF8.GetBytes("Извините, но запрошенный адрес не существует.");
                    return outstream.WriteAsync(buffer, 0, buffer.Length);
                }));
        }
    }
}

Обратите внимание на порядок вызовов Use(). Как уже упоминалось, модули вызываются в том порядке, в котором они были зарегистрированы. Только что созданный обработчик всегда завершает цепочку вызовов. Поэтому если подключить его до LoggerModule, то последний никогда не будет вызван.

Сам код достаточно простой. В нем устанавливается тип содержимого ответа (просто текст) и кодировка (UTF8). После чего выводится заданная строка. Возможные оптимизации и обработка ошибок не приведены для упрощения примера. Также стоит отметить, что созданный код по прежнему полагается только на соглашения и не использует какие-либо внешние дополнительные библиотеки.

И в завершении еще один пример: заменим обработчик по умолчанию на свой. Однако, в этом случае вместо Func<AppFunc, AppFunc> необходим просто AppFunc, ведь передавать запрос дальше некуда:

namespace OwinDemo
{
    using System.Collections.Generic;
    using System.Text;
    using Owin;

    using AppFunc = System.Func<System.Collections.Generic.IDictionary<string, object>, System.Threading.Tasks.Task>;

    public class Startup
    {
        public void Configuration(IAppBuilder appBuilder)
        {
            appBuilder.Use(typeof(LoggerModule), "Log Module >");

            appBuilder.Properties["builder.DefaultApp"] = new AppFunc(
                env => {
                    var headers = (IDictionary<string, string[]>)env["owin.ResponseHeaders"];
                    headers.Add("Content-Type", new[] { "text/plain; charset=utf-8" });

                    var outstream = (System.IO.Stream)env["owin.ResponseBody"];
                    var buffer = Encoding.UTF8.GetBytes("Извините, но запрошенный адрес не существует.");
                    return outstream.WriteAsync(buffer, 0, buffer.Length);
                });
        }
    }
}

Если запустить проект, то можно убедиться, что оба созданных модуля выполняют свои функции.

А дальше давайте посмотрим, как Katana помогает избежать создания "велосипедов".


Исходный код проекта (C#, Visual Studio 2012): OwinDemo-part3-2.zip

Добавить комментарий