So anyone who has tried to do documentation with NancyFX knows it’s notoriously bad. And I was REALLY tired of hand writing documentation. So I set out to make a custom solution in 8 hrs or less. I’ve tried their Swagger extension and I’ve never been able to get it to work and I’ve spent more time trying to get that to work than implementing this solution. I wanted it to have the following characteristics:

Metadata documentation is close to the code (will make it easier to update as the API updates, I really don’t like a separate module for metadata that has to be tracked down to update docs. Might as well hand write it at that point)

Has a decently friendly user interface to show UI developers the API contract

Doesn’t mess with Nancy’s code. No forking

Has url parameter documentation for calls with url parameters

Has JSON schema for calls with a body

Here’s my process:

So Nancy has metadata, but besides the name, I have no idea how to access it. There’s no documentation and I even dug through the NancyFX source code and couldn’t find anything. So I decided I’d use what is provided and append my own metadata to create the documentation.

Starting with What You Have (Inventory)

Starting with what they DO give you, you can leverage their IRouteCacheProvider to retrieve the the routes and info on the routes. It gets auto-injected into modules so just provide it to a DocModule and call GetCache() like so:

public class DocModule : NancyModule { public DocModule(IRouteCacheProvider routeCacheProvider) : base("docs") { Get[""] = _ => { var cache = routeCacheProvider .GetCache();

Well, um, that’s kind of it. They have the ability to do docs in a separate module but I already said that a requirement is to have the documentation near the code so that it’s easily update-able.

So that means we need to write something custom to join with the route definitions.

How to manage documentation close to code

So I started thinking about how to manage the documentation close to the code. First thing that came to mind was attributes. They describe what they are attached to, and very flexible. Unfortunately you can’t use attributes on inline code, as a Nancy endpoint is just an inline definition in the constructor. So, that’s a no go. Well what if we used attributes on the constructor or class? Is having it on the class or close enough for me?

I decided to keep looking.

Okay, well, the endpoint code is inline, why shouldn’t the documentation? After all an endpoint is just a dictionary, the metadata can be a dictionary as well. Then we can join the existing route data with our new data. Psuedocode time, what does this look like?

RouteMetadata["Test Endpoint"] = new SomeMetadataClass("description", ...); Post["Test Endpoint", "/", true] = async (parameters, token) => { ... }

Okay, I like. Documentation is close to the code, and fulfills all the requirements.

What does this new metadata look like?

So the metadata needs several things:

Description of the endpoint BodySchema Url Parameter Metadata Key Description Default Value Is Required Allows for Multiple Values Valid Values (if limited to certain values)



Okay, so I’m just going to cheat and give you the classes I made:

public class NRouteMetadata { public string Description { get; } public UrlParamProperties[] UrlParamProperties { get; } public string BodySchema { get; private set; } public NRouteMetadata(string description, Type bindModel, params UrlParamProperties[] urlParamProperties) { Description = description; UrlParamProperties = urlParamProperties; if (bindModel != null) { var schema = JsonSchema4.FromTypeAsync(bindModel); schema.ContinueWith(t => { BodySchema = JsonConvert.SerializeObject(t.Result.Properties, Formatting.Indented); }); } } } public class UrlParamProperties { public UrlParamProperties(string key, string description, string defaultValue, bool allowMultiples = false, params string[] acceptedValues) { Key = key; Description = description; DefaultValue = defaultValue; AllowMultiples = allowMultiples; AcceptedValues = acceptedValues?.ToList(); } public UrlParamProperties(string key, string description, bool required, bool allowMultiples = false, params string[] acceptedValues) { Key = key; Description = description; Required = required; AllowMultiples = allowMultiples; AcceptedValues = acceptedValues?.ToList(); } public string Key { get; set; } public string DefaultValue { get; set; } public bool Required { get; set; } public bool AllowMultiples { get; set; } public List AcceptedValues { get; set; } public string Description { get; set; } }

I came to realize that there if there is a default, we know the field isn’t required, and if there’s no default that doesn’t necessarily mean it’s required. Null is a valid value for a parameter.

Loading the documentation

Okay so we need a dictionary to store the new metadata in, in the Modules:

public static Dictionary<string, NRouteMetadata> RouteMetadata { get; } = new Dictionary<string, NRouteMetadata>();

Perfect. So now we’ll make a couple test endpoints:

// For a Post RouteMetadata["Test Endpoint"] = new NRouteMetadata("This is a test description of a test endpoint", typeof(TestEndpointContract)); Post["Test Endpoint", "/", true] = async (parameters, token) => { ... }; RouteMetadata["Query Something"] = new NRouteMetadata("Queries Something", null, // key of param, description, required/default, allows multiple values, accepted values new UrlParamProperties("start-date", "Datetime start", false), new UrlParamProperties("end-date", "Datetime end", false), new UrlParamProperties("page", "Current page (start at 1)", "1"), new UrlParamProperties("pageLength", "Number of items to be returned", "100"), new UrlParamProperties("hourOffset", "Hour offset for timezone", "Server Timezone Offset"), // so here we have "orderByAsc" that isn't required, no default, does allow for multiple values, and accepts the values from QuerySomethingSort new UrlParamProperties("orderByAsc", "Order by ascending fields", false, true, Enum.GetNames(typeof(QuerySomethingSort))), new UrlParamProperties("orderByDesc", "Order by descending fields", false, true, Enum.GetNames(typeof(QuerySomethingSort))) ); Get["Query Something", "/query", true] = async (parameters, token) => { ... }

Combining the route information and our custom metadata

Now we need a module to combine and display the metadata. Here’s my code for displaying as JSON:

public class DocModule : NancyModule { public DocModule(IRouteCacheProvider routeCacheProvider) : base("docs") { Get[""] = _ => { var cache = routeCacheProvider .GetCache(); var response = new Dictionary<string, Dictionary<string, Tuple<RouteDescription, NRouteMetadata>>>(); foreach (var item in cache.Keys) { if (!response.ContainsKey(item.Name)) { response[item.Name] = new Dictionary<string, Tuple<RouteDescription, NRouteMetadata>>(); } var metadata = (Dictionary<string, NRouteMetadata>)item.GetProperties().SingleOrDefault(p => p.Name == "RouteMetadata")?.GetValue(null, null); if (metadata != null) { foreach (var routes in cache[item]) { if (metadata.ContainsKey(routes.Item2.Name)) { response[item.Name][routes.Item2.Name] = new Tuple<RouteDescription, NRouteMetadata>(routes.Item2, metadata[routes.Item2.Name]); } } } } return Response.AsJson(response); }; }

Pretty self explanitory I think. We go through the modules and get the static RouteMetadata, join on the route name and the metadata name, and render as json.

Quick and Dirty Displaying of the Metadata

Everyone that works with me knows I am no special front end developer and have zero eye for design. I prefer to make it work over being pretty and that’s why this looks like something out of a 90’s website. I’ll start with the result, share my views, and hope to God there’s someone out there that may want to spice it up a little bit? Pretty please?

Okay, so the culmination of all of this:

Sooooo cool right? Even able to collapse modules! WOW! Okay, here’s the code, it’s nothing special and I am not a fan of this SuperSimpleViewEngine… Can’t nest iterations without partials… Oh well.

docs.sshtml

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Docs</title> <style> body { background: #F4F0BB; font-family: monospace } .b { font-weight: bold } .default-value[value=""] { display: none } .container { padding: 10px; margin: 10px } .module { background: #87C38F } .collapsable[value="open"] .expand-btn:after { content : '-' } .collapsable[value="closed"] .expand-btn:after { content : '+' } .collapsable[value="closed"] .module-content { display: none } .route { border-bottom : 1px solid black; margin-bottom: 10px; padding-bottom : 10px; } .schema { white-space: pre-wrap; } </style> </head> <body> <h1>Docs</h1> <hr> @Each.Model <div class="module collapsable container" value="open"> <h2 class="module-title" style='cursor: pointer' onclick="if(this.parentElement.getAttribute('value')=='open') { this.parentElement.setAttribute('value','closed') } else { this.parentElement.setAttribute('value','open') }"><span class="expand-btn"></span> @Current.Key</h2> @Partial['Views/module.sshtml', Current.Value] </div> @EndEach </body> </html>

module.sshtml

<div class="module-content"> @Each.Model @Partial['Views/route.sshtml', Current.Value] @EndEach </div>

route.sshtml

<div class="route"> <table> <tr> <td style="width:50%; vertical-align: text-top"> <h3 class="route-title">@Model.Item1.Method - @Model.Item1.Path (@Model.Item1.Name)</h3> <div class="description">@Model.Item2.Description</div> <div class="schema">@Model.Item2.BodySchema</div> </td> <td> <div class="url-params"> @Each.Item2.UrlParamProperties @Partial['Views/urlparam.sshtml', Current] @EndEach </div> </td> </tr> </table> </div>

urlparam.sshtml

<div class="url-param">> <table> <tr> <td> <div class="url-param-title"><span class="b">@Model.Key</span> - @Model.Description <span class="default-value" value="@Model.DefaultValue">(Default: @Model.DefaultValue)</span></div> <div class="url-param-attributes" > @If.AllowMultiples <div class="allow-multiples">Allows Multiples</div> @EndIf @If.HasAcceptedValues <div class="accepted-values"> Accepted Values <ul> @Each.AcceptedValues <li>@Current</li> @EndEach </ul> </div> @EndIf </div> </td> </tr> </table> </div>

And well, that’s it. That’s all I have to say. Leave me a comment if you have any questions or ideas on improvements, or if you can make my UI not look like it’s from the 90’s. Thanks!