Первоначально опубликовано на http://kcieslak.io/Magic-of-Saturn-controllers

Введение

Saturn - это новый веб-фреймворк на F #, который более функционально реализует хорошо известный шаблон проектирования - MVC. Несмотря на то, что Saturn - довольно молодой проект, он становится все более популярным среди сообщества F # и промышленных пользователей. Одна из основных целей Saturn - создать абстракции высокого уровня, которые позволят разработчикам сосредоточиться на написании домена, бизнес-кода вместо того, чтобы сосредоточиться на создании правильной маршрутизации для вашего приложения или настройке правильных заголовков ответов. Одна из таких абстракций, о которой я хочу сегодня поговорить, - это controller.

Эта статья была создана для Saturn версии 0.7.x

Базовое использование

controller - это простое вычислительное выражение (CE), которое позволяет легко реализовать конечную точку приложения в соответствии с соглашениями в стиле REST. Его можно использовать для реализации конечных точек, которые отрисовывают представления, если вы создаете приложение с использованием рендеринга на стороне сервера, или просто возвращаете сериализованные данные, если вы создаете API или ваше приложение использует рендеринг на стороне клиента. Как и все другие CE, используемые в Saturn, controller предоставляет набор настраиваемых операций, которые вы можете использовать. И, что важно, все операции в controller CE являются необязательными, что означает, что вы можете легко выбрать, какой набор функций вам нужен. Пример, базовая реализация выглядит так:

let resource = controller {
    index indexAction
    show showAction
    add addAction
    edit editAction
    create createAction
    update updateAction
    patch patchAction
    delete deleteAction
    deleteAll deleteAllAction
}

Давайте теперь пойдем по очереди и опишем каждую операцию:

  • index - отображается в GET запрос на / конечной точке. Обычно используется для визуализации представления, отображающего список элементов, или для возврата всего списка элементов.
  • show - отображается в GET запрос на /:id конечной точке. Обычно используется для рендеринга представления, отображающего детали определенного элемента, или для возврата одного элемента с заданным идентификатором.
  • add - отображается в GET запрос на /add конечной точке. Используется для отображения формы для добавления нового элемента. Обычно не используется в контроллерах API.
  • edit - отображается в GET запрос в /:id/edit. Используется для визуализации формы для редактирования существующего элемента. Обычно не используется в контроллерах API.
  • create - отображается в POST запрос на / конечной точке. Используется для создания и сохранения нового объекта.
  • update - сопоставлен с POST и PUT запросами на /:id конечной точке. Используется для обновления существующего элемента. Обычно заменяет исходный элемент (с сохранением идентификатора) и требует, чтобы во входящем элементе были заполнены поля.
  • patch - отображается в PATCH запрос на /:id конечной точке. Используется для обновления существующего элемента. Обычно изменяются только некоторые поля исходного элемента, тело запроса содержит только измененные поля или объект JSON Patch.
  • delete - отображается в DELETE запрос на /:id конечной точке. Используется для удаления или деактивации существующего элемента.
  • deleteAll - отображается в DELETE запрос на / конечной точке. Используется для удаления или деактивации всех элементов.

Помните, что Сатурн никоим образом не навязывает поведение или вводит действия, поэтому приведенные выше описания являются предложениями и лучшими практиками, а не чем-то, что закодировано во фреймворке. Единственное, что обеспечивает контроллер, - это каменная фрезерная конструкция.

Выполнение действий

Все действия indexAction, showAction… являются простыми функциями F #. Все они в качестве первого параметра принимают объект HttpContext - это класс ASP.NET, содержащий всю информацию о входящем запросе, ответе, сервере, среде и других данных, которые были введены в него фреймворком. Действия, использующие идентификатор элемента, например showAction или editAction, - это функции, которые получают id в качестве второго параметра. id может быть общим, но в настоящее время мы поддерживаем ограниченный набор возможных типов, в которые мы можем декодировать идентификатор из URL-адреса.

Поддерживаемые типы:

  • string
  • char
  • int
  • int64
  • float
  • bool
  • System.Guid

Если вам понадобится какой-то собственный тип идентификатора, я бы порекомендовал использовать string и десериализовать его вручную. Еще одним важным ограничением текущих контроллеров является тот факт, что все действия должны использовать один и тот же тип идентификатора в одном экземпляре контроллера. Опять же, если вам нужны разные типы идентификаторов - используйте string и десериализуйте его вручную.

Пример реализации действия может выглядеть так:

let myIndex (ctx: HttpContext) = Controller.text ctx "Hello world"
let myShow (ctx: HttpContext) (id: string) =
   id
   |> sprintf "Hello world, %s"
   |> Controller.text ctx
let myController = controller {
   index myIndex
   show myShow
}

Тип вывода действия

Если вы наведете курсор на myIndex или myShow, вы заметите, что тип возврата этих функций - Task<HttpContext option>. Во-первых, все действия в контроллерах Saturn асинхронны по конструкции, и они используют стандартную задачу .Net для ее моделирования. Однако они являются общими в отношении того, какой тип на самом деле возвращает задача. Если вы вернете HttpContext option, вы будете следовать стандартному пути интеграции с Giraffe (веб-библиотека, на которой построен Saturn). Это дает вам не только полный контроль над тем, что происходит и как изменяется ваш ответ, но также дает возможность интегрироваться с существующей экосистемой Giraffe. Кроме того, сам Saturn предоставляет богатый набор помощников, которые возвращают Task<HttpContext option> в модуле Controller (примером этого является функция Controller.text, используемая в примере, которая устанавливает содержимое ответа для данной строки, а также устанавливает соответствующий заголовок ответа).

Но возвращение Task<HttpContext option> - не единственный вариант. Вы также можете вернуть Task<’a> (где ’a - любой тип), и Saturn выполнит автоматическое согласование содержимого вывода. В таком случае Saturn проверит тип вывода вашего действия, проверит предпочтения клиента на основе заголовка Accept (если заголовок Accept отсутствует, вместо него будет использоваться заголовок Content-Type) и решит, как лучше всего обрабатывать объект ответа:

  • Если вы вернете string, Saturn вернет его с text/plain или text/html as Content-Type в зависимости от заголовка Accept
  • Если вы вернете GiraffeViewEngine.XmlNode (объект представления Жирафа) и клиент примет text/html ответов, Сатурн отобразит представление и вернет клиенту
  • Если вы вернете любой другой тип, он будет десериализован в JSON (с application/json as Content-Type), если клиент не примет ответ JSON - в этом случае будет использоваться XML.

Такой же алгоритм согласования выходного содержимого предоставляется Controller.response helper.

Пример реализации действия с использованием согласования содержимого вывода:

let myIndex (ctx: HttpContext) =
    task { return "Hello world" }
let myShow (ctx: HttpContext) (id: string) =
    task { 
        return sprintf "Hello world, %s" id
    }
let myAdd (ctx: HttpContext) =
    task { return DateTime.Now }
let myController = controller {
    index myIndex
    show myShow
    add myAdd
}

Контроль версий

Управление версиями конечных точек - одна из самых важных сквозных проблем в веб-приложениях ... и большинство веб-фреймворков не предоставляют встроенных способов легко с этим справиться. Saturn предлагает удобные способы упрощения версий ваших контроллеров. Saturn использует 1 из 3 неправильных способов - пользовательский заголовок x-controller-version, чтобы решить, какую версию элемента управления следует вызвать. Конечно, если вам не нравится эта стратегия, Saturn позволяет легко вернуться к немного более низкому уровню абстракции, так что вы можете создать совершенно неверную стратегию управления версиями.

Реализация управления версиями в ваших контроллерах тривиальна - просто добавляется одна дополнительная операция к вашим контроллерам - version. Вот пример:

let myController = controller {
    index myIndex
    show myShow
    add myAdd
}
let myControllerV1 = controller {
    version "1"
index myIndex
    show myShow
    add myAdd
}
let appRouter = router {
    forward "/endpoint" myControllerV1
    forward "/endpoint" myController
}

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

Вилки

Еще одна важная особенность любого веб-фреймворка - возможность (декларативно) подключать некоторые дополнительные действия / модификации для определенных действий в контроллерах. Например, в ASP .NET MVC это делается с помощью атрибутов и включает такие функции, как авторизация и аутентификация для определенных действий в контроллере (и многие, многие другие функции и сквозные проблемы). Saturn предоставляет гибкий механизм для обеспечения таких функций с помощью разъемов контроллера, используя одну простую операцию CE - plug, которая принимает список действий, к которым он должен применяться, и функцию подключения. Реализация плагина - это любая HttpHandler, что означает, что она хорошо интегрируется, если существующая экосистема и помощники, а реализация плагина отделена от самого контроллера, что означает, что вы можете легко создавать плагины для сквозных задач, таких как ведение журнала или авторизация, и повторно использовать их на многих контроллерах в ваше приложение.

Пример реализации:

let myControllerV1 = controller {
    plug [All] (setHttpHeader "controller-all" "123")
    plug [Index; Show] (setHttpHeader "controller-some" "456")
    plug (except Index) (setHttpHeader "controller-except" "789")
index myIndex
    show myShow
    add myAdd
}

Субконтроллеры

Последняя важная особенность controller - это возможность встраивать контроллеры. Это, опять же, довольно самоуверенная функция, которая следует соглашениям в стиле REST. Субконтроллер следует использовать в случае, когда один конкретный элемент (представленный в контроллере как /:id) имеет несколько дочерних элементов - например, элемент blog содержит список из post элементов. Или элемент post содержит список из comment элементов. Субконтроллер включен в /:id/:subcontrollerPath маршрут исходного контроллера (так, например, /:id/:subcontrollerPath/:id2 показывает конкретный комментарий, или /:id/:subcontrollerPath/add будет отображать форму для добавления нового дочернего элемента к родительскому элементу с заданным идентификатором). Добавление субконтроллера к вашему контроллеру выполняется с помощью еще одной пользовательской операции в CE - subController, которая принимает путь к субконтроллеру и дочернему контроллеру в качестве входных данных (передача идентификатора этому субконтроллеру).

Пример реализации:

let commentController userId = controller {
    index (fun ctx -> (sprintf "Comment Index %i" userId ) |> Controller.text ctx)
    add (fun ctx -> (sprintf "Comment Add %i" userId ) |> Controller.text ctx)
    show (fun ctx id -> (sprintf "Show comment %s %i" id userId ) |> Controller.text ctx)
    edit (fun ctx id -> (sprintf "Edit comment %s %i" id userId ) |> Controller.text ctx)
}
let userControllerVersion1 = controller {
    subController "/comments" commentController
    index (fun ctx -> "Index handler" |> Controller.text ctx)
    add (fun ctx -> "Add handler" |> Controller.text ctx)
    show (fun ctx id -> (sprintf "Show - %i" id) |> Controller.text ctx)
    edit (fun ctx id -> (sprintf "Edit - %i" id) |> Controller.text ctx)
}

subController операция фактически принимает любые HttpHandler не только контроллеры, а это значит, что у вас есть что-нибудь внутри. Кроме того, вы можете добавить несколько субконтроллеров к одному контроллеру, что может быть полезно… например, в сочетании с функцией управления версиями контроллера.

Резюме

В этом посте я попытался представить все возможности и возможности высокоуровневой абстракции Saturn - controller, а также гибкий дизайн, который они позволяют. Включение такого высокого уровня абстракций и предоставление нестандартного решения для некоторых сквозных проблем, таких как маршрутизация контроллера REST или управление версиями ваших API-интерфейсов, является одной из целей дизайна Saturn, и, надеюсь, более высокого уровня конструкции будут включены в каркас.