Generating API Document In Nancy Using Swagger

Introduction

In my last article, "Generating API Document In Nancy," I introduced how to generate an easy API document in Nancy without third party plugins, which contain less information. Today, we will learn how to use Swagger to generate a richer API document in Nancy through an open source project, Nancy.Swagger.

What is Swagger?

Swagger is a powerful open source framework backed by a large ecosystem of tools that helps you design, build, document, and consume your RESTful APIs.

We can use JSON and YAML to finish it. JSON seems more convenient when we are coding, because we can use lots of JSON libraries in our projects. By the way, the syntax of YAML is also easy to understand!

How does Swagger work? We only need to provide some data to Swagger and it will render the document view which we can see. And, Nancy.Swagger plays a vital role in providing data!

Now, let's begin.

Step 1

This is a previous step of this article. We need to do some preparation.

  1. Create a new empty solution named ApiApp, add three projects to the solution
    • Web - contains the swagger-ui and other static files
    • Modules - contain all Nancy modules and metadata modules
    • Models - contains all the models
  1. Download the static files of Swagger and add them to the ApiApp project. And I made them Embedded Resource which can be output to the bin directory.

    Swagger static files
  1. Add a Bootstrapper class and enable the static files of Swagger in the method ConfigureConventions.
    1. public class Bootstrapper : DefaultNancyBootstrapper  
    2. {  
    3.     protected override void ConfigureConventions(NancyConventions nancyConventions)  
    4.     {  
    5.         base.ConfigureConventions(nancyConventions);
    6.         nancyConventions.StaticContentsConventions.Add(StaticContentConventionBuilder.AddDirectory("swagger-ui"));  
    7.     }  
    8. }  
  1. Create a new doc.html file, which will show us the API document. We need to copy the content from the downloaded index.html in swagger-ui to the doc.html.
  1. Add a HomeModule class to deal with the first time the document is opened. When we open our index page of our API, we will be redirected to the document page.
    1. public class HomeModule : NancyModule  
    2. {  
    3.     public HomeModule()  
    4.     {  
    5.         Get("/", _ =>  
    6.         {  
    7.             return Response.AsRedirect("/swagger-ui");  
    8.         });  
    9.   
    10.         Get("/swagger-ui",_=>  
    11.         {                              
    12.             var url = $"{Request.Url.BasePath}/api-docs";  
    13.             return View["doc", url];  
    14.         });  
    15.     }  
    16. }  
  1. Edit the JavaScript code of doc.html so that we can get our own API information.
    1. window.onload = function() {  
    2. // Build a system  
    3. const ui = SwaggerUIBundle({  
    4.     url: "@Model"//replace the default url to our demo's url  
    5.     dom_id: '#swagger-ui',  
    6.     presets: [  
    7.         SwaggerUIBundle.presets.apis,  
    8.         SwaggerUIStandalonePreset  
    9.     ],  
    10.     plugins: [  
    11.         SwaggerUIBundle.plugins.DownloadUrl  
    12.     ],  
    13.     layout: "StandaloneLayout"})  
    14.     window.ui = ui;  
    15. }  

So far, we have finished the previous work of our demo.

Note

Nancy.Swagger does not include the swagger-ui, which is different compared to Swashbuckle.AspNetCore, which we use in ASP.NET Core Web API. Thus,  we need to add the swagger-ui by ourselves.

Step 2

Create a new module class named ProductsModule. This module contains all APIs.

  1. public class ProductsModule : NancyModule  
  2. {  
  3.     public ProductsModule() : base("/products")  
  4.     {  
  5.         Get("/", _ =>  
  6.         {  
  7.             var list = new List<Product>  
  8.             {  
  9.                 new Product{ Name="p1", Price=199 , IsActive = true },  
  10.                 new Product{ Name="p2", Price=299 , IsActive= true }  
  11.             };  
  12.   
  13.             return Negotiate.WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/json"), list);  
  14.         }, null"GetProductList");  
  15.   
  16.         Get("/{productid}", _ =>  
  17.         {  
  18.             var productId = _.productid;  
  19.             if (string.IsNullOrWhiteSpace(productId))  
  20.                 return HttpStatusCode.NotFound;  
  21.   
  22.             var isActive = Request.Query.isActive ?? true;  
  23.   
  24.             var product = new Product  
  25.             {  
  26.                 Name = "apple",  
  27.                 Price = 100,  
  28.                 IsActive = isActive  
  29.             };  
  30.   
  31.             return Negotiate.WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/json"), product);  
  32.         }, null"GetProductByProductId");  
  33.   
  34.         Post("/", _ =>  
  35.         {  
  36.             var product = this.Bind<Product>();  
  37.   
  38.             if(!Request.Headers.Any(x=>x.Key=="test"))  
  39.                 return HttpStatusCode.BadRequest;  
  40.   
  41.             return Negotiate  
  42.                 .WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/json"), product)  
  43.                 .WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/xml"), product);  
  44.         }, null"AddProduct");  
  45.   
  46.         Put("/", _ =>  
  47.         {  
  48.             var product = this.Bind<Product>();  
  49.   
  50.             return Negotiate  
  51.                 .WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/json"), product);  
  52.         }, null"UpdateProductByProductId");  
  53.   
  54.         Head("/",_=>  
  55.         {  
  56.             return HttpStatusCode.OK;  
  57.         },null,"HeadOfProduct");  
  58.   
  59.         Delete("/{productid}", _ =>  
  60.         {  
  61.             var productId = _.productid;  
  62.   
  63.             if (string.IsNullOrWhiteSpace(productId))  
  64.                 return HttpStatusCode.NotFound;  
  65.   
  66.             return Response.AsText("Delete product successful");  
  67.         }, null"DeleteProductByProductId");  
  68.     }}   
Step 3

Before writing code on a metadata module, we need to add the header for Swagger. It's part of our document, which is basic on OpenAPI Specification. You can follow this link.

Open our Bootrstapper class and add the code given below.

  1. protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)  
  2. {  
  3.     SwaggerMetadataProvider.SetInfo("Nancy Swagger Example""v1.0""Some open api"new Contact()  
  4.     {  
  5.         EmailAddress = "catcher_hwq@outlook.com",  
  6.         Name = "Catcher Wong",  
  7.         Url = "http://www.c-sharpcorner.com/members/catcher-wong"  
  8.     }, "http://www.c-sharpcorner.com/members/catcher-wong");  
  9.     base.ApplicationStartup(container, pipelines);  
  10. }   

This code specifies the header information of our API document. After you run the code (not now!), you may see the screenshot.

Step 4

In this step, we will create a metadata module to describe our API.

  1. public class ProductsMetadataModule : MetadataModule<PathItem>  
  2. {  
  3.     public ProductsMetadataModule(ISwaggerModelCatalog modelCatalog)  
  4.     {  
  5.         modelCatalog.AddModels(typeof(Product), typeof(IEnumerable<Product>));  
  6.   
  7.         Describe["GetProductList"] = desc => desc.AsSwagger(  
  8.             with => with.Operation(                      
  9.                 op => op.OperationId("GetProductList")  
  10.                         //Tag means this api belongs to which group  
  11.                    .Tag("Products")  
  12.                         //Description of this api  
  13.                         .Description("This returns a list of products")  
  14.                         //Summary of this api  
  15.                    .Summary("Get all products")  
  16.                         //Default response of the api  
  17.                    .Response(r => r.Schema<IEnumerable<Product>>(modelCatalog).Description("OK"))  
  18.                    ));  
  19.                           
  20.         Describe["GetProductByProductId"] = desc => desc.AsSwagger(  
  21.             with => with.Operation(  
  22.                 op => op.OperationId("GetProductByProductId")  
  23.                .Tag("Products2")  
  24.                .Summary("Get a product by product's id")  
  25.                .Description("This returns a product's infomation by the special id")  
  26.                                //specify the parameters of this api   
  27.                               .Parameter(new Parameter  
  28.                     {  
  29.                        Name = "productid",  
  30.                                  //specify the type of this parameter is path  
  31.                                  In = ParameterIn.Path,  
  32.                                  //specify this parameter is required or not  
  33.                                  Required = true,  
  34.                             Description = "id of a product"  
  35.                     })  
  36.                     .Parameter(new Parameter  
  37.                     {  
  38.                         Name = "isactive",  
  39.                         In = ParameterIn.Query,  
  40.                         Description = "get the actived product",  
  41.                         Required = false,  
  42.                     })  
  43.                     .Response(r => r.Schema<Product>(modelCatalog).Description("Here is the product"))  
  44.                     .Response(404, r => r.Description("Can't find the product"))  
  45.                     ));  
  46.   
  47.   
  48.         Describe["AddProduct"] = desc => desc.AsSwagger(  
  49.             with => with.Operation(  
  50.                 op => op.OperationId("AddProduct")  
  51.                         .Tag("Products")  
  52.                         .Summary("Add a new product to database")  
  53.                         .Description("This returns the added product's infomation")  
  54.                         //Request body when using POST,PUT..  
  55.                         .BodyParameter(para => para.Name("para").Schema<Product>().Description("the infomation of the adding product").Build())  
  56.                         .Parameter(new Parameter()  
  57.                         {  
  58.                             Name = "test",  
  59.                             In = ParameterIn.Header,//http header  
  60.                             Description = "must be not null",  
  61.                             Required = true,  
  62.                         })  
  63.                         //specify the request parameters can only be a JSON object  
  64.                         .ConsumeMimeType("application/json")  
  65.                         //specify the response result can be a JSON object or a xml object  
  66.                         .ProduceMimeTypes(new List<string> { "application/json""application/xml" })  
  67.                         .Response(r => r.Schema<Product>(modelCatalog).Description("Here is the added product"))  
  68.                         .Response(400, r => r.Description("Some errors occur during the processing"))  
  69.         ));  
  70.   
  71.         Describe["HeadOfProduct"] = desc => desc.AsSwagger(  
  72.                with => with.Operation(  
  73.                    op => op.OperationId("HeadOfProduct")  
  74.                            //specify this api belongs to multi groups  
  75.                            .Tags(new List<string>() { "Products""Products2" })  
  76.                            .Summary("Something is deprecated")  
  77.                            .Description("This returns only http header")  
  78.                            //specify this api is deprecated  
  79.                            .IsDeprecated()  
  80.                            .Response(r => r.Description("Nothing will return but http headers"))  
  81.                             ));  
  82.         }  
  83.     }   

I added some annotations on the code given above to show what they mean. For more details about them, you can visit the wiki page of Nancy.Swagger.

Now, we can run the project.

Here are some screenshots of the result

ApiApp

Screenshot 1

Screenshot 1 is the overview of our API document. We can find out there are two groups in our document, HTTP method of APIs, descriptions of APIs etc.

Generating API Document In Nancy Using Swagger

Screenshot 2

Screenshot 2 is the default view of the API GetProdcutList. We can find out the information, which we described in metadata module.

Generating API Document In Nancy Using Swagger

Screenshot 3

Screenshot 3 is the executed view of the API GetProdcutList. We can find out the information, which is the Server response, such as the status code and the response body.There is a CURL command, which you can find as well. When we use the terminal, we can get the same result from this request.

Note

There is a problem to which we need to pay attention. The file path between Modules and MetadataModules.

Path of Modules

Path of MetadataModules

./ProductsModule

./ProductsMetadataModule

./ProductsModule

./Metadata/ProductsMetadataModule

./Modules/ProductsModule

./Metadata/ProductsMetadataModule

When we create the modules, we need to observe about the rules, otherwise you can not get the route information in Nancy.

Be aware of the name of our Module and the MetadataModule. For example, I already have a module named ProductsModule and I want to create a MetadataModule of this module. What should I name it? ProductMetadataModule? ProductsMetadataModule? Or others? Maybe, there is only one answer: ProductsMetadataModule! Because there are some restrictions.

We can find out the reason on DefaultMetadataModuleConventions class in the project Nancy.Metadata.Modules

Summary

Swagger makes our API document beautiful and executable, and reduces our time to write the documents.

A mind map is given below to show the information about this article.

Nancy.Swagger

Thanks for your patient reading.