Routing requests and serving content with Sitelets
Sitelets are WebSharper's primary way to create server-side content. They provide facilities to route requests and generate HTML pages or JSON responses.
Sitelets allow you to:
Dynamically construct pages and serve arbitrary content.
Have full control of your URLs by specifying custom routers for linking them to content, or let the URLs be automatically inferred from an endpoint type.
Compose contents into sitelets, which may themselves be composed into larger sitelets.
Have safe links for referencing other content contained within your site.
Use the type-safe HTML and templating facilities from UI on the server side.
Automatically parse JSON requests and generate JSON responses based on your types.
Below is a minimal example of a complete site serving one HTML page:
using System;
using System.Threading.Tasks;
using WebSharper.Sitelets;
using static WebSharper.UI.CSharp.Html;
namespace MyWebsite
{
/// Endpoint for the single URL "/".
[EndPoint("/")]
public class Index { }
public class SampleSite
{
/// The content returned when receiving an Index request.
public static Task<Content> IndexContent(Context ctx)
{
var time = DateTime.Now.ToString();
return Content.Page(
Title: "Index",
Body: h1("Current time: ", time)
);
}
/// Defines the website's responses based on the parsed Endpoint.
[Website]
public static Sitelet<Index> MySampleWebsite =>
Sitelet.Content("/index", new Index(), IndexContent)
}
}
First, custom endpoint types are defined. They are used for linking requests to content within your sitelet. Here, you only need one endpoint, Index
, corresponding to your only page.
The content of the index page is defined as a Content.Page
, where the body consists of a server side HTML element. Here the current time is computed and displayed within an <h1>
tag.
The MySampleWebsite
value has type Sitelet<object>
. It defines a complete website: the URL scheme, the EndPoint
value corresponding to each served URL (only one in this case), and the content to serve for each endpoint. It uses the SiteletBuilder
class to construct a sitelet for the Index endpoint, associating it with the /index
URL and serving IndexContent
as a response.
MySampleWebsite
is annotated with the attribute [Website]
to indicate that this is the sitelet that should be served.
Routing
WebSharper Sitelets abstract away URLs and request parsing by using an endpoint type that represents the different HTTP endpoints available in a website. For example, a site's URL scheme can be represented by the following endpoint types:
[EndPoint("/")]
public class Index { }
[EndPoint("/stats/{username}")]
public class Stats
{
public string username;
}
[EndPoint("/blog/{id}/{slug}")]
public class BlogArticle
{
public int id;
public string slug;
}
Based on this, a Sitelet is a value that represents the following mappings:
Mapping from requests to endpoints. A Sitelet is able to parse a URL such as
/blog/1243/some-article-slug
into the endpoint valuenew BlogArticle {id = 1243, slug = "some-article-slug"}
. More advanced definitions can even parse query parameters, JSON bodies or posted forms.Mapping from endpoints to URLs. This allows you to have internal links that are verified by the type system, instead of writing URLs by hand and being at the mercy of a typo or a change in the URL scheme. You can read more on this in the "Context" section.
Mapping from endpoints to content. Once a request has been parsed, this determines what content (HTML or other) must be returned to the client.
Trivial Sitelets
Two helpers exist for creating a Sitelet with a trivial router: only handling requests on the root.
Application.Text
takes aFunc<Context, string>
function and creates a Sitelet that serves the result string as a text response.Application.SinglePage
takes aFunc<Context, Task<Content>>
function and creates a Sitelet that serves the returned content.
SiteletBuilder
A handy way to create a Sitelet is by using the SiteletBuilder
class. It functions conceptually similar to StringBuilder
, you can chain methods to assemble a final value:
- First, create an instance of
SiteletBuilder()
; - Then, add your mappings using
.With<T>(...)
method calls; - Finally, use
.Install()
to return your constructedSitelet
.
For example, using the endpoint types defined in the above section, you can create the following Sitelet:
[Website]
public static Sitelet<object> MySampleWebsite =>
new SiteletBuilder()
.With<Index>((ctx, endpoint) =>
Content.Page(
Title: "Welcome!",
Body: h1("Index page")
)
)
.With<Stats>((ctx, endpoint) =>
Content.Page(
Body: doc("Stats for ", endpoint.username)
)
)
.With<BlogArticle>((ctx, endpoint) =>
Content.Page(
Body: doc($"Article id {endpoint.id}, slug {endpoint.slug}")
)
)
.Install();
The above sitelets accepts URLs with the following shape:
Accepted Request: GET /Index
Parsed Endpoint: new Index()
Returned Content: <!DOCTYPE html>
<html>
<head><title>Welcome!</title></head>
<body>
<h1>Index page</h1>
</body>
</html>
Accepted Request: GET /Stats/someUser
Parsed Endpoint: new Stats { username = "someUser" }
Returned Content: <!DOCTYPE html>
<html>
<head></head>
<body>
Stats for someUser
</body>
</html>
Accepted Request: GET /BlogArticle/1423/some-article-slug
Parsed Endpoint: new BlogArticle { id = 1423, slug = "some-article-slug" }
Returned Content: <!DOCTYPE html>
<html>
<head></head>
<body>
Article id 1423, slug some-article-slug
</body>
</html>
It is also possible to create an endpoint for a specific URL, without associating an endpoint type to it:
new SiteletBuilder()
.With("/static-url", ctx =>
Content.Text("Replying to /static-url")
)
// Accepted Request: GET /static-url
// Returned Content: Replying to /static-url
Defining EndPoints
The following types can be used as endpoints:
- Numbers and strings are encoded as a single path segment.
SiteletBuilder().With<string>(/* ... */)
// Accepted Request: GET /abc
// Parsed Endpoint: "abc"
// Returned Content: (determined by .With())
SiteletBuilder().With<int>(/* ... */)
// Accepted Request: GET /1423
// Parsed Endpoint: 1423
// Returned Content: (determined by .With())
- Arrays are encoded as a number representing the length, followed by each element.
SiteletBuilder().With<string[]>(/* ... */)
// Accepted Request: GET /2/abc/def
// Parsed Endpoint: new string[] { "abc", "def" }
// Returned Content: (determined by .With())
System.Tuple
s,ValueTuple
s and objects are encoded with their fields as consecutive path segments.
SiteletBuilder().With<(int, string)>(/* ... */)
// Accepted Request: GET /1/abc
// Parsed Endpoint: new Tuple<int, string>(1, "abc")
// Returned Content: (determined by .With())
class T
{
int Number;
string Name;
}
// Accepted Request: GET /1/abc
// Parsed Endpoint: new T { Number = 1, Name = "abc" }
// Returned Content: (determined by .With())
- Objects with an
[EndPoint]
attribute are prefixed with the given path segment.
[EndPoint("/test/{number}/{name}")]
class T
{
int number;
string name;
}
SiteletBuilder().With<T>(/* ... */)
// Accepted Request: GET /test/1/abc
// Parsed Endpoint: new EndPoint { number = 1, name = "abc" }
// Returned Content: (determined by .With())
- Enumerations are encoded as their underlying type.
SiteletBuilder().With<System.IO.FileAccess>(/* ... */)
// Accepted Request: GET /3
// Parsed Endpoint: System.IO.FileAccess.ReadWrite
// Returned Content: (determined by .With())
System.DateTime
is serialized with the formatyyyy-MM-dd-HH.mm.ss
by default. Use[DateTimeFormat(string)]
on a field to customize it. Be careful as some characters are not valid in URLs; in particular, the ISO 8601 round-trip format ("o"
format) cannot be used because it uses the character:
.
SiteletBuilder().With<DateTime>(/* ... */)
// Accepted Request: GET /2015-03-24-15.05.32
// Parsed Endpoint: System.DateTime(2015,3,24,15,5,32)
// Returned Content: (determined by .With())
class T
{
[DateTimeFormat("yyy-MM-dd")]
DateTime date;
}
SiteletBuilder().With<T>(/* ... */)
// Accepted Request: GET /2015-03-24
// Parsed Endpoint: System.DateTime(2015,3,24)
// Returned Content: (determined by .With())
- The attribute
[Method("GET", "POST", ...)]
on a class indicates which methods are accepted by this endpoint. Without this attribute, all methods are accepted.
[Method("POST")]
class PostArticle
{
int id;
}
SiteletBuilder().With<PostArticle>(/* ... */)
// Accepted Request: POST /article/12
// Parsed Endpoint: new PostArticle { id = 12 }
// Returned Content: (determined by .With())
- If an endpoint accepts only one method, then a more concise way to specify it is directly in the
[EndPoint]
attribute:
[EndPoint("POST /article/{id}")]
class PostArticle
{
int id;
}
SiteletBuilder().With<PostArticle>(/* ... */)
// Accepted Request: POST /article/12
// Parsed Endpoint: new PostArticle { id = 12 }
// Returned Content: (determined by .With())
- A common trick is to use
[EndPoint("GET /")]
on a field-less class to indicate the home page.
[EndPoint("/")]
class Home { }
SiteletBuilder().With<Home>(/* ... */)
// Accepted Request: GET /
// Parsed Endpoint: new Home()
// Returned Content: (determined by .With())
- If several classes have the same
[EndPoint]
, then parsing tries them in the order in which they are passed to.With()
until one of them matches:
[EndPoint("GET /blog")]
class AllArticles { }
[EndPoint("GET /blog/{id}")]
class ArticleById
{
int id;
}
[EndPoint("GET /blog/{slug}")]
class ArticleBySlug
{
string slug;
}
SiteletBuilder()
.With<AllArticles>(/* ... */)
.With<ArticleById>(/* ... */)
.With<ArticleBySlug>(/* ... */)
// Accepted Request: GET /blog
// Parsed Endpoint: new AllArticles()
// Returned Content: (determined by .With())
//
// Accepted Request: GET /blog/123
// Parsed Endpoint: new ArticleById { id = 123 }
// Returned Content: (determined by .With())
//
// Accepted Request: GET /blog/my-article
// Parsed Endpoint: new ArticleBySlug { slug = "my-article" }
// Returned Content: (determined by .With())
[Query]
on a field indicates that this field must be parsed as a GET query parameter instead of a path segment. The value of this field must be either a base type (number, string) or anNullable
of a base type (in which case the parameter is optional).
[EndPoint]
class Article
{
[Query]
int id;
[Query]
string slug;
}
SiteletBuilder().With<Article>(/* ... */)
// Accepted Request: GET /article?id=1423&slug=some-article-slug
// Parsed Endpoint: new Article { id = 1423, slug = "some-article-slug" }
// Returned Content: (determined by .With())
//
// Accepted Request: GET /article?id=1423
// Parsed Endpoint: new Article { id = 1423, slug = null }
// Returned Content: (determined by .With())
- You can of course mix Query and non-Query parameters.
[EndPoint("{id}")]
class Article
{
int id;
[Query]
string slug;
}
SiteletBuilder().With<Article>(/* ... */)
// Accepted Request: GET /article/1423?slug=some-article-slug
// Parsed Endpoint: new Article { id = 1423, slug = Some "some-article-slug" }
// Returned Content: (determined by .With())
[Json]
on a field indicates that it must be parsed as JSON from the body of the request. If an endpoint type contains several[Json]
fields, a runtime error is thrown.
[EndPoint("POST /article/{id}")]
class PostArticle
{
int id;
[Json]
PostArticleData data;
}
[Serializable]
class PostArticleData
{
string slug;
string title;
}
SiteletBuilder().With<PostArticle>(/* ... */)
// Accepted Request: POST /article/1423
//
// {"slug": "some-blog-post", "title": "Some blog post!"}
//
// Parsed Endpoint: new PostArticle {
// id = 1423,
// data = new PostArticleData {
// slug = "some-blog-post",
// title = "Some blog post!" } }
// Returned Content: (determined by .With())
[Wildcard]
on a field indicates that it represents the remainder of the url's path. That field can be aT[]
or astring
. If an endpoint type contains several[Wildcard]
fields, a runtime error is thrown.
[EndPoint("/articles/{id}")]
class Articles
{
int pageId;
[Wildcard]
string[] tags;
}
[EndPoint("/articles")]
class Articles2
{
[Wildcard]
(int, string) tags;
}
[EndPoint("/file")]
class File
{
[Wildcard]
string file;
}
SiteletBuilder()
.With<Articles>(/* ... */)
.With<Articles2>(/* ... */)
.With<File>(/* ... */)
// Accepted Request: GET /articles/123/csharp/websharper
// Parsed Endpoint: new Articles {
// pageId = 123,
// tags = new[] { "csharp", "websharper" } }
// Returned Content: (determined by .With())
//
// Accepted Request: GET /articles/123/csharp/456/websharper
// Parsed Endpoint: new Articles2 { tags = new[] {
// (123, "csharp"), (456, "websharper") } }
// Returned Content: (determined by .With())
//
// Accepted Request: GET /file/css/main.css
// Parsed Endpoint: new File { file = "css/main.css" }
// Returned Content: (determined by .With())
Other Constructors and Combinators
The following functions are available to build simple sitelets or compose more complex sitelets out of simple ones:
Sitelet.Empty<T>()
creates a Sitelet which does not recognize any URLs.Sitelet.Content
, builds a sitelet that accepts a single URL and maps it to a given endpoint and content.
Sitelet.Content("/index", new Index(), IndexContent)
// Accepted Request: GET /index
// Parsed Endpoint: Index
// Returned Content: (value of IndexContent : Content<EndPoint>)
Sitelet.Infer
is used bySiteletBuilder.With
internally. UsingSitelet.Infer<EndPoint>(CreateContent)
is equal tonew SiteletBuilder().
Sitelet.Sum
takes any number of Sitelets (given as parameters, or as anIEnumerable<Sitelet<T>>
) and tries them in order until one of them accepts the URL. It is generally used to combine a list ofSitelet.Content
s.The following sitelet accepts
/index
and/about
:
Sitelet.Sum(
Sitelet.Content("/index", new Index(), IndexContent),
Sitelet.Content("/about", new About(), AboutContent)
)
// Accepted Request: GET /index
// Parsed Endpoint: Index
// Returned Content: (value of IndexContent : Content<EndPoint>)
//
// Accepted Request: GET /about
// Parsed Endpoint: About
// Returned Content: (value of AboutContent : Content<EndPoint>)
+
operator can be used on two Sitelets to try them in order.s1 + s2
is equivalent toSitelet.Sum(s1, s2)
.
Sitelet.Content("/index", new Index(), IndexContent)
| Sitelet.Content("/about", new About(), AboutContent)
// Same as above.
For the mathematically enclined, the functions Sitelet.Empty
and +
make sitelets a monoid. Note that it is non-commutative: if a URL is accepted by both sitelets, the left one will be chosen to handle the request.
.Shift
takes a Sitelet and shifts it by a path segment.
Sitelet.Content("/index", new Index(), IndexContent).Shift("folder")
// Accepted Request: GET /folder/index
// Parsed Endpoint: Index
// Returned Content: (value of IndexContent : Content<EndPoint>)
Sitelet.Folder
takes a sequence of Sitelets and shifts them by a path segment. It is effectively a combination ofSum
andShift
.
Sitelet.Folder("folder",
Sitelet.Content("/index", new Index(), IndexContent),
Sitelet.Content("/about", new About(), AboutContent)
)
// Accepted Request: GET /folder/index
// Parsed Endpoint: Index
// Returned Content: (value of IndexContent : Content<EndPoint>)
//
// Accepted Request: GET /folder/about
// Parsed Endpoint: About
// Returned Content: (value of AboutContent : Content<EndPoint>)
.Protect
creates protected content, i.e. content only available for authenticated users:
Sitelet.Content("/about", new About(), AboutContent)
.Protect(userName => VerifyUser(userName), LoginRedirect)
Given a predicate on the user name and a Func<EndPoint, EndPoint>
, Protect
returns a new sitelet that requires a logged in user that passes the givem predicate. If the user is not logged in, or the predicate returns false, the request is redirected to the action specified by the LoginRedirect
function. See here how to log users in and out.
.Map
converts a Sitelet to a different endpoint type using mapping functions in both directions.
[EndPoint("/article/{Title}")]
public class Article {
public string Title;
}
Sitelet.Infer<string>(ArticleContent).Map(t => new Article() { Title = t }, a => a.Title)
The mapping functions can also be partial, so one or both of them can return null
on some inputs. The only important thing is that the two functions are the inverse of each other on valid values, so decode(encode(x)) = x
for all values of x
. Also, null
should never be a valid endpoint value for this to work.
[EndPoint("/article/{Title}")]
public class Article : Home {
public string Title;
}
Sitelet.Infer<string>(ArticleContent).Map(t => new Article() { Title = t }, Home p => p is Article ? (a as Article).Title : null)
Content
Content describes the response to send back to the client: HTTP status, headers and body. Content is always worked with asynchronously: all the constructors and combinators described below take and return values of type Task<Content>
.
Creating Content
There are several functions that create different types of content, including ordinary text (Content.Text
), file (Content.File
), HTML page (Content.Page
), JSON (Content.Json
), any custom content (Content.Custom
), and HTTP error codes and redirects.
Content.Text
The simplest response is plain text content, created by passing a string to Content.Text
.
new SiteletBuilder()
.With<T>((ctx, endpoint) =>
Content.Text("This is the response body.")
)
Content.File
You can serve files using Content.File
. Optionally, you can set the content type returned for the file response and whether file access is allowed outside of the web root:
new SiteletBuilder()
.With<T>((ctx, endpoint) =>
Content.File("../Main.fs",
AllowOutsideRootFolder: true,
ContentType: "text/plain")
)
Content.Page
You can return full HTML pages, with managed dependencies using Content.Page
. Here is a simple example:
using static WebSharper.UI.CSharp.Html;
new SiteletBuilder()
.With<T>((ctx, endpoint) =>
Content.Page(
Title: "Welcome!",
Head: link(attr.href("/css/style.css"), attr.rel("stylesheet")),
Body: doc(
h1("Welcome to my site."),
p("It's great, isn't it?")
)
)
)
The optional named arguments Title
, Head
, Body
and Doctype
set the corresponding elements of the HTML page. To learn how to create HTML elements for Head
and Body
, see the HTML combinators documentation.
Content.Json
If you are creating a web API, then Sitelets can automatically generate JSON content for you based on the type of your data. Simply pass your value to Content.Json
, and WebSharper will serialize it. The format is the same as when parsing requests. See here for more information about the JSON format.
[EndPoint("/article/{id}")]
class GetArticle
{
int id;
}
[Serializable]
class GetArticleResponse
{
int id;
string slug;
string title;
}
new SiteletBuilder()
.With<GetArticle>((ctx, endpoint) =>
Content.Json(
new GetArticleResponse {
id = endpoint.id,
slug = "some-blog-article",
title = "Some blog article!"
}
)
)
.Install()
// Accepted Request: GET /article/1423
// Parsed Endpoint: new GetArticle { id = 1423 }
// Returned Content: {"id": 1423, "slug": "some-blog-article", "title": "Some blog article!"}
Content.Custom
Content.Custom
can be used to output any type of content. It takes three optional named arguments that corresponds to the aforementioned elements of the response:
Status
is the HTTP status code. It can be created using the functionHttp.Status.Custom
, or you can use one of the predefined statuses such asHttp.Status.Forbidden
.Headers
is the HTTP headers. You can create them using the functionHttp.Header.Custom
.WriteBody
writes the response body.
new SiteletBuilder()
.With("/someTextFile.txt", ctx =>
Content.Custom(
Status: Http.Status.Ok,
Headers: new[] { Http.Header.Custom("Content-Type", "text/plain") },
WriteBody: stream =>
{
using (var w = new System.IO.StreamWriter(stream))
{
w.Write("The contents of the text file.");
}
}
)
)
// Accepted Request: GET /someTextFile.txt
// Returned Content: The contents of the text file.
Helpers
In addition to the four standard Content families above, the Content
module contains a few helper functions.
- Redirection:
static class Content {
/// Permanently redirect to an endpoint. (HTTP status code 301)
static Task<Content> RedirectPermanent(object endpoint);
/// Permanently redirect to a URL. (HTTP status code 301)
static Task<Content> RedirectPermanentToUrl(string url);
/// Temporarily redirect to an endpoint. (HTTP status code 307)
static Task<Content> RedirectTemporary(object endpoint);
/// Temporarily redirect to a URL. (HTTP status code 307)
static Task<Content> RedirectTemporaryToUrl(string url);
}
- Response mapping: if you want to return HTML or JSON content, but further customize the HTTP response, then you can use one of the following:
static class Content {
/// Set the HTTP status of a response.
static Task<Content> SetStatus(this Task<Content> content, Http.Status status);
/// Add headers to a response.
static Task<Content> WithHeaders(this Task<Content> content, IEnumerable<Header> headers);
/// Replace the headers of a response.
static Task<Content> SetHeaders(this Task<Content> content, IEnumerable<Header> headers);
}
// Example use
new SiteletBuilder()
.With("/", ctx =>
Content.Page(
Title: "No entrance!",
Body: text("Oops! You're not supposed to be here."))
.SetStatus(Http.Status.Forbidden)
.WithHeaders(new[] { Http.Header.Custom("Content-Language", "en") })
)
Using the Context
The method SiteletBuilder.With()
provides a context of type Context
. This context can be used for several purposes; the most important are creating internal links and managing user sessions.
Creating links
Since every accepted URL is uniquely mapped to an action value, it is also possible to generate internal links from an action value. For this, you can use the function context.Link
.
[EndPoint("/article/{id}/{slug}")]
class Article
{
public int id;
public string slug;
}
new SiteletBuilder()
.With<Article>((context, endpoint) =>
Content.Page(
Title: "Welcome!",
Body: doc(
h1("Index page"),
a(attr.href(context.Link(new Article { id = 1423, slug = "some-article-slug" })),
"Go to some article"),
br(),
a(attr.href(context.ResolveUrl("~/Page2.html")), "Go to page 2")
)
)
)
Note how context.Link
is used in order to resolve the URL to the Article
endpoint. Endpoint URLs are always constructed relative to the application root, whether the application is deployed as a standalone website or in a virtual folder. context.ResolveUrl
helps to manually construct application-relative URLs to resources that do not map to sitelet endpoints.
Managing User Sessions
Context<'T>
can be used to access the currently logged in user. The member UserSession
has the following extension members, which require using WebSharper.Web;
:
Task LoginUserAsync(string username, bool persistent = false)
Task LoginUserAsync(string username, System.TimeSpan duration)
Logs in the user with the given username. This sets a cookie that is uniquely associated with this username. The second parameter determines the expiration of the login:
LoginUserAsync("username")
creates a cookie that expires with the user's browser session.LoginUserAsync("username", persistent: true)
creates a cookie that lasts indefinitely.LoginUserAsync("username", duration: d)
creates a cookie that expires after the given duration.
Example:
public async Task<Content<EndPoint>> LoggedInPage(Context<EndPoint> context, string username) { // We're assuming here that the login is successful, // eg you have verified a password against a database. await context.UserSession.LoginUserAsync(username, duration: TimeSpan.FromDays(30.)); return Content.Page( Title: "Welcome!", Body: text($"Welcome, {username}!") ); }
Task<string> GetLoggedInUserAsync()
Retrieves the currently logged in user's username, or
null
if the user is not logged in.Example:
public async Task<Content<EndPoint>> HomePage(Context<EndPoint> context) { var username = await context.UserSession.GetLoggedInUserAsync(); return Content.Page( Title: "Welcome!", Body: text ( username is null ? "Welcome, stranger!" : $"Welcome back, {username}!" ) ); }
Task LogoutAsync()
Logs the user out.
Example:
public async Content<EndPoint> Logout(Context<EndPoint> context) { await context.UserSession.LogoutAsync(); return Content.RedirectTemporary(new Home()); }
The implementation of these functions relies on cookies and thus requires that the browser has enabled cookies.
Other Context members
WebSharper.Sitelets.Context
inherits from WebSharper.Web.Context
, and a number of properties and methods from it are useful. See the documentation for WebSharper.Web.Context
.
Routers
The router component of a sitelet can be constructed in multiple ways. The main options are:
- Declaratively, using
InferRouter.Router.Infer
which is also used internally bySitelets.Infer
. The main advantage of creating a router value separately, is that it can be also be added a[JavaScript]
attribute, so that the client can generate links from endpoint values too.WebSharper.UI
also contains functionality for client-side routing, making it possible to handle all or a subset of internal links without browser navigation. So sharing the router abstraction between client and server means that server can generate links that the client will handle and vice versa. - Manually, by using combinators to build up larger routers from elementary
Router
values or inferred ones. You can use this to further customize routing logic if you want an URL schema that is not fitting default inferred URL shapes, or add additional URLs to handle (e. g. for keeping compatibility with old links). - Implementing the
IRouter
interface. This is the most universal way, but has less options for composition.
The following example shows how you can create a router of type WebSharper.Sitelets.IRouter<EndPoint>
by writing the two mappings manually:
using WebSharper.Sitelets;
public enum EndPoint { Page1, Page2 }
public class MyRouter : IRouter<EndPoint>
{
public EndPoint Route(Http.Request req)
{
switch (req.Uri.LocalPath)
{
case "/page1": return EndPoint.Page1;
case "/page2": return EndPoint.Page2;
default: return null;
}
}
public Uri Link(EndPoint endpoint)
{
switch (endpoint)
{
case EndPoint.Page1: return new Uri("/page1", System.UriKind.Relative);
case EndPoint.Page2: return new Uri("/page2", System.UriKind.Relative);
default: return null;
}
}
}
Specifying routers manually gives you full control of how to parse incoming requests and to map endpoints to corresponding URLs. It is your responsibility to make sure that the router forms a bijection of URLs and endpoints, so that linking to an endpoint produces a URL that is in turn routed back to the same endpoint.
Constructing routers manually is only required for very special cases. The above router can for example be generated using Router.Table
:
var MyRouter : Router<EndPoint> =
Router.Table(
Tuple.Create(EndPoint.Page1, "/page1"),
Tuple.Create(EndPoint.Page2, "/page2")
)
Even simpler, if you want to create the same URL shapes that would be generated by Sitelet.Infer
, you can simply use InferRouter.Router.Infer()
:
var MyRouter : Router<EndPoint> =
InferRouter.Router.Infer ()
Router primitives
The WebSharper.Sitelets.RouterOperators
module exposes the following basic Router
values and construct functions: (following examples are assuming that you have using static WebSharper.Sitelets.RouterOperators;
)
rRoot
: Recognizes and writes an empty path.r "path"
: Recognizes and writes a specific subpath. You can also writer "path/subpath"
to parse two or more segments of the URL.rString
,rChar
: Recognizes a URIComponent as a string or char and writes it as a URIComponent.rTryParse<T>
: Creates a router for any type that defines aTryParse
static method.rInt
,rDouble
, ...: Creates a router for numeric values.rBool
,rGuid
: Additional primitive types to parse from or write to the URL.rDateTime
: Parse or write aDateTime
, takes a format string.
Router combinators
Router.Combine
: Parses or writes using two routers one after the other. For exampleRouter.Combine(rString, rInt)
will have typeRouter<Tuple<string, int>>
./
: Same as above, but when one side is a non-genericRouter
or a string which adds a constant URL fragment. For exampler("article") / r("id") / rInt
can be shortened to"article/id" / rInt
.+
(aliasRouter.Add
): Parses or writes using the first router if successful, otherwise the second.Router.Sum
: Optimized version of combining a sequence of routers with+
. Parses or writes with the first router in the sequence that can handle the path or value..Map
: A bijection (or just surjection) between representations handled by routers. For example if you have a class namedPerson
with fieldsstring Name
andint Age
, then you can define a router for it by mapping from aRouter<Tuple<string, int>>
like so
See thatvar rPerson = Router.Combine(rString, rInt) .Map( (n, a) => new Person { Name = n, Age = a }, p => (p.Name, p.Age).ToTuple() );
Map
needs two function arguments, to convert data back and forth between representations. All values of the resulting type must be mapped back to underlying type by the second function in a way compatible with the first function to work correctly..MapTo
: Maps a non-genericRouter
to a single valuedRouter<T>
. For example ifHome
is a base class for your endpoint type hierarchy with a singleton instance, you can create a router for it by:
This only needs a single value as argument, but the type used must be comparable, so the writer part of the newly createdvar rHome = rRoot.MapTo(Home.Instance);
Router<T>
can decide if it is indeed theHome.Instance
value that it needs to write by the underlying router (in our case producing a root URL)..Filter
: restricts a router to parse/write values only that are passing a check. Usage:rInt.Filter(x => x >= 0)
, which won't parse and write negative values..Query
: Modifies a router to parse from and write to a specific query argument instead of main URL segments. Usage:rInt.Query("x")
, which will read/write query segments like?x=42
. You should pass only a router that is always reading/writing a single segment, which inclide primitive routers,Router.Nullable
, andSum
s andMap
s of these..QueryNullable
: Modifies a router to read an optional query value as aSystem.Nullable
. Creates aRouter<Nullable<T>
, same restrictions apply as toQuery
..Box
: Converts aRouter<T>
to aRouter<object>
. When writing, it uses a type check to see if the object is of typeT
so it can be passed to underlying router..Unbox
: Converts aRouter<object>
to aRouter<T>
. When parsing, it uses a type check to see if the object is of typeT
so that the parsed value can be represented inT
..Array
: Creates an array parser/writer. The URL will contain the length and then the items, so for examplerString.Array()
can handle2/x/y
..Nullable
: Creates aNullable
value parser/writer. Writes or readsnull
for null or a value that is handled by the input router. ForInferRouter.Router.Infer
: Creates a router based on type shape. The attributes recognized are the same asSitelet.Infer
described in the Sitelets documentation.Router.Table
: Creates a router mapping from any number ofTuple<Endpoint, string>
arguments, connecting the given endpoint values and paths.Router.Method
: Creates a router that only parses request with the inner router, it the HTTP method methes the given method argument. By default, routers ignore the method.Router.Body
: Creates a router that parses and serializes any value to and from the request body with custom functions. If the will be used on server-side only to parse requests and generate links, the serialize function can return just a null or empty string. For exampleRouter.Body(x => x, x => x)
just gets the request body as a string.Router.Json
creates a router that parses the request body by the JSON format derived from the type argument.Router.FormData
creates a router from an underlying router handling query arguments that parses query arguments from the request body of a form post instead of the URL.Router.Delay
can be used to construct routers for recursive data types. Takes anFunc<Router<'T>>
function, and evaluates it firsthe t time the router is used for parsing and writing (never just when combining them).
Using the router
Router.Link
creates a (relative) link using a router. A useful helper to have in the file defining your router is:public Doc MakeLink(EndPoint page, string content) => a(attr.href(router.Link(page)), content);
This works the same on both server and client-side to create basic <a>
links to pages of your web application.
Sitelet.New
creates a Sitelet from a router and handler. Example:
[Website]
static Sitelet<object> Main =>
Sitelet.New(rPages, (ctx, ep) => {
switch (ep)
{
case Home h: return div("This is the home page");
case Contact c: return client (() => ContactMain());
default: return null;
}
}
Here we return a static page for the root, but call into a client-side generated content in the Contact
pages, which is parsing the URL again to show the contact details from the URL.
Sitelets are only a server-side type.
Router.Ajax
makes a request from an endpoint value on the client and executes it usingjQuery.ajax
. Returns aTask<string>
, which raises an exception internally if the request fails. Example:
// [<EndPoint "/get-data/{i}">] public class GetData { public int i; }
public async Task<string> GetDataAsyncSafe(int i) {
try
return await router.Ajax(new GetData { i = i });
else
return null;
}