Script Pages

One of the most popular use-cases for a high-performance and versatile scripting language like #Script is as a server-side HTML #Script Pages for .NET Web Applications where it can provide a simpler, cleaner and portable alternative than Razor and Razor Pages in ASP.NET and ASP.NET Core Web Apps.

#Script Pages in ServiceStack

The SharpPagesFeature plugin provides a first-class experience for generating dynamic websites where it's able to generate complete server-generated websites (like this one) without requiring any additional Controllers or Services.

To enable #Script Pages in ServiceStack you just need to register the SharpPagesFeature plugin:

public void Configure(Container container)
{
    Plugins.Add(new SharpPagesFeature());
}

SharpPagesFeature is a subclass of ScriptContext which defines the context on which all ServiceStack #Script Pages are executed within. It provides deep integration within ServiceStack by replacing the ScriptContext's stand-alone dependencies with ServiceStack AppHost providers, where it:

If preferred, you can change which .html extension gets handled by #Script Pages with:

Plugins.Add(new SharpPagesFeature { HtmlExtension = "htm" });

Register additional functionality

The same APIs for extending a ScriptContext is also how to extend SharpPagesFeature to enable additional functionality in your #Script Pages:

Plugins.Add(new SharpPagesFeature {
    Args = { ... },              // Global Arguments available to all Scripts, Pages, Partials, etc
    ScriptMethods = { ... },     // Additional Methods
    ScriptBlocks = { ... },      // Additional Script Blocks 
    FilterTransformers = { .. }, // Additional Stream Transformers
    PageFormats = { ... },       // Additional Text Document Formats
    Plugins = { ... },           // Encapsulated Features e.g. Markdown, Protected or ServiceStack Features

    ScanTypes = { ... },         // Auto register Methods, Blocks and Code Page Types
    ScanAssemblies = { ... },    // Auto register all Methods, Blocks and Code Page Types in Assembly
});

Runs Everywhere

The beauty of #Script working natively with ServiceStack is that it runs everywhere ServiceStack does which is in all major .NET Server Platforms. That is, your same #Script-based Web Application is able to use the same #Script implementation, "flavour" and feature-set and is portable across whichever platform you choose to host it on:

Once registered, SharpPagesFeature gives all your .html pages scripting super powers where sections can be compartmentalized and any duplicated content can now be extracted into reusable partials, metadata can be added to the top of each page and its page navigation dynamically generated, contents of files and urls can be embedded directly and otherwise static pages can come alive with access to Default Scripts.

The Starter Projects below provide a quick way to get started with a pre-configured ServiceStack App with #Script Pages:

.NET Core #Script Pages Project

Create a new #Script Pages Website .NET Core 3.1 App with x new:

$ dotnet tool install --global x 

$ x new script ProjectName
.NET Core Starter Template

ASP.NET Core #Script Pages Project on .NET Framework

To create ASP.NET Core Project on the .NET Framework:

$ x new script-corefx ProjectName

ASP.NET v4.5 #Script Pages Project

For ASP.NET v4.5+ projects create a new ServiceStack ASP.NET #Script Pages with Bootstrap from the VS.NET Templates in ServiceStackVS VS.NET Extension to create an ASP.NET v4.5 Project using ServiceStack's recommended project structure:

ASP.NET v4.5 Starter Template

Content Pages

There are a number of different ways you can use #Script to render dynamic pages, requests that calls and renders #Script .html pages directly are called "Content Pages" which don't use any Services or Controllers and can be called using a number of different styles and calling conventions:

Page Based Routing

Any .html page available from your AppHost's configured Virtual File Sources can be called directly, typically this would mean the File System which in a .NET Core Web App starts from the WebRootPath (e.g /wwwroot) so a request to /docs/sharp-pages goes through all configured VirtualFileSources to find the first match, which for this website is the file /src/wwwroot/docs/sharp-pages.html.

Pretty URLs by default

Essentially #Script Pages embraces conventional page-based routing which enables pretty urls inferred from the pages file and directory names where each page can be requested with or without its .html extension:

path page
/db  
/db.html /db.html
/posts/new  
/posts/new.html /posts/new.html

The default route / maps to the index.html in the directory if it exists, e.g:

path page
/ /index.html
/index.html /index.html

Dynamic Page Routes

In addition to these static conventions, #Script Pages now supports Nuxt-like Dynamic Routes where any file or directory names prefixed with an _underscore enables a wildcard path which assigns the matching path component to the arguments name:

path page arguments
/ServiceStack /_user/index.html user=ServiceStack
/posts/markdown-example /posts/_slug/index.html slug=markdown-example
/posts/markdown-example/edit /posts/_slug/edit.html slug=markdown-example

Layout and partial recommended naming conventions

The _underscore prefix for declaring wildcard pages is also what is used to declare "hidden" pages, to distinguish them from hidden partials and layouts, the recommendation is for them to include layout and partial their name, e,g:

Pages with layout or partial in their name remain hidden and are ignored in wildcard path resolution.

If following the recommended _{name}-partial.html naming convention, you'll be able to reference them using just their name:

{{ 'menu' |> partial }}          // Equivalent to:
{{ '_menu-partial' |> partial }}

View Pages

"View Pages" are pages that are rendered after a Service is executed, where it's typically used to provide the "HTML UI" for the Service's Response DTO where it's populated in the Model page argument as done in Razor ViewPages.

View Pages can be in any nested folder within the /Views folder but all page names within the /Views folder need to be unique. The name should use the format using the format {PageName}.html where PageName can be either:

There are a number of other ways to specify which View you want to render:

Specify the Services DefaultView

You can specify which view all Services should use to render their responses by using the [DefaultView] Request Filter Attribute:

[DefaultView("CustomPage")]
public class MyServices
{
    public object Any(CreateContact request) => ...;

    public object Any(UpdateContact request) => ...;
}

Specify View with custom HttpResult

Just like ServiceStack.Razor, you can specify to use different Views or Layouts by returning a decorated response in custom HttpResult with the View or Template you want the Service rendered in , e.g:

public object Any(MyRequest request)
{
    ...
    return new HttpResult(response)
    {
        View = "CustomPage",
        Template = "_custom-layout",
    };
}

Return Custom PageResult

For maximum flexibility to control how views are rendered you can return a custom PageResult using Request.GetPage() or Request.GetCodePage() extension methods as seen in Model View Controller:

public class CustomerServices : Service
{
    public object Any(ViewCustomer request) =>
        new PageResult(Request.GetPage("examples/customer")) {
            Model = TemplateQueryData.GetCustomer(request.Id)
        };
}

Allow Views to be specified on the QueryString

You can use the [ClientCanSwapTemplates] Request Filter attribute to let the View and Template by specified on the QueryString, e.g: ?View=CustomPage&Template=_custom-layout

[ClientCanSwapTemplates]
public class MyServices
{
    public object Any(CreateContact request) => ...;

    public object Any(UpdateContact request) => ...;
}

Additional examples of dynamically specifying the View and Template are available in SharpViewsTests.cs.

Cascading Layouts

One difference from Razor is that it uses a cascading _layout.html instead of /Views/Shared/_Layout.cshtml.

So if your view page was in:

/Views/dir/MyRequest.html

It will use the closest `_layout.html` it can find starting from:

/Views/dir/_layout.html
/Views/_layout.html
/_layout.html

Layout Selection

Unless it's a complete HTML Page (e.g. starts with html or HTML5 tag) the page gets rendered using the closest _layout.html page it can find starting from the directory where the page is located, traversing all the way up until it reaches the root directory. Which for this page uses the /src/wwwroot/_layout.html template in the WebRoot directory, which as it's in the root directory, is the fallback Layout for all .html pages.

Pages can change the layout they use by either adding their own _layout.html page in their sub directory or specifying a different layout in their pages metadata header, e.g:

<!--
layout: mobile-layout
-->

Where it instead embed the page using the closest mobile-layout.html it can find, starting from the Page's directory. If your pages are instead embedded in the different folder you can request it directly from the root dir:

<!--
layout: templates/mobile-layout
-->

Request Variables

The QueryString and FORM variables sent to the page are available as arguments within the page using the form and query (or its shorter qs alias) collections, so a request like /docs/sharp-pages?id=1 can access the id param with {{ qs.id }}. The combined {{ 'id' | formQuery }} method enables the popular use-case of checking for the param in POST FormData before falling back to the QueryString. Use {{ 'ids' | formQueryValues }} for accessing multiple values sent by multiple checkboxes or multiple select inputs. The {{ 'id' | httpParam }} method searches all Request params including HTTP Headers, QueryString, FormData, Cookies and Request.Items.

To help with generating navigation, the following Request Variables are also available:

You can use {{ PathInfo }} to easily highlight the active link in a links menu as done in sidebar.html:

<nav id="sidebar">
<div class="inner">

<ul>
    <li>
        <h5>docs</h5>
        <ul>
            {{#each docLinks}}
            <li {{ {active: matchesPathInfo(Key)} |> htmlClass }}><a href="{{Key}}">{{Value}}</a></li>
            {{/each}}
        </ul>
    </li>

    <li>
        <h5>use cases</h5>
        <ul>
            {{#each useCaseLinks}}
            <li {{ {active: matchesPathInfo(Key)} |> htmlClass }}><a href="{{Key}}">{{Value}}</a></li>
            {{/each}}
        </ul>
    </li>

    <li>
        <h5>linq examples</h5>
        <ul>
            {{#each linqLinks}}
            <li {{ {active: matchesPathInfo(Key)} |> htmlClass }}><a href="{{Key}}">{{Value}}</a></li>
            {{/each}}
        </ul>
    </li>
</ul>

</div>
</nav>

Init Pages

Just as how Global.asax.cs can be used to run Startup initialization logic in ASP.NET Web Applications and Startup.cs in .NET Core Apps, you can now add a /_init.html page for any script logic that's only executed once on Startup.

This is used in the Blog Web App's _init.html where it will create a new blog.sqlite database if it doesn't exist seeded with the UserInfo and Posts Tables and initial data, e.g:

{{  `CREATE TABLE IF NOT EXISTS "UserInfo" 
    (
        "UserName" VARCHAR(8000) PRIMARY KEY, 
        "DisplayName" VARCHAR(8000) NULL, 
        "AvatarUrl" VARCHAR(8000) NULL, 
        "AvatarUrlLarge" VARCHAR(8000) NULL, 
        "Created" VARCHAR(8000) NOT NULL,
        "Modified" VARCHAR(8000) NOT NULL
    );`    
    |> dbExec
}}

{{ dbScalar(`SELECT COUNT(*) FROM Post`) |> to => postsCount }}

{{#if postsCount == 0 }}

    ========================================
    Seed with initial UserInfo and Post data
    ========================================

    ...

{{/if}

{{ htmlError }}

If there was an Exception with any of the SQL Statements it will be displayed in the {{ htmlError }} filter which can be later viewed in the /log page above.

The output of the _init page is captured in the initout argument which can be later inspected as a normal template argument as seen in log.html:

<div>
    Output from init.html:
    <pre>{{initout |> raw}}</pre>
</div>

A good idea to view Script Init & Page Errors is to include both initError and htmlError in your _layout.html as done in /SharpData/wwwroot/_layout.html:

{{initError |> htmlError}}
{{htmlError}}

init.ss

Instead of init.html you can also use _init.ss which is used in Apps like SharpData to construct a ServiceStack App's SVG stylesheet bundle from a user-defined list of embedded *.svg resources and inline SVG declarations, e.g:

{{ 
    var AppSvgs = {
        'action/home.svg':                    'home',
        'device/storage.svg':                 'db',
        'action/list.svg':                    'table',
        'navigation/first_page.svg':          'chevron-first',
        'navigation/last_page.svg':           'chevron-last',
        'navigation/expand_more.svg':         'chevron-down',
        'navigation/chevron_left.svg':        'chevron-left',
        'navigation/chevron_right.svg':       'chevron-right',
        'navigation/expand_less.svg':         'chevron-up',
        'content/clear.svg':                  'clear',
        'content/filter_list.svg':            'filter',
    } 
}}

{{#each AppSvgs}}
    {{`/lib/svg/material/${it.Key}` |> svgAddFile(it.Value,'app')}}
{{/each}}

{{#svg fields app}}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="black" width="48px" height="48px">
    <path d="M0 0h24v24H0V0z" fill="none"/>
    <path d="M4 5v13h17V5H4zm10 2v9h-3V7h3zM6 7h3v9H6V7zm13 9h-3V7h3v9z" fill="#ffffff"/>
</svg>
{{/svg}}

Ignoring Pages

You can ignore #Script from evaluating static .html files with the following page arguments:

<!--
ignore: page
-->

Nothing in this page will be evaluated but will still be rendered inside the closest Layout.
<!--
ignore: template
-->

This page will not be evaluated nor will it have a Layout.
<!--
layout: none
-->

This page will be evaluated but rendered without a layout.
Complete .html pages starting with <!DOCTYPE HTML> or <html have their layouts ignored by default.

Server UI Controls

The #Script Pages UI Controls are utilized in new #Script Pages project templates and the World Validation Application.

UI Component List

Currently the component libraries include common Bootstrap UI Form Controls and Navigation Components:

Control Description
validationSummary Show validation summary error message unless there's an error in specified fields
validationSuccess Display a "Success Alert Box"
formInput Display a <input type="text"/> UI Control
formTextarea Display a <textarea/> UI Control
formSelect Display a <select/> UI Control
formInput({type:'checkbox'}) Display a <input type="checkbox"/> UI Control
htmlHiddenInputs Emit HTML <input type="hidden"/> field for each specified Key/Value pair entry
svgImage Return <svg/> markup for the named image
nav Display a list of NavItem's
navbar Display the navbar main menu
navLink Display a nav-link nav-item
navButtonGroup Display a list of NavItem's btn-group

Bootstrap UI Form Controls

The Bootstrap UI form controls include built-in support for validation where they can render validation errors from ServiceStack's ResponseStatus object, e.g the Login Page in World Validation:

<form action="/auth/credentials" method="post" class="col-lg-4">
    <div class="form-group">
        {{ ['userName','password'] |> validationSummary({class:'alert alert-warning'}) }}
        {{ { continue: qs.continue ?? '/server/', errorView:'/server/login' } |> htmlHiddenInputs }}
    </div>
    <div class="form-group">
        {{ {id:'userName'} 
           |> formInput({label:'Email',help:'Email you signed up with',size:'lg'}) }}
    </div>
    <div class="form-group">
        {{ {id:'password',type:'password'} 
           |> formInput({label:'Password',help:'6 characters or more',size:'lg',preserveValue:false}) }}
    </div>
    <div class="form-group">
        {{ {id:'rememberMe',type:'checkbox',checked:true} |> formInput({label:'Remember Me'}) }}
    </div>
    <div class="form-group">
        <button type="submit" class="btn btn-lg btn-primary">Login</button>
    </div>
    <div class="form-group">
        <a class="lnk" href="/server/register">Register New User</a>
    </div>
</form>

Login Page UI

The Login Page contains a standard Bootstrap Username/Password form with labels, placeholders and help text, which initially looks like:

What it looks like after submitting an empty form with Server Exception Errors rendered against their respective fields:

Form Control Properties

The #Script properties uses camelCase names and JS Object literals. The first (aka target) argument is for attributes you want to add to the HTML <input/> Element whilst the 2nd Argument is used to customize any of its other high-level features, e.g:

{{ htmlAttrs |> controlName(inputOptions) }}

The inputOptions available map to camelCase properties on the typed InputOptions class:

/// High-level Input options for rendering HTML Input controls
public class InputOptions
{
    /// Display the Control inline 
    public bool Inline { get; set; }
    
    /// Label for the control
    public string Label { get; set; }
    
    /// Class for Label
    public string LabelClass { get; set; }
    
    /// Override the class on the error message (default: invalid-feedback)
    public string ErrorClass { get; set; }

    /// Small Help Text displayed with the control
    public string Help { get; set; }
    
    /// Bootstrap Size of the Control: sm, lg
    public string Size { get; set; }
    
    /// Multiple Value Data Source for Checkboxes, Radio boxes and Select Controls 
    public object Values { get; set; }

    /// Typed setter of Multi Input Values
    public IEnumerable<KeyValuePair<string, string>> InputValues
    {
        set => Values = value;
    }

    /// Whether to preserve value state after post back
    public bool PreserveValue { get; set; } = true;

    /// Whether to show Error Message associated with this control
    public bool ShowErrors { get; set; } = true;
}

Contacts Page

The Contacts Page shows a more complete example with a number of different UI Controls.

<form action="/contacts" method="post" class="col-lg-4">
    <div class="form-group">
        {{ 'title,name,color,age,filmGenres,agree' |> validationSummary }}
    </div>
    <div class="form-group">
        {{ {id:'title',type:'radio'} |> formInput({values:contactTitles,inline:true}) }}
    </div>
    <div class="form-group">
        {{ {id:'name',placeholder:'Name'} |> formInput({label:'Full Name',help:'Your first and last name'}) }}
    </div>
    <div class="form-group">
        {{ {id:'color',class:'col-4'}
           |> formSelect({label:'Favorite color',values:{'', ...contactColors}}) }}
    </div>
    <div class="form-group">
        {{ {id:'filmGenres',type:'checkbox'} 
          |> formInput({label:'Favorite Film Genres',values:contactGenres,help:"choose one or more"}) }}
    </div>
    <div class="form-group">
        {{ {id:'age',type:'number',min:13,placeholder:'Age',class:'col-3'} |> formInput }}
    </div>
    <div class="form-group">
        {{ {id:'agree',type:'checkbox'} |> formInput({label:'Agree to terms and conditions'}) }}
    </div>
    <div class="form-group">
        <button class="btn btn-primary" type="submit">Add Contact</button>
        <a href="/server/contacts/">reset</a>
    </div>
</form>

These Server UI Controls provide auto Validation Form Binding for any validation rules specified on the CreateContact Validator:

public class CreateContactValidator : AbstractValidator<CreateContact>
{
    public CreateContactValidator()
    {
        RuleFor(r => r.Title).NotEqual(Title.Unspecified).WithMessage("Please choose a title");
        RuleFor(r => r.Name).NotEmpty();
        RuleFor(r => r.Color).Must(x => x.IsValidColor()).WithMessage("Must be a valid color");
        RuleFor(r => r.FilmGenres).NotEmpty().WithMessage("Please select at least 1 genre");
        RuleFor(r => r.Age).GreaterThan(13).WithMessage("Contacts must be older than 13");
        RuleFor(x => x.Agree).Equal(true).WithMessage("You must agree before submitting");
    }
}

As well as any ArgumentException thrown within the Service Implementation:

public object Any(CreateContact request) 
{
    var newContact = request.ConvertTo<Data.Contact>();
    newContact.Id = Interlocked.Increment(ref Counter);
    newContact.UserAuthId = this.GetUserId();
    newContact.CreatedDate = newContact.ModifiedDate = DateTime.UtcNow;

    var contacts = Contacts.Values.ToList();
    var alreadyExists = contacts.Any(x => x.UserAuthId == newContact.UserAuthId && x.Name == request.Name);
    if (alreadyExists)
        throw new ArgumentException($"You already have contact named '{request.Name}'",nameof(request.Name));
    
    Contacts[newContact.Id] = newContact;
    return new CreateContactResponse { Result = newContact.ConvertTo<Contact>() };
}

Contacts Page UI

The Contacts Page is representative of a more complex page that utilizes a variety of different form controls where the same page is also responsible for rendering the list of existing contacts:

Here's an example of what a partially submitted invalid form looks like:

To view the complete implementation in context checkout World Validation Server Implementation.

Navigation Controls

The Server Navigation Controls are used to render your Apps Unified Navigation where you can use the navbar and navButtonGroup methods to render NavItems:

Navbar

You can render the main menu navigation using the navbar script method:

{{ navbar }}

Which by default renders the View.NavItems main navigation, using the default NavOptions and User Attributes (if authenticated):

You can also render a different Navigation List with:

{{ navItems('submenu').navbar() }}

Which can be customized using the different NavOptions properties above, in camelCase:

{{ navItems('submenu').navbar({ navClass: 'navbar-nav navbar-light bg-light' }) }}

Button group

The navButtonGroup script can render NavItems in a button group, e.g. the OAuth buttons are rendered with:

{{ 'auth'.navItems().navButtonGroup({ navClass: '', navItemClass: 'btn btn-block btn-lg' }) }}

Which renders a vertical, spaced list of buttons which look like:

NavOptions Properties

Each Nav UI Control can be further customized by overriding the navOptions that map to camelCase properties on the typed NavOptions class:

public class NavOptions
{
    /// User Attributes for conditional rendering, e.g:
    ///  - auth - User is Authenticated
    ///  - role:name - User Role
    ///  - perm:name - User Permission 
    public HashSet<string> Attributes { get; set; }
    
    /// Path Info that should set as active 
    public string ActivePath { get; set; }
    
    /// Prefix to include before NavItem.Path (if any)
    public string BaseHref { get; set; }

    // Custom classes applied to different navigation elements (defaults to Bootstrap classes)
    public string NavClass { get; set; }
    public string NavItemClass { get; set; }
    public string NavLinkClass { get; set; }
    
    public string ChildNavItemClass { get; set; }
    public string ChildNavLinkClass { get; set; }
    public string ChildNavMenuClass { get; set; }
    public string ChildNavMenuItemClass { get; set; }
}

Admin Service

The new Admin Service lets you run admin actions against a running instance which by default is only accessible to Admin users and can be called with:

/script/admin

Which will display the available actions which are currently only:

Zero downtime deployments

These actions are useful after an xcopy/rsync deployment to enable zero downtime deployments by getting a running instance to invalidate all internal caches and force existing pages to check if it has been modified, the next time their called.

Actions can be invoked in the format with:

/script/admin/{Actions}

Which can be used to call 1 or more actions:

/script/admin/invalidateAllCaches
/script/admin/invalidateAllCaches,RunInitPage

By default it's only available to be called by **Admin** Users (or AuthSecret) but can be changed with:

Plugins.Add(new SharpPagesFeature {
    ScriptAdminRole = RoleNames.AllowAnyUser, // Allow any Authenticated User to call /script/admin
    //ScriptAdminRole = RoleNames.AllowAnon,  // Allow anyone
    //ScriptAdminRole = null,                 // Do not register /script/admin service
});

This also the preferred way to enable the Metadata Debug Inspector in production, which is only available in DebugMode and Admin Role by default:

Plugins.Add(new SharpPagesFeature {
    MetadataDebugAdminRole = RoleNames.Admin,          // Only allow Admin users to call /metadata/debug
    //MetadataDebugAdminRole = RoleNames.AllowAnyUser, // Allow Authenticated Users
    //MetadataDebugAdminRole = RoleNames.AllowAnon,    // Allow anyone
    //MetadataDebugAdminRole = null,                   // Default. Do not register /metadata/debug service
});

ServiceStack Scripts

Methods for integrating with ServiceStack are available in ServiceStack Scripts and Info Scripts both of which are pre-registered when registering the SharpPagesFeature Plugin.

Markdown Support

Markdown is a great way to maintain and embed content of which #Script has great support for, which can be rendered using the markdown filter to render markdown text to HTML:

{{ `## Heading` |> markdown }}

This can be combined with other composable filters like includeFile to easily and efficiently render markdown content maintained in a separate file:

{{ 'doc.md' |> includeFile |> markdown }}

Or you could use includeUrlWithCache to efficiently render markdown from an external URL:

{{ url |> includeUrlWithCache |> markdown }}

A popular way for embedding static markdown content inside a page is to use the markdown script block which this website makes extensive use of:

{{#markdown}}
### Heading
> Static Markdown Example
{{/markdown }}

Providing an easy way for mixing in markdown content inside a dynamic web page. To embed dynamically rendered markdown content you can use the capture script block to capture dynamic markdown that you can render with the markdown filter:

{{#capture md}}
### Dynamic Markdown Example
{{#each i in range(1,5)}}
  - {{i}}
{{/each}}
{{/capture}}
{{ md |> markdown }}

made with by ServiceStack