Sharp APIs

In addition to being productive high-level .NET scripting language for generating dynamic HTML pages, #Script can also be used to rapidly develop Web APIs which can take advantage of the new support for Dynamic Page Based Routes to rapidly develop data-driven JSON APIs and make them available under the ideal "pretty" URLs whilst utilizing the same Live Development workflow that doesn't need to define any C# Types or execute any builds - as all development can happen in real-time whilst the App is running, enabling the fastest way to develop Web APIs in .NET!

Dynamic Sharp APIs

The only difference between a Sharp Page that generates HTML or a Sharp Page that returns an API Response is that Sharp APIs return a value using the return method.

For comparison, to create a Hello World C# ServiceStack Service you would typically create a Request DTO, Response DTO and a Service implementation:

[Route("/hello/{Name}")]
public class Hello : IReturn<HelloResponse>
{
    public string Name { get; set; }
}
public class HelloResponse
{
    public string Result { get; set; }
}
public class HelloService : Service
{
    public object Any(Hello request) => new HelloResponse { Result = $"Hello, {request.Name}!" };
}

/hello API Page

Usage: /hello/{name}

An API which returns the same wire response as above can be implemented in API Pages by creating a page at /hello/_name/index.html that includes the 1-liner:

{{ { result: `Hello, ${name}!` } | return }}

Which supports the same content negotiation as a ServiceStack Service where calling it in a browser will generate a human-friendly HTML Page:

Or calling it with a JSON HTTP client containing Accept: application/json HTTP Header or with a ?format=json query string will render the API response in the JSON Format:

Alternatively you can force a JSON Response by specifying the Content Type in the return arguments:

{{ { result: `Hello, ${name}!` } | return({ format: 'json' }) }} 
// Equivalent to:
{{ { result: `Hello, ${name}!` } | return({ contentType: 'application/json' }) }}

More API examples showing the versatility of this feature is contained in the new blog.web-app.io which only uses Templates and Dynamic API Pages to implement all of its functionality.

/preview API Page

Usage: /preview?content={templates}

The /preview.html API page uses this to force a plain-text response with:

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

The preview API above is what provides the new Blog Web App's Live Preview feature where it will render any #Script provided in the content Query String or HTTP Post Form Data, e.g:

Which renders the plain text response:

0,1,4,9,16,25,36,49,64,81,

/_user/api Page

Usage: /{user}/api

The /_user/api.html API page shows an example of how easy it is to create data-driven APIs where you can literally return the response of a parameterized SQL query using the dbSelect filter and returning the results:

{{ `SELECT * 
      FROM Post p INNER JOIN UserInfo u on p.CreatedBy = u.UserName 
     WHERE UserName = @user 
    ORDER BY p.Created DESC` 
   | dbSelect({ user })
   | return }}

The user argument is populated as a result of dynamic route from the _user directory name which will let you view all @ServiceStack posts with:

Which also benefits from ServiceStack's multiple built-in formats where the same API can be returned in:

/posts/_slug/api Page

Usage: /posts/{slug}/api

The /posts/_slug/api.html page shows an example of using the httpResult filter to return a custom HTTP Response where if the post with the specified slug does not exist it will return a 404 Post was not found HTTP Response:

{{ `SELECT * 
      FROM Post p INNER JOIN UserInfo u on p.CreatedBy = u.UserName 
     WHERE Slug = @slug 
     ORDER BY p.Created DESC` 
   | dbSingle({ slug })
   | assignTo: post 
}}
{{ post ?? httpResult({ status:404, statusDescription:'Post was not found' }) 
   | return }}

The httpResult filter returns a ServiceStack HttpResult which allows for the following customizations:

httpResult({ 
  status:            404,
  status:            'NotFound' // can also use .NET HttpStatusCode enum name
  statusDescription: 'Post was not found',
  response:          post,
  format:            'json',
  contentType:       'application/json',
  'X-Powered-By':    '#Script',
}) 

Any other unknown arguments like 'X-Powered-By' are returned as HTTP Response Headers.

Returning the httpResult above behaves similarly to customizing a HTTP response using return arguments:

{{ post | return({ format:'json', 'X-Powered-By':'#Script' }) }}

Using the explicit httpResult filter is useful for returning a custom HTTP Response without a Response Body, e.g. the New Post page uses httpFilter to redirect back to the Users posts page after they've successfully created a new Post:

{{#if success}}
    {{ httpResult({ status:301, Location:`/${userName}` }) | return }}
{{/if}}

Dedicated Sharp APIs

Dedicated Sharp APIs lets you specify a path where your "Sharp API" are located when registering SharpPagesFeature:

Plugins.Add(new SharpPagesFeature 
{
    ApiPath = "/api"
})

All pages within the /api folder are also treated like "Sharp API" for creating Web APIs where instead of writing their response to the Output Stream, their return value is serialized in the requested Content-Type using the return method:

{{ response | return }}
{{ response | return({ ... }) }}
{{ httpResult({ ... }) | return }}

The route for the dedicated API page starts the same as the filename and one advantage over Dynamic Sharp APIs above is that a single Page can handle multiple requests with different routes, e.g:

/api/customers                // PathArgs = []
/api/customers/1              // PathArgs = ['1']
/api/customers/by-name/Name   // PathArgs = ['by-name','Name']

API Page Examples

To demonstrate Sharp APIs in action we've added Web APIs equivalents for Rockwind's customers and products HTML pages with the implementation below:

/api/customers

The entire implementation of the customers API is 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}}

{{#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 }}
{{/if}}

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

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

These are some of the API's that are made available with the above implementation:

/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

/api/products

The Products API is an example of a more complex API where data is sourced from multiple tables:

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

{{ `select p.Id, 
           ProductName,
           c.CategoryName Category,
           s.CompanyName Supplier, 
           QuantityPerUnit, 
           ${sqlCurrency("UnitPrice")} UnitPrice, 
           UnitsInStock, UnitsOnOrder, ReorderLevel   
      from Product p
           inner join Category c on p.CategoryId = c.Id
           inner join Supplier s on p.SupplierId = s.Id
     where Discontinued = 0`
  | assignTo: sql }}

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

{{#if id}}           {{ 'p.Id = @id'                 | addTo: filters }} {{/if}}
{{#if category}}     {{ 'c.CategoryName = @category' | addTo: filters }} {{/if}}
{{#if supplier}}     {{ 's.CompanyName = @supplier'  | addTo: filters }} {{/if}}
{{#if nameContains}} {{ 'ProductName LIKE @name'     | addTo: filters }} {{/if}}

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

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

{{ sql | dbSelect({ id, category, supplier, name: `%${nameContains}%` }) 
       | return }}

Some API examples using the above implementation:

/products API
All Products
Chai Product Details
As List
Beverage Products
Bigfoot Breweries Products
Products containing Tofu

Untyped APIs

As these APIs don't have a Typed Schema they don't benefit from any of ServiceStack's metadata Services, i.e. they're not listed in Metadata pages, included in Open API or have Typed APIs generated using Add ServiceStack Reference.

made with by ServiceStack