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
  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. Get("/swagger-ui",_=>
    10. {
    11. var url = $"{Request.Url.BasePath}/api-docs";
    12. return View["doc", url];
    13. });
    14. }
    15. }
  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. return Negotiate.WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/json"), list);
  13. }, null, "GetProductList");
  14. Get("/{productid}", _ =>
  15. {
  16. var productId = _.productid;
  17. if (string.IsNullOrWhiteSpace(productId))
  18. return HttpStatusCode.NotFound;
  19. var isActive = Request.Query.isActive ?? true;
  20. var product = new Product
  21. {
  22. Name = "apple",
  23. Price = 100,
  24. IsActive = isActive
  25. };
  26. return Negotiate.WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/json"), product);
  27. }, null, "GetProductByProductId");
  28. Post("/", _ =>
  29. {
  30. var product = this.Bind<Product>();
  31. if(!Request.Headers.Any(x=>x.Key=="test"))
  32. return HttpStatusCode.BadRequest;
  33. return Negotiate
  34. .WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/json"), product)
  35. .WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/xml"), product);
  36. }, null, "AddProduct");
  37. Put("/", _ =>
  38. {
  39. var product = this.Bind<Product>();
  40. return Negotiate
  41. .WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/json"), product);
  42. }, null, "UpdateProductByProductId");
  43. Head("/",_=>
  44. {
  45. return HttpStatusCode.OK;
  46. },null,"HeadOfProduct");
  47. Delete("/{productid}", _ =>
  48. {
  49. var productId = _.productid;
  50. if (string.IsNullOrWhiteSpace(productId))
  51. return HttpStatusCode.NotFound;
  52. return Response.AsText("Delete product successful");
  53. }, null, "DeleteProductByProductId");
  54. }}
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 = "[email protected]",
  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. Describe["GetProductList"] = desc => desc.AsSwagger(
  7. with => with.Operation(
  8. op => op.OperationId("GetProductList")
  9. //Tag means this api belongs to which group
  10. .Tag("Products")
  11. //Description of this api
  12. .Description("This returns a list of products")
  13. //Summary of this api
  14. .Summary("Get all products")
  15. //Default response of the api
  16. .Response(r => r.Schema<IEnumerable<Product>>(modelCatalog).Description("OK"))
  17. ));
  18. Describe["GetProductByProductId"] = desc => desc.AsSwagger(
  19. with => with.Operation(
  20. op => op.OperationId("GetProductByProductId")
  21. .Tag("Products2")
  22. .Summary("Get a product by product's id")
  23. .Description("This returns a product's infomation by the special id")
  24. //specify the parameters of this api
  25. .Parameter(new Parameter
  26. {
  27. Name = "productid",
  28. //specify the type of this parameter is path
  29. In = ParameterIn.Path,
  30. //specify this parameter is required or not
  31. Required = true,
  32. Description = "id of a product"
  33. })
  34. .Parameter(new Parameter
  35. {
  36. Name = "isactive",
  37. In = ParameterIn.Query,
  38. Description = "get the actived product",
  39. Required = false,
  40. })
  41. .Response(r => r.Schema<Product>(modelCatalog).Description("Here is the product"))
  42. .Response(404, r => r.Description("Can't find the product"))
  43. ));
  44. Describe["AddProduct"] = desc => desc.AsSwagger(
  45. with => with.Operation(
  46. op => op.OperationId("AddProduct")
  47. .Tag("Products")
  48. .Summary("Add a new product to database")
  49. .Description("This returns the added product's infomation")
  50. //Request body when using POST,PUT..
  51. .BodyParameter(para => para.Name("para").Schema<Product>().Description("the infomation of the adding product").Build())
  52. .Parameter(new Parameter()
  53. {
  54. Name = "test",
  55. In = ParameterIn.Header,//http header
  56. Description = "must be not null",
  57. Required = true,
  58. })
  59. //specify the request parameters can only be a JSON object
  60. .ConsumeMimeType("application/json")
  61. //specify the response result can be a JSON object or a xml object
  62. .ProduceMimeTypes(new List<string> { "application/json", "application/xml" })
  63. .Response(r => r.Schema<Product>(modelCatalog).Description("Here is the added product"))
  64. .Response(400, r => r.Description("Some errors occur during the processing"))
  65. ));
  66. Describe["HeadOfProduct"] = desc => desc.AsSwagger(
  67. with => with.Operation(
  68. op => op.OperationId("HeadOfProduct")
  69. //specify this api belongs to multi groups
  70. .Tags(new List<string>() { "Products", "Products2" })
  71. .Summary("Something is deprecated")
  72. .Description("This returns only http header")
  73. //specify this api is deprecated
  74. .IsDeprecated()
  75. .Response(r => r.Description("Nothing will return but http headers"))
  76. ));
  77. }
  78. }

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.