Sharp Apps

Sharp Apps are a new approach to dramatically simplify .NET Wep App development and provide the most productive development experience possible whilst maximizing reuse and component sharing. They also open up a number of new use-cases for maintaining clean isolation between front-end and back-end development with front-end developers not needing any knowledge of C#/.NET to be able to develop UIs for high-performance .NET Web Apps. Sharp Apps also make it easy to establish and share an approved suite of functionality amongst multiple websites all consuming the same back-end systems and data stores.

Sharp Apps leverages #Script to develop entire content-rich, data-driven websites without needing to write any C#, compile projects or manually refresh pages - resulting in the easiest and fastest way to develop Web Apps in .NET!

Ultimate Simplicity

Not having to write any C# code or perform any app builds dramatically reduces the cognitive overhead and conceptual knowledge required for development where the only thing front-end Web developers need to know is #Script's familiar syntax and what methods are available to call. Because of [#Script's JavaScript compatibility](/docs/expression-viewer), developing a Website with #Script will be instantly familiar to JavaScript devs despite calling and binding directly to .NET APIs behind the scenes.

All complexity with C#, .NET, namespaces, references, .dlls, strong naming, packages, MVC, Razor, build tools, IDE environments, etc has been eliminated leaving all Web Developers needing to do is run the cross-platform web dotnet tool and configure a simple app.settings text file to specify which website folder to use, which ServiceStack features to enable, which db or redis providers to connect to, etc. Not needing to build also greatly simplifies deployments where multiple websites can be deployed with a single rsync or xcopy command or if deploying your App in a Docker Container, you just need to copy your website files, or just the app.settings if you're using an S3 or Azure Virtual File System.

Rapid Development Workflow

The iterative development experience is also unparalleled for a .NET App, no compilation is required so you can just leave the web dotnet tool running whilst you add .html files needed to build your App and thanks to the built-in Hot Reloading support, pages will refresh automatically as you save. You'll just need to do a full page refresh when modifying external .css/.js files to bypass the browser cache and you'll need to restart web to pick up any changes to your app.settings or added any .dlls to your /plugins folder.

Getting Started

All Sharp Apps can be run either as .NET Core Web Apps by installing the web dotnet tool:

$ dotnet tool install -g web
Then you can run web list to see which apps are available to install:
$ web list

By default this will list all sharp-apps available:

1. redis           Redis Admin Viewer developed as Vue Client Single Page App
2. bare            Bootstrap + jQuery multi-page Website with dynamic Menu Navigation + API pages
3. chat            Highly extensible App with custom AppHost leveraging OAuth + SSE for real-time Chat
4. plugins         Extend Sharp Apps with Plugins, Filters, ServiceStack Services and other C# extensions
5. blog            Minimal multi-user Twitter OAuth blogging platform that creates living powerful pages
6. rockwind-aws    Rockwind Cloud Sharp App on AWS
7. rockwind-azure  Rockwind Cloud Sharp App on Azure
8. redis-html      Redis Admin Viewer developed as server-generated HTML Website
9. spirals         Explore and generate different Spirals with SVG
10. rockwind       Sharp App combining multi-layout Rockstars website + data-driven Northwind Browser

Usage: web install <name>

Where any of the apps can be installed by specifying the name, e.g. spirals can be installed with:

$ web install spirals

Then to run any app, change into its installed directory and run web without any arguments:

$ cd spirals && web

Each Sharp App can also be run as a .NET Core Windows Desktop App by installing the app dotnet tool:

$ dotnet tool install -g app

Then running the Web App with app instead of web.

$ app


web install spirals - - mythz/spirals

Spirals is a good example showing how easy it is to create .NET Core Desktop Web Apps utilizing HTML5's familiar and simple development model to leverage advanced Web Technologies like SVG in a fun, interactive and live development experience.

See the Making of Spirals for a walk through on how to create the Spirals Web App from scratch.

SharpApp Project Templates

A more complete starting option is to start from one of the project templates below. All project templates can be installed using the web new tool, which if not already can be installed with:

$ dotnet tool install --global web

Bare SharpApp

To start with a simple and minimal website, create a new bare-webapp project template:

$ web new bare-webapp ProjectName

This creates a multi-page Bootstrap Website with Menu navigation that's ideal for content-heavy Websites.

Parcel SharpApp

For more sophisticated JavaScript Sharp Apps we recommended starting with the parcel-webapp project template:

$ web new parcel-webapp ProjectName

This provides a simple and powerful starting template for developing modern JavaScript .NET Core Sharp Apps utilizing the zero-configuration Parcel bundler to enable a rapid development workflow with access to best-of-class web technologies like TypeScript that's managed by pre-configured npm scripts to handle its entire dev workflow.

If needed, it includes an optional server component containing any C# extensions needed that's automatically built and deployed to the app's /plugins folder when started. This enables an extensibile .NET Core Web App with an integrated Parcel bundler to enable building sophisticated JavaScript Apps out-of-the-box within a live development environment.

Cloud Apps Starter Projects

If you intend to deploy your Web App on AWS or Azure you may prefer to start with one of the example Cloud Apps below which come pre-configured with deployment scripts for deploying with Travis CI and Docker:

About Bare WebApp

The Getting Started project contains a copy of the project below which is representative of a typical Company splash Website:

Bare WebApp screenshot

The benefits over using a static website is improved maintenance as you can extract and use its common _layout.html instead of having it duplicated in each page. The menu.html partial also makes menu items easier to maintain by just adding an entry in the JavaScript object literal. The dynamic menu also takes care of highlighting the active menu item.

{{ { '/':         'Home',
     '/about':    'About',
     '/services': 'Services',
     '/contact':  'Contact',
   } | toList | assignTo: links }}

Ideal for Web Designers and Content Authors

The other primary benefit is that this is an example of a website that can be maintained by employees who don't have any programming experience as #Script in their basic form are intuitive and approachable to non-developers, e.g: The title of each page is maintained as metadata HTML comments:

title: About Us

#Script syntax is also the ideal way to convey variable substitution, e.g: <title>{{ title }}</title> and even embedding a partial reads like english {{ 'menu' | partial }} which is both intuitive and works well with GUI HTML designers.


Below is the app.settings for a Basic App, with contentRoot being the only setting required as the rest can be inferred but including the other relevant settings is both more descriptive to other developers as well making it easier to use tools like sed or powershell to replace them during deployment.

debug true
name Bare Web App
contentRoot ~/../bare
webRoot ~/../bare
debug true controls the level of internal diagnostics available and whether or not Hot Reloading is enabled.

About Parcel WebApp

The Parcel SharpApp template is maintained in the following directory structure:

  • /app - Your Web App's published source code and any plugins
  • /client - The Parcel managed Client App where client source code is maintained
  • /server - Extend your Web App with an optional server.dll plugin containing additional Server functionality
  • /web - The Web App binaries

Most development will happen within /client which is automatically published to /app using parcel's CLI that's invoked from the included npm scripts.


The difference with templates-webapp is that the client source code is maintained in the /client folder and uses Parcel JS to automatically bundle and publish your App to /app/wwwroot which is updated with live changes during development.

The client folder also contains the npm package.json which contains all npm scripts required during development.

If this is the first time using Parcel, you will need to install its global CLI:

$ npm install -g parcel-bundler

Then you can run a watched parcel build of your Web App with:

$ npm run dev

Parcel is a zero configuration bundler which inspects your .html files to automatically transpile and bundle all your .js and .css assets and other web resources like TypeScript .ts source files into a static .html website synced at /app/wwwroot.

Then to start the ServiceStack Server to host your Web App run:

$ npm run server

Which will host your App at http://localhost:5000 which in debug mode will enable hot reloading which will automatically reload your web page as it detects any file changes made by parcel.


To enable even richer functionality, this Sharp Apps template is also pre-configured with a custom Server project where you can extend your Web App with Plugins where all Plugins, Services, Filters, etc are automatically wired and made available to your Web App.

This template includes a simple ServerPlugin.cs which contains an Empty ServerPlugin and Hello Service:

public class ServerPlugin : IPlugin
    public void Register(IAppHost appHost)

//[Route("/hello/{Name}")] // Handled by /hello/_name.html API page, uncomment to take over
public class Hello : IReturn<HelloResponse>
    public string Name { get; set; }

public class HelloResponse
    public string Result { get; set; }

public class MyServices : Service
    public object Any(Hello request)
        return new HelloResponse { Result = $"Hi {request.Name} from server.dll" };

To build the server.csproj project and copy the resulting server.dll to /app/plugins/serer.dll which will require restarting the server to load the latest plugin:

$ npm run server

This will automatically load any Plugins, Services, Filters, etc and make them available to your Web App.

One benefit of creating your APIs with C# ServiceStack Services instead of API Pages is that you can generate TypeScript DTOs with:

$ npm run dtos

Which saves generate DTOs for all your ServiceStack Services in dtos.ts which can then be accessed in your TypeScript source code.

If preferred you can instead develop Server APIs with API Pages, an example is included in /client/hello/_name.html

{{ { result: `Hi ${name} from /hello/_name.html` } | return }}

Which as it uses the same data structure as the Hello Service above, can be called with ServiceStack's JsonServiceClient and generated TypeScript DTOs.

The /client/index.ts shows an example of this where initially the App calls the C# Hello ServiceStack Service:

import { client } from "./shared";
import { Hello, HelloResponse } from "./dtos";

const result = document.querySelector("#result")!;

document.querySelector("#Name")!.addEventListener("input", async e => {
  const value = ( as HTMLInputElement).value;
  if (value != "") {
    const request = new Hello(); = value;
    const response = await client.get(request);
    // const response = await client.get<HelloResponse>(`/hello/${}`); //call /hello/_name.html
    result.innerHTML = response.result;
  } else {
    result.innerHTML = "";

But while your App is running you can instead toggle the uncommented the line and hit Ctrl+S to save index.ts which Parcel will automatically transpile and publish to /app/wwwroot that will be detected by Hot Reload to automatically reload the page with the latest changes. Now typing in the text field will display the response from calling the /hello/_name.html API instead.


During development Parcel maintains a debug and source-code friendly version of your App. Before deploying you can build an optimized production version of your App with:

$ npm run build

Which will bundle and minify all .css, .js and .html assets and publish to /app/wwwroot.

Then to deploy Sharp Apps you just need to copy the /app and /web folders to any server with .NET Core 2.1 runtime installed. The Deploying Sharp Apps docs.

Example Sharp Apps

In addition to the templates there's a number of Sharp Apps to illustrate the various features available and to showcase the different kind of Web Apps that can easily be developed. The source code for each app is available either individually from

Redis HTML

web install redis-html - - sharp-apps/redis-html

For the Redis Browser Web App, we wanted to implement an App that was an ideal candidate for a Single Page App but constrain ourselves to do all HTML rendering on the server and have each interaction request a full-page reload to see how a traditional server-generated Web App feels like with the performance of .NET Core 2.1 and #Script. We're pleasantly surprised with the result as when the App is run locally the responsiveness is effectively indistinguishable from an Ajax App. When hosted on the Internet there is a sub-second delay which causes a noticeable flicker but it still retains a pleasant UX that's faster than most websites.

The benefits of a traditional website is that it doesn't break the web where the back button and deep linking work without effort and you get to avoid the complexity train of adopting a premier JavaScript SPA Framework's configuration, dependencies, workflow and build system which has become overkill for small projects.

Redis HTML WebApp Screenshot

We've had a sordid history developing Redis UI's which we're built using the popular JavaScript frameworks that appeared dominant at the time but have since seen their ecosystem decline, starting with the Redis Admin UI (src) built using Google's Closure Library that as it works different to everything else needed a complete rewrite when creating (src) using the hot new React framework, unfortunately it uses React's old deprecated ES5 syntax and Reflux which is sufficiently different from our current recommended TypeScript + React + Redux + WebPack JavaScript SPA Stack, that is going to require a significant refactor to adopt our preferred SPA tech stack.

Beautiful, succinct, declarative code

The nice thing about generating HTML is that it's the one true constant in Web development that will always be there. The entire functionality for the Redis Web App is contained in a single /redis-html/app/index.html which includes all Template and JavaScript Source Code in < 200 lines which also includes all as server logic as it doesn't rely on any back-end Services and just uses the Redis Scripts to interface with Redis directly. The source code also serves as a good demonstration of the declarative coding style that #Script encourages that in addition to being highly-readable requires orders of magnitude less code than our previous Redis JavaScript SPA's with a comparable feature-set.

Having a much smaller code-base makes it much easier to maintain and enhance whilst being less susceptible to becoming obsolete by the next new JavaScript framework as it would only require rewriting 75 lines of JavaScript instead of the complete rewrite that would be required to convert the existing JavaScript Apps to a use different JavaScript fx.


The app.settings for Redis is similar to Web App Starter above except it adds a redis.connection to configure a RedisManagerPool at the connection string provided as well as Redis Scripts to give the scripts access to the Redis instance.

debug true
name Redis Web App
contentRoot ~/../redis-html
webRoot ~/../redis-html
redis.connection localhost:6379

Redis Vue

web install redis - - sharp-apps/Redis

Whilst the above server-generated HTML Redis UI shows how you can easily develop traditional Web Apps using #Script, we've also rewritten the Redis UI as a Single Page App which is the more suitable choice for an App like this as it provides a more optimal and responsive UX by only loading the HTML page once on Startup then utilizes Ajax to only download and update the incremental parts of the App's UI that needs changing.

Instead of using jQuery and server-side HTML this version has been rewritten to use Vue where the UI has been extracted into isolated Vue components utilizing Vue X-Templates to render the App on the client where all Redis Vue's functionality is contained within the Redis/app/index.html page.

Redis Vue WebApp Screenshot

Simple Vue App

#Script also provides a great development experience for Single Page Apps which for the most part gets out of your way letting you develop the Single Page App as if it were a static .html file, but also benefits from the flexibility of a dynamic web page when needed.

The containing _layout.html page can be separated from the index.html page that contains the App's functionality, where it's able to extract the title of the page and embed it in the HTML <head/> as well as embed the page's <script /> in its optimal location at the bottom of the HTML <body/>, after the page's blocking script dependencies:

<title>{{ title ?? 'Redis Vue SharpApp' }}</title>
<i hidden>{{ '/js/hot-loader.js' | ifDebugIncludeScript }}</i>

<link rel="stylesheet" href="../assets/css/bootstrap.css">
<link rel="stylesheet" href="../assets/css/default.css">
    <h2 id="title"><a href="/"><img src="/assets/img/redis-logo.png" /> {{ title }}</a></h2>

    {{ page }}

    <script src="../assets/js/html-utils.js"></script>
    <script src="../assets/js/vue{{ '.min' | if(!debug) }}.js"></script>
    <script src="../assets/js/axios.min.js"></script>
    {{ scripts | raw }} 

Redis Vue avoids the complexity of adopting a npm build system by referencing Vue libraries as a simple script include:

<script src="../assets/js/vue{{ '.min' | if(!debug) }}.js">

Where it uses the more verbose and developer-friendly vue.js during development whilst using the production optimized vue.min.js for deployments. So despite avoiding the complexity tax of an npm-based build system it still gets some of its benefits like conditional deployments and effortless hot reloading.

Server Pages

Whilst most of index.html is a static Vue app, #Script is leveraged to generate the body of the <redis-info/> Component on the initial home page render:

<script type="text/x-template" id="redis-info-template">
<div id="redis-info">
  <table class="table table-striped" style="width:450px">
    {{#each toList(redisInfo) }}
          <th>{{ it.Key | replace('_',' ') }}</th>
          <td title="{{it.Value}}">{{ it.Value | substringWithEllipsis(32) }}</td>

This technique increases the time to first paint by being able to render the initial Vue page without waiting for an Ajax call response whilst benefiting from improved SEO from server-generated HTML.

Server Handling

Another area #Script is used is to handle the HTTP POST where it calls the redisChangeConnection filter to change the current Redis connection before rendering the connection-info.html partial with the current connection info:

<script type="text/x-template" id="connection-template">
<div id="connection-info" class="container">
    {{ continueExecutingFiltersOnError }}
    {{#if Request.Verb == "POST" }}
        {{ { host, port, db, password } | withoutEmptyValues | redisChangeConnection | end }}
        {{#if lastErrorMessage }}
            <div class="alert alert-danger">{{lastErrorMessage}}</div>
            <div class="alert alert-success">Connection Changed</div>

Vue Ajax Server APIs

All other Server functionality is invoked by Vue using Ajax to call one of the Ajax APIs below implemented as API Pages:


Called when searching for Redis keys where the query is forwarded to the redisSearchKeys filter:

{{ limit ?? 100  | assignTo: limit }}

{{ `${q ?? ''}*` | redisSearchKeys({ limit }) 
                 | return }}

Called to execute an arbitrary Redis command on the connected instance, with the response from Redis is returned as a plain-text HTTP Response:

{{ { command } | ensureAllArgsNotEmpty }}

{{ ['flush','monitor','brpop','blpop'] | any => contains(lower(command), it)
   | assignTo: illegalCommand }}

{{ illegalCommand ? throwArgumentException('Command is not allowed.') : null }}

{{ command  | redisCall | assignTo: contents }}

{{ contents | return({ 'Content-Type': 'text/plain' }) }}

The benefits of using API Pages instead of a normal C# Service is being able to retain Web App's productive development workflow where the entire Redis Vue App is built without requiring any compilation.

Deep linking and full page reloads

The Redis Vue Single Page App also takes advantage of HTML5's history.pushState API to enable deep-linking and back-button support where most UI state changes is captured on the query string and used to initialize the Vue state on page navigation or full-page reloads where it provides transparent navigation and back-button support that functions like a traditional Web App but with the instant performance of a Single Page App.


The Rockwind website shows an example of combining multiple websites in a single Web App - a Rockstars Content Website and a dynamic data-driven UI for the Northwind database which can run against either SQL Server, MySql or SQLite database using just configuration. It also includes API Pages examples for rapidly developing Web APIs.


/rockstars is an example of a Content Website that itself maintains multiple sub sections with their own layouts - /rockstars/alive for living Rockstars and /rockstars/dead for the ones that have died. Each Rockstar maintains their own encapsulated mix of HTML, markdown content and splash image that intuitively uses the closest _layout.html, and splash.jpg from the page they're referenced from. This approach makes it easy to move entire sub sections over by just moving a folder and it will automatically use the relevant layout and partials of its parent.

Rockwind WebApp screenshot


/northwind is an example of a dynamic UI for a database containing a form to filter results, multi-nested detail pages and deep-linking for quickly navigating between referenced data. #Script is also a great solution for rapidly developing Web APIs where the /api/customers.html API Page below:

{{ limit ?? 100 | assignTo: limit }}

{{ 'select Id, CompanyName, ContactName, ContactTitle, City, Country from Customer' | assignTo: sql }}

{{#if !isEmpty(PathArgs)}}
   {{ `${sql} where Id = @id` | dbSingle({ id: PathArgs[0] }) 
      | return }}

{{#if id}}      {{ 'Id = @id'           | addTo: filters }} {{/if}}
{{#if city}}    {{ 'City = @city'       | addTo: filters }} {{/if}}
{{#if country}} {{ 'Country = @country' | addTo: filters }} {{/if}}

{{#if !isEmpty(filters)}}
  {{ `${sql} WHERE ${join(filters, ' AND ')}` | assignTo: sql }}

{{ `${sql} ORDER BY CompanyName ${sqlLimit(limit)}` | assignTo: sql }}

{{ sql | dbSelect({ id, city, country }) 
       | return }}

Is all the code needed to generate the following API endpoints:

/customers API
All Customers
Accept HTTP Header also supported
Alfreds Futterkiste Details
As List
Customers in Germany
Customers in London
Combination Query /api/customers?city=London&country=UK&limit=3

Multi platform configurations

In addition to being a .NET Core 2.1 App that runs flawlessly cross-platform on Windows, Linux and OSX, Sharp Apps can also support multiple RDBMS's and Virtual File Systems using just configuration.


SQLite uses a file system database letting you bundle your database with your App. So we can share the northwind.sqlite database across multiple Apps, the contentRoot is set to the /apps directory which can only be accessed by your App, whilst the webRoot is configured to use the Sharp Apps folder that hosts all the publicly accessible files of your App.

debug true
name Rockwind SQLite Web App
contentRoot ..
webRoot .
db sqlite
db.connection ~/northwind.sqlite

To run the Rockwind app using the northwind.sqlite database, run the command below on Windows, Linux or OSX:

dotnet web/app.dll ../rockwind/app.sqlite.settings

To switch to use the Northwind database in SQL Server we just need to update the configuration to point to a SQL Server database instance. Since the App no longer need access to the northwind.sqlite database, the contentRoot can be reverted back to the Sharp Apps folder:

debug true
name Rockwind SQL Server Web App
port 5000
db sqlserver
db.connection Server=localhost;Database=northwind;User Id=test;Password=test;

The /support/northwind-data project lets you quickly try out Rockwind against your local RDBMS by populating it with a copy of the Northwind database using the same sqlserver identifier and connection string from the App, e.g:

dotnet run sqlserver "Server=localhost;Database=northwind;User Id=test;Password=test;"


You can run against a MySql database in the same way as SQL Server above but using a MySql db connection string:

debug true
name Rockwind MySql Web App
port 5000
db mysql
db.connection Server=localhost;Database=northwind;UID=root;Password=test;SslMode=none

The example Azure configuration is also configured to use a different Virtual File System where instead of sourcing Web App files from the filesystem they're sourced from an Azure Blob Container. In this case we're not using any files from the App so we don't need to set a contentRoot or webRoot path. This also means that for deployment we're just deploying the WebApp binaries with just this app.settings since both the Web App files and database are sourced remotely.

# Note: values prefixed with '$' are resolved from Environment Variables
debug false
name Azure Blob SQL Server Web App
bind *
port 5000
db sqlserver
files azure
files.config {ConnectionString:$AZURE_BLOB_CONNECTION_STRING,ContainerName:rockwind-fs}

# Reduces a Blob Storage API call, but takes longer for modified pages to appear
checkForModifiedPagesAfterSecs 60
defaultFileCacheExpirySecs     60

The /support/copy-files project lets you run Rockwind against your own Azure Blob Container by populating it with a copy of the /rockwind App's files using the same configuration above:

dotnet run azure "{ConnectionString:$AZURE_BLOB_CONNECTION_STRING,ContainerName:rockwind}"


As #Script is unable to use a Typed ORM like OrmLite to hide the nuances of each database, we need to be a bit more diligent in #Script to use parameterized SQL that works across multiple databases by using the sql* DB Filters to avoid using RDBMS-specific SQL syntax. The /northwind/customer.html contains a good example containing a number of things to watch out for:

{{#if id}}
    {{ `select o.Id, 
            ${sqlConcat(["e.FirstName", "' '", "e.LastName"])} Employee, 
            OrderDate, ShipCountry, ShippedDate, 
            ${sqlCurrency("sum((d.Unitprice * d.Quantity) -")} Total 
        from ${sqlQuote("Order")} o
            inner join
            OrderDetail d on o.Id = d.OrderId
            inner join 
            Employee e on o.EmployeeId = e.Id
        where CustomerId = @id
        group by o.Id, EmployeeId, FirstName, LastName, OrderDate, ShipCountry, ShippedDate`
        | dbSelect({ id }) 
        | assignTo: orders }}

Use sqlConcat to concatenate strings using the RDBMS-specific SQL for the configured database. Likewise sqlCurrency utilizes RDBMS-specific SQL functions to return monetary values in a currency format, whilst sqlQuote is used for quoting tables named after a reserved word.

Of course if you don't intend on supporting multiple RDBMS's, you can ignore this and use RDBMS-specific syntax.

Rockwind VFS

web install rockwind-aws - - sharp-apps/rockwind-aws

/rockwind-vfs is a clone of the Rockwind Web App with 3 differences: It uses the resolveAsset filter for each .js, .css and image web asset so that it's able to generate external URLs directly to the S3 Bucket, Azure Blob Container or CDN hosting a copy of your files to both reduce the load on your Web App and maximize the responsiveness to the end user.

To maximize responsiveness when using remote storage, all embedded files utilize caching:

{{ "" | includeFileWithCache | markdown }}

The other difference is that each table and column has been quoted in "double-quotes" so that it works in PostgreSQL which otherwise treats unquoted symbols as lowercase. This version of Rockwind also works with SQL Server and SQLite as they also support "Table" quotes but not MySql which uses `BackTicks` or [SquareBrackets]. It's therefore infeasible to develop Apps that support both PostgreSQL and MySql unless you're willing to use all lowercase, snake_case or the sqlQuote filter for every table and column.

Rockwind VFS WebApp screenshot


If using a remote file storage like AWS S3 or Azure Blob Storage it's a good idea to use the resolveAsset filter for each external file reference. By default it returns the same path it was called with so it will continue to work locally but then ServiceStack effectively becomes a proxy where it has to call the remote Storage Service for each requested download.

<link rel="stylesheet" href="{{ 'assets/css/bootstrap.css' | resolveAsset }}" />

<img src="{{ 'splash.jpg' | resolveAsset }}" id="splash" alt="Dave Grohl" />

ServiceStack asynchronously writes each file to the Response Stream with the last Last-Modified HTTP Header to enable browser caching so it's still a workable solution but for optimal performance you can specify an args.assetsBase in your app.settings to populate the assetsBase ScriptContext Argument the resolveAsset filter uses to generate an external URL reference to the file on the remote storage service, reducing the load and improving the performance of your App, especially if it's configured to use a CDN.

Pure Cloud Apps


The AWS settings shows an example of this where every external resource has been replaced with a direct reference to the asset on the S3 bucket:

# Note: values prefixed with '$' are resolved from Environment Variables
debug false
name AWS S3 PostgreSQL Web App
db postgres
db.connection $AWS_RDS_POSTGRES
files s3
files.config {AccessKey:$AWS_S3_ACCESS_KEY,SecretKey:$AWS_S3_SECRET_KEY,Region:us-east-1,Bucket:rockwind}

# Reduces an S3 API call, but takes longer for modified pages to appear
checkForModifiedPagesAfterSecs 60
defaultFileCacheExpirySecs     60

With all files being sourced from S3 and the App configured to use AWS RDS PostgreSQL, the AWS settings is an example of a Pure Cloud App where the entire App is hosted on managed cloud services that's decoupled from the .NET Core 2.1 binary that runs it that for the most part won't require redeploying the Web App binary unless making configuration changes or upgrading the web/app.dll as any App changes can just be uploaded straight to S3 which changes reflected within the checkForModifiedPagesAfterSecs setting, which tells the Web App how long to wait before checking for file changes whilst defaultFileCacheExpirySecs specifies how long to cache files like for.


Deployments are also greatly simplified as all that's needed is to deploy the WebApp binary and app.settings of your Cloud App, e.g. here's the DockerFile for - deployed to AWS ECS using the deployment scripts in rockwind-aws and following our .NET Core Docker Deployment Guideline:

FROM microsoft/dotnet:2.1-sdk AS build-env
COPY app /app
RUN dotnet tool install -g web

# Build runtime image
FROM microsoft/dotnet:2.1-aspnetcore-runtime
COPY --from=build-env /app app
COPY --from=build-env /root/.dotnet/tools tools
ENTRYPOINT ["/app/tools/web", "app/app.settings"]

We can also create Azure Cloud Apps in the same we've done for AWS above, which runs the same /rockwind-vfs Web App but using an Azure hosted SQL Server database and its files hosted on Azure Blob Storage:

# Note: values prefixed with '$' are resolved from Environment Variables
debug false
name Azure Blob SQL Server Web App
bind *
db sqlserver
files azure
files.config {ConnectionString:$AZURE_BLOB_CONNECTION_STRING,ContainerName:rockwind}

# Reduces an S3 API call, but takes longer for modified pages to appear
checkForModifiedPagesAfterSecs 60
defaultFileCacheExpirySecs     60


web install plugins - - sharp-apps/Plugins

Up till now the Apps above only have only used functionality built into ServiceStack, to enable even greater functionality but still retain all the benefits of developing Web Apps you can drop .dll with custom functionality into your Web App's /plugins folder. The plugins support in Web Apps is as friction-less as we could make it, there's no configuration to maintain or special interfaces to implement, you're able to drop your existing implementation .dll's as-is into the App's `/plugins` folder.

Plugins allow "no touch" sharing of ServiceStack Plugins, Services, Script Methods Sharp Code Pages, Validators, etc. contained within .dll's or .exe's dropped in a Sharp App's /plugins folder which are auto-registered on startup. The source code for all plugins used in this App were built from the .NET Core 2.1 projects in the /example-plugins folder. The Sharp App below walks through examples of using Custom Filters, Services and Validators:

Plugins WebApp screenshot

Registering ServiceStack Plugins

ServiceStack Plugins can be added to your App by listing it's Type Name in the features config entry in app.settings:

debug true
name Web App Plugins
contentRoot ~/../plugins
webRoot ~/../plugins
features CustomPlugin, OpenApiFeature, PostmanFeature, CorsFeature, ValidationFeature
CustomPlugin { ShowProcessLinks: true }
ValidationFeature { ScanAppHostAssemblies: true }

All plugins listed in features will be added to your Web App's AppHost in the order they're specified. They can further customized by adding a separate config entry with the Plugin Name and a JavaScript Object literal to populate the Plugin at registration, e.g the config above is equivalent to:

Plugins.Add(new CustomPlugin { ShowProcessLinks = true });
Plugins.Add(new OpenApiFeature());
Plugins.Add(new PostmanFeature());
Plugins.Add(new CorsFeature());
Plugins.Add(new ValidationFeature { ScanAppHostAssemblies = true });

Custom Plugin

In this case it tells our CustomPlugin from /plugins/ServerInfo.dll to also show Process Links in its /metadata Page:

public class CustomPlugin : IPlugin
    public bool ShowDrivesLinks { get; set; } = true;
    public bool ShowProcessLinks { get; set; }

    public void Register(IAppHost appHost)
        if (ShowDrivesLinks)
            var diskFormat = Env.IsWindows ? "NTFS" : "ext2";
                .AddPluginLink("/drives", "All Disks")
                .AddPluginLink($"/drives?DriveFormatIn={diskFormat}", $"{diskFormat} Disks");

        if (ShowProcessLinks)
                .AddPluginLink("/processes", "All Processes")
                .AddPluginLink("/process/current", "Current Process");

Where as it was first registered in the list will appear before any links registered by other plugins:

Metadata screenshot

Built-in Plugins

It also tells the ValidationFeature to scan all Service Assemblies for Validators and to automatically register them which is how ServiceStack was able to find the ContactValidator used to validate the StoreContact request.

Other optional plugins registered in this Web App is the metadata Services required for Open API, Postman as well as support for CORS. You can check the /metadata/debug Inspector for all Plugins loaded in your AppHost.

.NET Extensibility

Plugins can also implement .NET Core's IStartup to be able to register any required dependencies without any coupling to any Custom AppHost.

To simplify configuration you can use the `plugins/*` wildcard in app.settings at the end of an ordered plugin list to register all remaining Plugins it finds in the apps `/plugins` folder:

features OpenApiFeature, PostmanFeature, CorsFeature, ValidationFeature, plugins/*
CustomPlugin { ShowProcessLinks: true }

Each plugin registered can continue to be furthered configured by specifying its name and a JavaScript object literal as seen above.

The /plugins2 App shows an example of this with the StartupPlugin registering a StartupDep dependency which is used by its StartupServices at runtime:

public class StartupDep
    public string Name { get; } = nameof(StartupDep);

public class StartupPlugin : IPlugin, IStartup
    public void Configure(IApplicationBuilder app) {}

    public IServiceProvider ConfigureServices(IServiceCollection services)
        services.AddSingleton(new StartupDep());
        return null;

    public void Register(IAppHost appHost)
            .AddPluginLink("/startup-dep", "Startup Service");

public class GetStartupDep : IReturn<string> {}

public class StartupServices : Service
    public StartupDep StartupDep { get; set; }

    [AddHeader(ContentType = MimeTypes.PlainText)]
    public object Any(GetStartupDep request) => StartupDep.Name;

ServiceStack Ecosystem

All Services loaded by plugins continue to benefit from ServiceStack's rich metadata services, including being listed in the /metadata page, being able to explore and interact with Services using /swagger-ui/ as well as being able to generate Typed APIs for the most popular Mobile, Web and Desktop platforms.


web install chat - - sharp-apps/Chat

/chat is an example of the ultimate form of extensibility where instead of just being able to add Services, Filters and Plugins, etc. You can add your entire AppHost which Sharp Apps will use instead of its own. This vastly expands the use-cases that can be built with Sharp Apps as it gives you complete fine-grained control over how your App is configured.

Chat WebApp screenshot

Develop back-end using .NET IDE's

For we've taken a copy of the existing .NET Core 2.1 Chat App and moved its C# code to /example-plugins/Chat and its files to /apps/chat where it can be developed like any other Web App except it utilizes the Chat AppHost and implementation in the SelfHost Chat App.

Customizations from the original .NET Core Chat implementation includes removing MVC and Razor dependencies and configuration, extracting its _layout.html and converting index.html to use #Script from its original default.cshtml. It's also been enhanced with the ability to evaluate scripts from the Chat window, as seen in the screenshot above.

Chat AppHost

public class Startup
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        var appSettings = new TextFileSettings("~/../../apps/chat/app.settings".MapProjectPath());
        app.UseServiceStack(new AppHost(appSettings));

public class AppHost : AppHostBase
    public AppHost() : base("Chat Web App", typeof(ServerEventsServices).GetAssembly()) {}
    public AppHost(IAppSettings appSettings) : this() => AppSettings = appSettings;

    public override void Configure(Container container)
        Plugins.AddIfNotExists(new SharpPagesFeature()); //Already added if it's running as a Web App
        Plugins.Add(new ServerEventsFeature());

        SetConfig(new HostConfig
            DefaultContentType = MimeTypes.Json,
            AllowSessionIdsInHttpParams = true,


        //Register all Authentication methods you want to enable for this web app.            
        Plugins.Add(new AuthFeature(
            () => new AuthUserSession(),
            new IAuthProvider[] {
                new TwitterAuthProvider(AppSettings),   //Sign-in with Twitter
                new FacebookAuthProvider(AppSettings),  //Sign-in with Facebook
                new GithubAuthProvider(AppSettings),    //Sign-in with GitHub

        container.RegisterAutoWiredAs<MemoryChatHistory, IChatHistory>();

        Plugins.Add(new CorsFeature(
            allowOriginWhitelist: new[] { "http://localhost", "" },
            allowCredentials: true,
            allowedHeaders: "Content-Type, Allow, Authorization"));
Reusing Web App's app.setting and files

One nice thing from being able to reuse existing AppHost's is being able to develop all back-end C# Services and Custom Filters as a stand-alone .NET Core Project where it's more productive with access to .NET IDE tooling and debugging.

To account for these 2 modes we use AddIfNotExists to only register the SharpPagesFeature plugin when running as a stand-alone App and add an additional constructor so it reuses the existing app.settings as its IAppSettings provider for is custom App configuration like OAuth App keys required for enabling Sign-In's via with Twitter, Facebook and GitHub when running on http://localhost:5000:

debug true
name Chat Web App
contentRoot ~/../chat
webRoot ~/../chat

oauth.RedirectUrl http://localhost:5000/
oauth.CallbackUrl http://localhost:5000/auth/{0}
oauth.twitter.ConsumerKey JvWZokH73rdghDdCFCFkJtCEU
oauth.twitter.ConsumerSecret WNeOT6YalxXDR4iWZjc4jVjFaydoDcY8jgRrGc5FVLjsVlY2Y8
oauth.facebook.Permissions email
oauth.facebook.AppId 447523305684110
oauth.facebook.AppSecret 7d8a16d5c7cbbfab4b49fd51183c93a0
oauth.github.Scopes user
oauth.github.ClientId dbe8c242e3d1099f4558
oauth.github.ClientSecret 42c8db8d0ca72a0ef202e0c197d1377670c990f4

After the back-end has been implemented we can build and copy the compiled Chat.dll into the Chat's /plugins folder where we can take advantage of the improved development experience for rapidly developing its UI.


web install blog - - sharp-apps/Blog

Live Demo:

To maximize approachability the /Blog Web App has no C# source code, plugins and uses no JavaScript or CSS frameworks yielding an enjoyable development experiences as all the usual complexities of developing a C# Server and modern JS App has been dispensed and you can just focus on the App you want to create, using a plain-text editor on the left, a live updating browser on the right and nothing else to interrupt your flow.

Any infrastructure dependencies have also been avoided by using SQLite by default which is automatically created an populated on first run if no database exists, or if preferred can be changed to use any other popular RDBMS using just config.

Multi User Blogging Platform

Any number of users can Sign In via Twitter and publish content under their Twitter Username where only they'll be able to modify their own Content. Setting up Twitter is as easy as it can be which just requires modifying the Twitter Auth Config in app.settings with the URL where the blog is hosted and the OAuth Keys for the Twitter OAuth App created at

Rich Content

Whilst most blogging platforms just edit static text, each Post content has access to full Templates language features so they can be used to create Live Documents or Render Markdown which is itself just one of the available blocks where it will render to HTML any content between the markdown blocks:

## Markdown Content

By default the Markdig implementation is used to render Markdown but can also be configured to use an alternate Markdown provider.

Rich Markdown Editor

To make it easy to recall Markdown features, each Content is equipped with a Rich Text editor containing the most popular formatting controls along with common short-cuts for each feature, discoverable by hovering over each button:

The Editor also adopts popular behavior in IDEs where Tab and SHIFT+Tab can be used to indent blocks of text and lines can be commented with Ctrl+/ or blocks with CTRL+SHIFT+/.

Another nice productivity win is being able to CTRL+CLICK on any Content you created to navigate to its Edit page.

Auto saved drafts

The content in each Text input and textarea is saved to localStorage on each key-press so you can freely reload pages and navigate around the site where all unpublished content will be preserved upon return.

When you want to revert back to the original published content you can clear the text boxes and reload the page which will load content from the database instead of localStorage.

Server Validation

The new.html and edit.html pages shows examples of performing server validation with #Script:

{{ assignErrorAndContinueExecuting: ex }}

{{ 'Title must be between 5 and 200 characters'      
   | onlyIf(length(postTitle) < 5 || length(postTitle) > 200) | assignTo: titleError }}
{{ 'Content must be between 25 and 64000 characters' 
   | onlyIf(length(content) < 25  || length(content) > 64000) | assignTo: contentError }}
{{ 'Potentially malicious characters detected'       
   | ifNotExists(contentError) | onlyIf(containsXss(content)) | assignTo: contentError }}

For more info see the docs on Error Handling.

Live Previews

Creating and Posting content benefit from Live Previews where its rendered output can be visualized in real-time before it's published.

Any textarea can easily be enhanced to enable Live Preview by including the data-livepreview attribute with the element where the output should be rendered in, e.g:

<textarea data-livepreview=".preview"></textarea>
<div class="preview"></div>

The implementation of which is surprisingly simple where the JavaScript snippet in default.js below is used to send its content on each change:

// Enable Live Preview of new Content
textAreas = document.querySelectorAll("textarea[data-livepreview]");
for (let i = 0; i < textAreas.length; i++) {
  textAreas[i].addEventListener("input", livepreview, false);
  livepreview({ target: textAreas[i] });

function livepreview(e) {
  let el =;
  let sel = el.getAttribute("data-livepreview");

  if (el.value.trim() == "") {
    document.querySelector(sel).innerHTML = "<div>Live Preview</div>";

  let formData = new FormData();
  formData.append("content", el.value);

  fetch("/preview", {
    method: "post",
    body: formData
  }).then(function(r) { return r.text(); })
    .then(function(r) { document.querySelector(sel).innerHTML = r; });

To the /preview.html API Page which just renders and captures any Template Content its sent and returns the output:

{{ content  | evalTemplate({use:{plugins:'MarkdownScriptPlugin'}}) | assignTo:response }}
{{ response | return({ contentType:'text/plain' }) }}

By default the evalTemplate filter renders Templates in a new ScriptContext which can be customized to utilize any additional plugins, filters and blocks available in the configured SharpPagesFeature, or for full access you can use {use:{context:true}} to evaluate any Template content under the same context that evalTemplate is run on.

Customizable Auth Providers

Authentication can now be configured using plain text config in your app.settings where initially you need register the AuthFeature plugin as normal by specifying it in the features list:

features AuthFeature

Then using AuthFeature.AuthProviders you can specify which Auth Providers you want to have registered, e.g:

AuthFeature.AuthProviders TwitterAuthProvider, GithubAuthProvider

Each Auth Provider checks the Sharp Apps app.settings for its Auth Provider specific configuration it needs, e.g. to configure both Twitter and GitHub Auth Providers you would populate it with your OAuth Apps details:


oauth.twitter.ConsumerKey {Twitter App Consumer Key}
oauth.twitter.ConsumerSecret {Twitter App Consumer Secret Key}

oauth.github.ClientId {GitHub Client Id}
oauth.github.ClientSecret {GitHub Client Secret}
oauth.github.Scopes {GitHub Auth Scopes}

Customizable Markdown Providers

By default Sharp Apps now utilize Markdig implementation to render its Markdown. You can also switch it back to the built-in Markdown provider that ServiceStack uses with:

markdownProvider MarkdownDeep

Rich Config Arguments

Any app.settings configs that are prefixed with args. are made available to Sharp Pages and any arguments starting with a { or [ are automatically converted into a JS object: { name:'', href:'/' }
args.tags ['technology','marketing']

Where they can be referenced as an object or an array directly:

{{#each tags}} {{it}} {{/each}}
<a href="{{blog.href}}">{{}}</a>

The alternative approach is to give each argument value a different name:

args.blogHref /

made with by ServiceStack