Первоначально опубликовано на 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
asContent-Type
в зависимости от заголовкаAccept
- Если вы вернете
GiraffeViewEngine.XmlNode
(объект представления Жирафа) и клиент приметtext/html
ответов, Сатурн отобразит представление и вернет клиенту - Если вы вернете любой другой тип, он будет десериализован в JSON (с
application/json
asContent-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, и, надеюсь, более высокого уровня конструкции будут включены в каркас.