Binsor je skvělou konfigurační DSL pro Windsor IoC kontejner. Proč nevyužít možností, které nám využití jazyka Boo přináší a nerozšířit si Binsor o pár dalších pravidel pro konfiguraci routingu v ASP.NET MVC?
Routing v ASP.NET MVC
System.Web.Routing je jedním ze základních kamenů ASP.NET
MVC. Přijímá parametry z URL a posílá je MvcRouteHandleru,
který vybere controller a jeho akci, která má požadavek zpracovat. Routing
je na MVC nezávislý a využívá se třeba i v ASP.NET Dynamic Data.
Pokud chceme routing nakonfigurovat, máme několik možností, jak toho dosáhnout. Tou první je využít standardní API:
public void Application_Start() {
RouteTable.Routes.Add(
new Route("{resource}.axd/{*pathInfo}", new StopRoutingHandler())
);
RouteTable.Routes.Add("default",
new Route("{controller}/{action}/{id}", new MvcRouteHandler()) {
Defaults = new RouteValueDictionary(new {
controller = "Home",
action = "Index"
})
}
);
}
Jak jistě vidíte, psát takto routovací pravidla je celkem na dlouho. Kód obsahuje zbytečně mnoho šumu a můžete snadno něco opomenout. Proto je součástí MVC frameworku několik extenzí, které mají konfiguraci usnadnit:
public void Application_Start() {
RouteTable.Routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
RouteTable.Routes.MapRoute(
"default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index" }
);
}
Takový kód už je o poznání kratší, většina šumu už je pryč, ale stejně tu zůstává několik zbytečných znaků a na první pohled není vidět, co vlastně je ta anonymní třída. Navíc jde o konfiguraci a ta by si zasloužila být v konfiguračním souboru. Změna routy si pak nevyžádá rekompilaci aplikace, ale pouze restart… Proto jsem se rozhodl vytvořit si vlastní konfigurační DSL a to za pomocí Boo.
Krok první: návrh DSL
Začněme tedy s tím, jak by taková DSL měla vypadat. To, co má dělat, je už nastíněno výše. První můj návrh vypadal asi takto:
ignore:
route "{resource}.axd/{*pathInfo}"
map:
route "Default", "{controller}/{action}/{id}":
defaults:
controller = "Home"
action = "Index"
Bloky ignore a map by mohli mít vícero potomků
route. Tuto variantu jsem však záhy zapověděl, protože
vyžaduje zbytečné odsazení navíc a v delších konfiguracích už nemusí
být jasné, jestli zrovna jsme v bloku ignore nebo
map popřípadě jiném. Tak jsem nad tím přemýšlel a nakonec
zvolil následující variantu:
ignore_route "{resource}.axd/{*pathInfo}"
map_route "Default", "{controller}/{action}/{id}":
defaults:
controller = "Home"
action = "Index"
Takže syntaxi bych měl, teď přijde ta veselejší část: implementace.
Krok druhý: implementace DSL
Jedním z důvodů proč jsou oblíbené dynamické jazyky jako Ruby a Python je meta programování. Meta programování nám umožňuje vytvářet nové programové konstrukce v jazyku, zkrátka obohatit jeho sémantiku. Já si pro implementaci vybral Boo.
Ačkoli je Boo statický jazyk, díky otevřenému konceptu kompilátoru, můžeme jazyk obohacovat stejně efektivně, jako v jazyce dynamickém. Výhodou je, že gramatickou správnost našich rozšíření můžeme ověřit při kompilaci. Základním kamenem při tvorbě vlastních DSL jsou meta funkce, které jsem však nepoužil, a makra.
Makro je třída.
Ano lze ho napsat v jakémkoli jazyce nad CLI. Stačí nám implementovat
rozhraní IAstMacro a třídu pojmenovat s konvenčním
přídomkem Macro, např. Map_routeMacro.
V implementovaných metodách si pak můžete dle libosti hrát s AST. Takhle je v C# napsanej třeba
Binsor. Já jsem však k implementaci routovacích maker zvolil přímo jazyk
Boo, který má od verze 0.8.1 podporu pro snadné psaní maker přímo
v jazyce.
Začneme jednodušším makrem pro ignorování routy:
import Boo.Lang.Extensions
import Boo.Lang.Compiler
import Boo.Lang.Compiler.Ast
import Boo.Lang.Compiler.Ast.Visitors
macro ignore_route:
path, = ignore_route.Arguments
block = Block()
block.Add([| RouteTable.Routes.Add(Route($path, StopRoutingHandler())) |])
return block
Pro snadnější pochopení si kód trochu popíšeme. Na prvních řádkách
si naimportujeme potřebné jmenné prostory. Klíčovým slovem
macro zahájíme psaní makra a v zápětí mu přiřadíme
jméno, protože, co dokážeme pojmenovat, dokážeme ovládat. Pak si načteme
první parametr a uložíme si ho do proměnné path. Pak si
vytvoříme nový blok kódu. A teď začíná magic. Všechno co je v bloku
[| … |] je normální kód v Boo, který se převede na AST.
Pomocí $ lze do toho kódu zasahovat zvenčí, takže místo
$path bude v kódu obsah proměnné path.
Připravený blok kódu pak vrátíme kompilátoru.
Díky našemu makru se kód ignore_route "test" přeloží na
RouteTable.Routes.Add(Route("test", StopRoutingHandler())) a pokud
je naimportován jmenný prostor System.Web.Routing, kompilátor
všemu rozumí a máme jednoduchou syntaxi pro definování
ignorovaných rout.
Bylo to celkem snadné, že? Satačí pochopit základní princip. Ono totiž
klíčové slovo macro není nic jiného než makro, které bere
jako první parametr název makra a následující blok je de facto
implementací metody Expand. :)
Postoupíme dál. Makro map_route už bude o něco větší
oříšek, protože může obsahovat vnořené bloky defaults, kde
jsou vypsané výchozí hodnoty parametrů routy, a constraints,
kde jsou podmínky, které tyto parametry musejí splňovat.
macro map_route:
name, path = map_route.Arguments
defaults = map_route["defaults"] as MacroStatement or MacroStatement()
constraints = map_route["constraints"] as MacroStatement or MacroStatement()
block = Block()
block.Add([| route = Route($path, MvcRouteHandler()) |])
block.Add([| route.Defaults = RouteValueDictionary() |])
block.Add([| route.Constraints = RouteValueDictionary() |])
for exp as ExpressionStatement in defaults.Block.Statements:
binary = exp.Expression as BinaryExpression
block.Add([| route.Defaults.Add($(binary.Left.ToString()), $(binary.Right)) |])
for exp as ExpressionStatement in constraints.Block.Statements:
binary = exp.Expression as BinaryExpression
block.Add([| route.Constraints.Add($(binary.Left.ToString()), $(binary.Right)) |])
block.Add([| RouteTable.Routes.Add($name, route) |])
return block
macro defaults:
allowedParents as List = [ "map_route" ]
parent as MacroStatement = defaults.GetAncestor(NodeType.MacroStatement)
assert allowedParents.Contains(parent.Name)
parent["defaults"] = defaults
macro constraints:
allowedParents as List = [ "map_route", "ignore_route" ]
parent as MacroStatement = constraints.GetAncestor(NodeType.MacroStatement)
assert allowedParents.Contains(parent.Name)
parent["constraints"] = constraints
Makro map_route je trošku rozšířenou obdobou
ignore_route, podívejme se na makro defaults, které
přináší něco jiného. Zprvu si nadefinujeme, jaká makra mohou toto makro
obsahovat, a následně ověříme, že opravdu není v jiném makru. Pak
vezmeme kód tohoto makra a pošleme ho ke zpracování nadřazenému makru.
Žádný kód generovat nebudeme. Makro constraints je na tom
téměr stejně, jen předpokládá, že bude i v ignore_route,
což zatím není implementováno, ale budiž.
Vraťme se k makru map_route a konkrétně k for
cyklům. Tam totiž projíždíme předané kusy kódu. Jednotlivé řádky
považujeme za výrazy. Konkrétně výraz přiřazení, což je binární
výraz. Binární výraz má vždy pravou a levou stranu a operátor. Na levé
straně máme klíče do RouteValueDictionary, což jsou řetězce.
Proto levou stranu musíme převést na řetězec metodou
ToString(). Pravá strana je hodnota přidružená ke klíči a
může to být cokoli (object). Takže tam i cokoli pošleme.
Vezmeme oba kusy kódu a uložíme je do souboru
RoutingMacros.boo
Krok třetí: užívání DSL
Než začneme makra využívat, musíme si je nejprve naimportovat. Na
začátek souboru Windsor.boo přidáme řádky:
import file from RoutingMacros.boo
import System.Web.Routing
import System.Web.Mvc
No a potom už můžeme na konci souboru přidávat routovací pravidla.
Update
Kompletní skripty pro routování naleznete v článku Oprava předchozích
článků, kde jsem provedl několik oprav v makru
ignore_route a lehký refactoring.











Komentáře
aprilchild
20.59 - 29. prosince 2008 | #
Aleš Roubíček
09.26 - 30. prosince 2008 | #
aprilchild
15.14 - 30. prosince 2008 | #
Aleš Roubíček
18.15 - 30. prosince 2008 | #
Místo pro tvůj názor