Click here to Skip to main content
15,881,204 members
Articles / Web Development / ASP.NET

Adding Documentation into ASP.NET Web API

Rate me:
Please Sign up or sign in to vote.
4.58/5 (6 votes)
12 Mar 2018CPOL5 min read 11.1K   76   4   2
When you provide Web API, there is a question, how to inform a user about all its abilities, about the syntax of requests, etc. Usually, you should create some available Web page, where you discuss these topics. But wouldn't it be great, if the Web API itself provided access to the documentation?

If you open a page of any serious project on GitHub, you'll see well-written Readme.md document. This Markdown document describes the purpose of the repository and frequently contains links to other documents. The great thing here is that GitHub automatically converts these documents in HTML and shows the result to you. It makes Markdown files a good place to store documentation about your project. First of all, they can provide rich formatting. Also, they are stored in the VCS along with your code. It makes these files a first-class citizen. You treat them as a part of your code and modify them when you make modifications to your code. At least, it should be in theory. Now you have all your documentation in your repository.

It is a good thing if your repository is opened. But I work for a company, which provides some Web APIs to external clients. These clients do not have access to our code repositories. How should I provide documentation about these services?

I can create a separate site with documentation. But now, I have two places where information about my product is stored: in Markdown files and on this site. I can automate the process of creation of the site with documentation to generate it from my Markdown files. Or I can create a separate document (e.g. PDF) containing content of these files.

There is nothing wrong with this approach. But I think we can get one more step in this direction. Why should we separate our API from documentation? Can we provide them in one place? For example, if our Web API is accessible at URL http://www.something.com/api/data, then documentation about this API can be accessible at URL http://www.something.com/api/help.md.

How difficult is it to implement such documentation system using ASP.NET Web API? Let's take a look.

I'll start with simple Web API using OWIN. Here is my Startup file:

C#
[assembly: OwinStartup(typeof(OwinMarkdown.Startup))]

namespace OwinMarkdown
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            HttpConfiguration config = new HttpConfiguration();

            config.Formatters.Clear();
            config.Formatters.Add(
                new JsonMediaTypeFormatter
                {
                    SerializerSettings = GetJsonSerializerSettings()
                });

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new {id = RouteParameter.Optional}
            );

            app.UseWebApi(config);
        }
        
        private static JsonSerializerSettings GetJsonSerializerSettings()
        {
            var settings = new JsonSerializerSettings();

            settings.Converters.Add(new StringEnumConverter { CamelCaseText = false });

            settings.ContractResolver = new CamelCasePropertyNamesContractResolver();

            return settings;
        }
    }
}

I'll add some Markdown files to my project:

I want to say a couple of words about these files. First of all, there could be a complex structure of subfolders keeping different parts of my documentation. Next, there are other files here, like images. My Markdown files can reference them. Our solution must support both: tree of folders and additional files.

Let's start with Web.config file. We need to make some modifications to it. You see, Internet Information Services (IIS) can provide static files all by itself. For example, if I'll ask for http://myhost/help/root.md, IIS will understand, that there is such a file on the disk. Then, it'll try to return it. It means that IIS will not pass the request to our application. But this is not what we want. We don't want to return raw Markdown file. We want to convert it to HTML first. This is why we need to modify Web.config. We must instruct IIS, that it should pass all requests to our application. We do it by altering the system.webServer section:

XML
<system.webServer>
  <modules runAllManagedModulesForAllRequests="true" />
  <handlers>
    <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
    <remove name="OPTIONSVerbHandler" />
    <remove name="TRACEVerbHandler" />
    <add name="Owin" verb="" path="*" type="Microsoft.Owin.Host.SystemWeb.OwinHttpHandler, 
     Microsoft.Owin.Host.SystemWeb" />
  </handlers>
</system.webServer>

Now IIS will not process static files. But we still need it (e.g., for images in our documentation). This is why we'll use Microsoft.Owin.StaticFiles NuGet package. Let's say, I want my documentation to be available at path "/api/doc". In this case, I'll configure this package the following way:

C#
[assembly: OwinStartup(typeof(OwinMarkdown.Startup))]

namespace OwinMarkdown
{
    public class Startup
    {
        private static readonly string HelpUrlPart = "/api/doc";

        public void Configuration(IAppBuilder app)
        {
            var basePath = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;

            app.UseStaticFiles(new StaticFileOptions
            {
                RequestPath = new PathString(HelpUrlPart),
                FileSystem = new PhysicalFileSystem(Path.Combine(basePath, "Help"))
            });

            HttpConfiguration config = new HttpConfiguration();

            config.Formatters.Clear();
            config.Formatters.Add(
                new JsonMediaTypeFormatter
                {
                    SerializerSettings = GetJsonSerializerSettings()
                });

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new {id = RouteParameter.Optional}
            );

            app.UseWebApi(config);
        }
        
        private static JsonSerializerSettings GetJsonSerializerSettings()
        {
            var settings = new JsonSerializerSettings();

            settings.Converters.Add(new StringEnumConverter { CamelCaseText = false });

            settings.ContractResolver = new CamelCasePropertyNamesContractResolver();

            return settings;
        }
    }
}

Now we can serve static files from "Help" folder of our application by "/api/doc" path. But we still need to somehow convert Markdown files into HTML and serve them. For this purpose, we'll write OWIN middleware. This middleware will use Markdig NuGet package.

C#
[assembly: OwinStartup(typeof(OwinMarkdown.Startup))]

namespace OwinMarkdown
{
    public class Startup
    {
        private static readonly string HelpUrlPart = "/api/doc";

        public void Configuration(IAppBuilder app)
        {
            var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();

            app.Use(async (context, next) =>
            {
                var markDownFile = GetMarkdownFile(context.Request.Path.ToString());

                if (markDownFile == null)
                {
                    await next();

                    return;
                }

                using (var reader = markDownFile.OpenText())
                {
                    context.Response.ContentType = @"text/html";

                    var fileContent = reader.ReadToEnd();

                    fileContent = Markdown.ToHtml(fileContent, pipeline);

                    // Send our modified content to the response body.
                    await context.Response.WriteAsync(fileContent);
                }

            });

            var basePath = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;

            app.UseStaticFiles(new StaticFileOptions
            {
                RequestPath = new PathString(HelpUrlPart),
                FileSystem = new PhysicalFileSystem(Path.Combine(basePath, "Help"))
            });

            HttpConfiguration config = new HttpConfiguration();

            config.Formatters.Clear();
            config.Formatters.Add(
                new JsonMediaTypeFormatter
                {
                    SerializerSettings = GetJsonSerializerSettings()
                });

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new {id = RouteParameter.Optional}
            );

            app.UseWebApi(config);
        }
        
        private static JsonSerializerSettings GetJsonSerializerSettings()
        {
            var settings = new JsonSerializerSettings();

            settings.Converters.Add(new StringEnumConverter { CamelCaseText = false });

            settings.ContractResolver = new CamelCasePropertyNamesContractResolver();

            return settings;
        }

        private static FileInfo GetMarkdownFile(string path)
        {
            if (Path.GetExtension(path) != ".md")
                return null;

            var basePath = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
            var helpPath = Path.Combine(basePath, "Help");

            var helpPosition = path.IndexOf(HelpUrlPart + "/", StringComparison.OrdinalIgnoreCase);
            if (helpPosition < 0)
                return null;

            var markDownPathPart = path.Substring(helpPosition + HelpUrlPart.Length + 1);
            var markDownFilePath = Path.Combine(helpPath, markDownPathPart);
            if (!File.Exists(markDownFilePath))
                return null;

            return new FileInfo(markDownFilePath);
        }
    }
}

Let's take a look at how this middleware works. First of all, it checks if the request was for a Markdown file or not. This is what GetMarkdownFile function does. It tries to find a Markdown file corresponding to a request and returns its FileInfo object if the file is found, or null otherwise. I agree that my implementation of this function is not the best. It just serves to prove the concept. You can replace it with any other implementation that you want.

If the file was not found, our middleware just passes the request further using await next(). But if the file is found, it reads its content, converts it to HTML and returns the response.

Now you have a documentation available for users of your API in several places. It is available in your VCS repository. It is also available directly through your Web API. And your documentation is a part of your code, which is stored under VCS.

This is a great achievement from my point of view. But there is one drawback I'd like to discuss. This system is good if your product is stable. But in the early phase of development, it is not always clear how your API should look like, what is the form of requests and responses, etc. It means that on this phase, your documentation should be opened to comments. There must be some system to comment on the content of Markdown files. GitHub has the system of Issues, where you can write comments about the code. As documentation is a part of our code now, we can use Issues to discuss the content of documentation at development phase. But I think it is not the best solution. It would be much better to write comments directly on the document like we can do it in Confluence. Shortly speaking, I think we still need some tool to be able to discuss Markdown documents at an early stage of development.

You can read more my articles on my blog.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Finstek
China China
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionInteresting approach Pin
Klaus Luedenscheidt13-Mar-18 20:25
Klaus Luedenscheidt13-Mar-18 20:25 
QuestionNice! Pin
C Grant Anderson13-Mar-18 4:23
professionalC Grant Anderson13-Mar-18 4:23 
Nice and innovative! Very good work!

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.