将文档添加到 ASP.NET Web API






4.58/5 (6投票s)
当您提供 Web API 时,会有一个问题:如何告知用户它的所有功能、请求的语法等。通常,您需要创建一个 Web 页面来讨论这些主题。但是,如果 Web API 本身可以提供文档访问,那不是很好吗?
如果您查看 GitHub 上任何严肃项目的页面,您会看到编写精良的 Readme.md 文档。这个 Markdown 文档描述了存储库的目的,并且经常包含指向其他文档的链接。这里很棒的一点是 GitHub 会自动将这些文档转换为 HTML 并显示给您。这使得 Markdown 文件成为存储项目文档的好地方。首先,它们可以提供丰富的格式。此外,它们与代码一起存储在 VCS 中。这使得这些文件成为一流的公民。您可以将它们视为代码的一部分,并在修改代码时修改它们。至少,理论上应该是这样。现在您的所有文档都已在您的存储库中。
如果您的存储库是公开的,那很好。但我为一家为外部客户提供 Web API 的公司工作。这些客户无法访问我们的代码存储库。我应该如何为这些服务提供文档?
我可以创建一个单独的文档站点。但现在,我的产品信息存储在两个地方:Markdown 文件和这个站点上。我可以自动化创建文档站点的过程,从我的 Markdown 文件生成它。或者我可以创建一个单独的文档(例如 PDF),其中包含这些文件的内容。
这种方法没有什么不好。但我认为我们可以再向前迈出一步。为什么要将我们的 API 与文档分开?我们不能将它们放在同一个地方吗?例如,如果我们的 Web API 可在 URL http://www.something.com/api/data 访问,那么关于此 API 的文档可以访问 URL http://www.something.com/api/help.md。
使用 ASP.NET Web API 实现这样的文档系统有多难?让我们来看看。
我将从一个简单的 OWIN Web API 开始。这是我的 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;
}
}
}
我将在我的项目中添加一些 Markdown 文件
我想谈谈这些文件。首先,可能会有一个包含我文档不同部分的复杂子文件夹结构。其次,这里还有其他文件,例如图像。我的 Markdown 文件可以引用它们。我们的解决方案必须同时支持:文件夹树和附加文件。
让我们从 Web.config 文件开始。我们需要对其进行一些修改。您知道,Internet Information Services (IIS) 可以自己提供静态文件。例如,如果我请求 http://myhost/help/root.md,IIS 会知道磁盘上存在这样一个文件。然后,它会尝试返回它。这意味着 IIS 不会将请求传递给我们的应用程序。但这并不是我们想要的。我们不希望返回原始 Markdown 文件。我们想先将其转换为 HTML。这就是为什么我们需要修改 Web.config。我们必须指示 IIS 将所有请求传递给我们的应用程序。我们通过修改 system.webServer
部分来实现这一点。
<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>
现在 IIS 将不会处理静态文件。但我们仍然需要它(例如,用于文档中的图像)。这就是为什么我们将使用 Microsoft.Owin.StaticFiles
NuGet 包。假设我想让我的文档在路径 "/api/doc" 下可用。在这种情况下,我将这样配置此包。
[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;
}
}
}
现在我们可以通过 "/api/doc" 路径为应用程序的 "Help" 文件夹提供静态文件。但我们仍然需要某种方式将 Markdown 文件转换为 HTML 并提供它们。为此,我们将编写 OWIN 中间件。这个中间件将使用 Markdig
NuGet 包。
[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);
}
}
}
让我们看看这个中间件是如何工作的。首先,它会检查请求是否是 Markdown 文件。这就是 GetMarkdownFile
函数所做的。它会尝试查找与请求对应的 Markdown 文件,如果找到文件则返回其 FileInfo
对象,否则返回 null
。我承认我这个函数的实现不是最好的。它只是为了证明概念。您可以将其替换为您想要的任何其他实现。
如果找不到文件,我们的中间件将通过 await next()
将请求传递给下一个。但如果文件找到了,它将读取其内容,将其转换为 HTML 并返回响应。
现在,您的 API 用户可以在多个地方找到文档。它可以在您的 VCS 存储库中找到。它也可以直接通过您的 Web API 找到。您的文档是您代码的一部分,存储在 VCS 下。
从我的角度来看,这是一项了不起的成就。但有一个缺点我想讨论一下。如果您的产品稳定,这个系统就很好。但在开发的早期阶段,您的 API 应该是什么样子、请求和响应的形式是什么等并不总是清楚的。这意味着在这个阶段,您的文档应该开放评论。必须有一个系统来评论 Markdown 文件的内容。GitHub 有 Issues 系统,您可以在其中编写关于代码的评论。现在文档是代码的一部分,我们可以在开发阶段使用 Issues 来讨论文档内容。但我认为这不是最佳解决方案。像我们在 Confluence 中那样,直接在文档上编写评论会更好。简而言之,我认为在开发早期阶段,我们仍然需要一个工具来讨论 Markdown 文档。
您可以在 我的博客 上阅读我更多的文章。