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-sluginto 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.Texttakes aFunc<Context, string>function and creates a Sitelet that serves the result string as a text response.Application.SinglePagetakes 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.Tuples,ValueTuples 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.DateTimeis serialized with the formatyyyy-MM-dd-HH.mm.ssby 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 anNullableof 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.Inferis used bySiteletBuilder.Withinternally. UsingSitelet.Infer<EndPoint>(CreateContent)is equal tonew SiteletBuilder().Sitelet.Sumtakes 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.Contents.The following sitelet accepts
/indexand/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 + s2is 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.
.Shifttakes 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.Foldertakes a sequence of Sitelets and shifts them by a path segment. It is effectively a combination ofSumandShift.
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>)
.Protectcreates 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.
.Mapconverts 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:
Statusis 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.Headersis the HTTP headers. You can create them using the functionHttp.Header.Custom.WriteBodywrites 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
nullif 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.Inferwhich 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.UIalso 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
Routervalues 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
IRouterinterface. 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 aTryParsestatic 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-genericRouteror a string which adds a constant URL fragment. For exampler("article") / r("id") / rIntcan 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 namedPersonwith fieldsstring Nameandint 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() );Mapneeds 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-genericRouterto a single valuedRouter<T>. For example ifHomeis 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.Instancevalue 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, andSums andMaps 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 typeTso 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 typeTso 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 aNullablevalue parser/writer. Writes or readsnullfor 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.Inferdescribed 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.Jsoncreates a router that parses the request body by the JSON format derived from the type argument.Router.FormDatacreates 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.Delaycan 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.Linkcreates 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.Newcreates 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.Ajaxmakes 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;
}