Когда вы предоставляете ваш сервис в виде Web API, встает вопрос о том, как проинформировать пользователя о его возможностях, о синтаксисе его запросов и т.п. Обычно вам приходится создавать отдельные Web-страницы, где вы расскрываете эти темы. Но не было бы лучше, если бы сам ваш Web API обеспечивал доступ к своей документации?
Если вы откроете страницу какого-нибудь серьезного проекта на GitHub, вы увидите хорошо оформленный Readme.md. Этот Markdown-документ описывает цель кода, хранящегося в репозитории, и часто содержит ссылки на другие документы. GitHub автоматически конвертирует Markdown в HTML-представление и показывает вам результат в удобной для чтения форме. Это делает Markdown-файлы удобным способом хранения документации о вашем проекте. Прежде всего, этот формат дает достаточно богатые возможности по форматированию текста. Кроме того, данные файлы хранятся в вашей системе контроля версий (VCS) вместе с вашим кодом. Это делает такие документы равноправными с самими файлами кода (first-class citizens). Вы рассматриваете их как часть кода и изменяете их, когда вносите модификации в код. По крайней мере, так должно быть в теории. Теперь у вас есть вся документация в вашем репозитории.
Если ваш репозиторий является открытым, то все замечательно. Пользователи вашего API могут увидеть документацию там. Но я работаю в компании, которая предоставляет некоторые Web API внешним клиентам. Эти клиенты не имеют доступа к нашим репозиториям. Как нам предоставить им документацию по нашим сервисам?
Можно создать отдельный сайт с документацией. Но тогда у нас будет 2 места, где хранится информация о продукте: в Markdown-файлах и на этом сайте. Можно, конечно, автоматизировать процесс создания сайта с документацией, генерируя его из Markdown-документов. Или же можно создавать отдельный документ (например, PDF), включающий в себя содержимое всех этих файлов.
В этом подходе нет ничего плохого. Но, я думаю, что можно сделать еще один шаг в этом направлении. Зачем мы отделяем документацию от самого API? Нельзя ли поставлять их вместе? Например, наш Web API доступен по адресу www.something.com/api/data, а его документация доступна по адресу www.something.com/api/help.md
Насколько трудно реализовать этот подход в ASP.NET Web API? Давайте посмотрим.
Начнем с простого Web API, основанного на OWIN. Вот мой Startup файл:
[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;
}
}
}
<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>
[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;
}
}
}
[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);
}
}
}
К сожалению, не доступен сервер mySQL