XmlDocumentationExtensions
Provides extension methods for reading XML comments from reflected members.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace NJsonSchema.Infrastructure
{
public static class XmlDocumentationExtensions
{
private class AsyncLock : IDisposable
{
private SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);
public AsyncLock Lock()
{
_semaphoreSlim.Wait();
return this;
}
public void Dispose()
{
_semaphoreSlim.Release();
}
}
private static readonly AsyncLock Lock = new AsyncLock();
private static readonly Dictionary<string, XDocument> Cache = new Dictionary<string, XDocument>(StringComparer.OrdinalIgnoreCase);
public static Task<string> GetXmlSummaryAsync(this Type type)
{
return type.GetTypeInfo().GetXmlDocumentationTagAsync("summary");
}
public static Task<string> GetXmlRemarksAsync(this Type type)
{
return type.GetTypeInfo().GetXmlDocumentationTagAsync("remarks");
}
[Obsolete("Use GetXmlSummary instead.")]
public static Task<string> GetXmlDocumentationAsync(this Type type)
{
return type.GetXmlDocumentationTagAsync("summary");
}
public static Task<string> GetXmlDocumentationTagAsync(this Type type, string tagName)
{
return ((MemberInfo)type.GetTypeInfo()).GetXmlDocumentationTagAsync(tagName);
}
public static async Task<string> GetXmlSummaryAsync(this MemberInfo member)
{
return await member.GetXmlDocumentationTagAsync("summary").ConfigureAwait(false);
}
public static async Task<string> GetXmlRemarksAsync(this MemberInfo member)
{
return await member.GetXmlDocumentationTagAsync("remarks").ConfigureAwait(false);
}
public static async Task<string> GetDescriptionAsync(this MemberInfo memberInfo, IEnumerable<Attribute> attributes)
{
dynamic val = attributes.TryGetIfAssignableTo("System.ComponentModel.DescriptionAttribute", TypeNameStyle.FullName);
dynamic val2 = val != null;
if ((val2 ? false : true) ? val2 : (val2 & !string.IsNullOrEmpty(val.Description)))
return (string)val.Description;
dynamic val3 = attributes.TryGetIfAssignableTo("System.ComponentModel.DataAnnotations.DisplayAttribute", TypeNameStyle.FullName);
val2 = (val3 != null);
if ((val2 ? false : true) ? val2 : (val2 & !string.IsNullOrEmpty(val3.Description)))
return (string)val3.Description;
if (memberInfo != (MemberInfo)null) {
string text = await memberInfo.GetXmlSummaryAsync().ConfigureAwait(false);
if (text != string.Empty)
return text;
}
return null;
}
public static async Task<string> GetDescriptionAsync(this ParameterInfo parameter, IEnumerable<Attribute> attributes)
{
dynamic val = attributes.TryGetIfAssignableTo("System.ComponentModel.DescriptionAttribute", TypeNameStyle.FullName);
dynamic val2 = val != null;
if ((val2 ? false : true) ? val2 : (val2 & !string.IsNullOrEmpty(val.Description)))
return (string)val.Description;
dynamic val3 = attributes.TryGetIfAssignableTo("System.ComponentModel.DataAnnotations.DisplayAttribute", TypeNameStyle.FullName);
val2 = (val3 != null);
if ((val2 ? false : true) ? val2 : (val2 & !string.IsNullOrEmpty(val3.Description)))
return (string)val3.Description;
if (parameter != null) {
string text = await parameter.GetXmlDocumentationAsync().ConfigureAwait(false);
if (text != string.Empty)
return text;
}
return null;
}
public static string GetXmlDocumentationText(this XElement element)
{
if (element != null) {
StringBuilder stringBuilder = new StringBuilder();
foreach (XNode item in element.Nodes()) {
XElement xElement;
if ((xElement = (item as XElement)) != null) {
if (xElement.Name == (XName)"see") {
XAttribute xAttribute = xElement.Attribute("langword");
if (xAttribute != null)
stringBuilder.Append(xAttribute.Value);
else if (!string.IsNullOrEmpty(xElement.Value)) {
stringBuilder.Append(xElement.Value);
} else {
xAttribute = xElement.Attribute("cref");
if (xAttribute != null)
stringBuilder.Append(xAttribute.Value.Trim('!', ':').Trim().Split(new char[1] {
'.'
})
.Last());
else {
xAttribute = xElement.Attribute("href");
if (xAttribute != null)
stringBuilder.Append(xAttribute.Value);
}
}
} else
stringBuilder.Append(xElement.Value);
} else
stringBuilder.Append(item);
}
return stringBuilder.ToString();
}
return null;
}
public static Task ClearCacheAsync()
{
using (Lock.Lock()) {
Cache.Clear();
return DynamicApis.FromResult<object>(null);
}
}
public static async Task<string> GetXmlDocumentationTagAsync(this MemberInfo member, string tagName)
{
if (!DynamicApis.SupportsXPathApis || !DynamicApis.SupportsFileApis || !DynamicApis.SupportsPathApis)
return string.Empty;
AssemblyName name = member.Module.Assembly.GetName();
using (Lock.Lock()) {
if (IgnoreAssembly(name))
return string.Empty;
return RemoveLineBreakWhiteSpaces(((await member.GetXmlDocumentationWithoutLockAsync(await GetXmlDocumentationPathAsync(member.Module.Assembly).ConfigureAwait(false)).ConfigureAwait(false))?.Element(tagName)).GetXmlDocumentationText());
}
}
public static async Task<string> GetXmlDocumentationAsync(this ParameterInfo parameter)
{
if (!DynamicApis.SupportsXPathApis || !DynamicApis.SupportsFileApis || !DynamicApis.SupportsPathApis)
return string.Empty;
AssemblyName name = parameter.Member.Module.Assembly.GetName();
using (Lock.Lock()) {
if (IgnoreAssembly(name))
return string.Empty;
return RemoveLineBreakWhiteSpaces((await parameter.GetXmlDocumentationWithoutLockAsync(await GetXmlDocumentationPathAsync(parameter.Member.Module.Assembly).ConfigureAwait(false)).ConfigureAwait(false)).GetXmlDocumentationText());
}
}
public static async Task<XElement> GetXmlDocumentationAsync(this Type type, string pathToXmlFile)
{
using (Lock.Lock())
return await type.GetTypeInfo().GetXmlDocumentationWithoutLockAsync(pathToXmlFile).ConfigureAwait(false);
}
public static async Task<XElement> GetXmlDocumentationAsync(this ParameterInfo parameter, string pathToXmlFile)
{
try {
if (pathToXmlFile == null || !DynamicApis.SupportsXPathApis || !DynamicApis.SupportsFileApis || !DynamicApis.SupportsPathApis)
return null;
parameter.Member.Module.Assembly.GetName();
using (Lock.Lock())
return await parameter.GetXmlDocumentationWithoutLockAsync(pathToXmlFile).ConfigureAwait(false);
} catch {
return null;
}
}
public static async Task<XElement> GetXmlDocumentationAsync(this MemberInfo member)
{
using (Lock.Lock())
return await member.GetXmlDocumentationWithoutLockAsync().ConfigureAwait(false);
}
public static async Task<XElement> GetXmlDocumentationAsync(this MemberInfo member, string pathToXmlFile)
{
using (Lock.Lock())
return await member.GetXmlDocumentationWithoutLockAsync(pathToXmlFile).ConfigureAwait(false);
}
private static async Task<XElement> GetXmlDocumentationWithoutLockAsync(this ParameterInfo parameter, string pathToXmlFile)
{
try {
if (!DynamicApis.SupportsXPathApis || !DynamicApis.SupportsFileApis || !DynamicApis.SupportsPathApis)
return null;
XDocument xDocument = await TryGetXmlDocumentAsync(parameter.Member.Module.Assembly.GetName(), pathToXmlFile).ConfigureAwait(false);
if (xDocument == null)
return null;
return await parameter.GetXmlDocumentationAsync(xDocument).ConfigureAwait(false);
} catch {
return null;
}
}
private static async Task<XElement> GetXmlDocumentationWithoutLockAsync(this MemberInfo member)
{
if (!DynamicApis.SupportsXPathApis || !DynamicApis.SupportsFileApis || !DynamicApis.SupportsPathApis)
return null;
if (IgnoreAssembly(member.Module.Assembly.GetName()))
return null;
return await member.GetXmlDocumentationWithoutLockAsync(await GetXmlDocumentationPathAsync(member.Module.Assembly).ConfigureAwait(false)).ConfigureAwait(false);
}
private static async Task<XElement> GetXmlDocumentationWithoutLockAsync(this MemberInfo member, string pathToXmlFile)
{
try {
if (!DynamicApis.SupportsXPathApis || !DynamicApis.SupportsFileApis || !DynamicApis.SupportsPathApis)
return null;
XDocument xDocument = await TryGetXmlDocumentAsync(member.Module.Assembly.GetName(), pathToXmlFile).ConfigureAwait(false);
if (xDocument == null)
return null;
XElement element = member.GetXmlDocumentation(xDocument);
await member.ReplaceInheritdocElementsAsync(element).ConfigureAwait(false);
return element;
} catch {
return null;
}
}
private static async Task<XDocument> TryGetXmlDocumentAsync(AssemblyName assemblyName, string pathToXmlFile)
{
if (!Cache.ContainsKey(assemblyName.FullName)) {
if (!(await DynamicApis.FileExistsAsync(pathToXmlFile).ConfigureAwait(false))) {
Cache[assemblyName.FullName] = null;
return null;
}
string fullName = assemblyName.FullName;
XDocument value = await Task.Factory.StartNew(() => XDocument.Load(pathToXmlFile, LoadOptions.PreserveWhitespace)).ConfigureAwait(false);
Cache[fullName] = value;
}
return Cache[assemblyName.FullName];
}
private static bool IgnoreAssembly(AssemblyName assemblyName)
{
if (Cache.ContainsKey(assemblyName.FullName) && Cache[assemblyName.FullName] == null)
return true;
return false;
}
private static XElement GetXmlDocumentation(this MemberInfo member, XDocument xml)
{
string memberElementName = GetMemberElementName(member);
return ((IEnumerable)DynamicApis.XPathEvaluate(xml, "/doc/members/member[@name='" + memberElementName + "']")).OfType<XElement>().FirstOrDefault();
}
private static async Task<XElement> GetXmlDocumentationAsync(this ParameterInfo parameter, XDocument xml)
{
string name = GetMemberElementName(parameter.Member);
IEnumerable source = (IEnumerable)DynamicApis.XPathEvaluate(xml, "/doc/members/member[@name='" + name + "']");
XElement element = source.OfType<XElement>().First();
await parameter.Member.ReplaceInheritdocElementsAsync(element).ConfigureAwait(false);
source = ((!parameter.IsRetval && !string.IsNullOrEmpty(parameter.Name)) ? ((IEnumerable)DynamicApis.XPathEvaluate(xml, "/doc/members/member[@name='" + name + "']/param[@name='" + parameter.Name + "']")) : ((IEnumerable)DynamicApis.XPathEvaluate(xml, "/doc/members/member[@name='" + name + "']/returns")));
return source.OfType<XElement>().FirstOrDefault();
}
private static async Task ReplaceInheritdocElementsAsync(this MemberInfo member, XElement element)
{
if (element != null) {
List<XNode> source = element.Nodes().ToList();
foreach (XElement item in source.OfType<XElement>()) {
if (item.Name.LocalName.ToLowerInvariant() == "inheritdoc") {
Type baseType = member.DeclaringType.GetTypeInfo().BaseType;
MemberInfo memberInfo = ((object)baseType != null) ? baseType.GetTypeInfo().DeclaredMembers.SingleOrDefault((MemberInfo m) => m.Name == member.Name) : null;
if (memberInfo != (MemberInfo)null) {
XElement xElement = await memberInfo.GetXmlDocumentationWithoutLockAsync().ConfigureAwait(false);
if (xElement != null)
item.ReplaceWith(xElement.Nodes().OfType<object>().ToArray());
else
await member.ProcessInheritdocInterfaceElementsAsync(item).ConfigureAwait(false);
} else
await member.ProcessInheritdocInterfaceElementsAsync(item).ConfigureAwait(false);
}
}
}
}
private static async Task ProcessInheritdocInterfaceElementsAsync(this MemberInfo member, XElement child)
{
foreach (Type implementedInterface in member.DeclaringType.GetTypeInfo().ImplementedInterfaces) {
MemberInfo memberInfo = ((object)implementedInterface != null) ? implementedInterface.GetTypeInfo().DeclaredMembers.SingleOrDefault((MemberInfo m) => m.Name == member.Name) : null;
if (memberInfo != (MemberInfo)null) {
XElement xElement = await memberInfo.GetXmlDocumentationWithoutLockAsync().ConfigureAwait(false);
if (xElement != null)
child.ReplaceWith(xElement.Nodes().OfType<object>().ToArray());
}
}
}
private static string RemoveLineBreakWhiteSpaces(string documentation)
{
if (string.IsNullOrEmpty(documentation))
return string.Empty;
documentation = "\n" + documentation.Replace("\r", string.Empty).Trim(new char[1] {
'\n'
});
string value = Regex.Match(documentation, "(\\n[ \\t]*)").Value;
documentation = documentation.Replace(value, "\n");
return documentation.Trim(new char[1] {
'\n'
});
}
private static string GetMemberElementName(dynamic member)
{
Type type;
dynamic val = ((object)(type = (member as Type)) != null && !string.IsNullOrEmpty(type.FullName)) ? type.FullName : (member.DeclaringType.FullName + "." + member.Name);
char c;
switch ((string)member.MemberType.ToString()) {
case "Constructor":
val = val.Replace(".ctor", "#ctor");
goto case "Method";
case "Method": {
c = 'M';
string text = string.Join(",", (from x in ((MethodBase)member).GetParameters()
select Regex.Replace(x.ParameterType.FullName, "(`[0-9]+)|(, .*?PublicKeyToken=[0-9a-z]*)", string.Empty).Replace("[[", "{").Replace("]]", "}")).ToArray());
if (!string.IsNullOrEmpty(text))
val += "(" + text + ")";
break;
}
case "Event":
c = 'E';
break;
case "Field":
c = 'F';
break;
case "NestedType":
val = val.Replace('+', '.');
goto case "TypeInfo";
case "TypeInfo":
c = 'T';
break;
case "Property":
c = 'P';
break;
default:
throw new ArgumentException("Unknown member type.", "member");
}
return (string)string.Format("{0}:{1}", c, val.Replace("+", "."));
}
private static async Task<string> GetXmlDocumentationPathAsync(dynamic assembly)
{
try {
if (assembly == null)
return null;
if (string.IsNullOrEmpty(assembly.Location))
return null;
dynamic assemblyName = assembly.GetName();
if (string.IsNullOrEmpty(assemblyName.Name))
return null;
if (Cache.ContainsKey(assemblyName.FullName))
return null;
string path4 = DynamicApis.PathGetDirectoryName((string)assembly.Location);
string path = DynamicApis.PathCombine(path4, (string)assemblyName.Name + ".xml");
if (await DynamicApis.FileExistsAsync(path).ConfigureAwait(false))
return path;
if (ReflectionExtensions.HasProperty(assembly, "CodeBase")) {
path = (string)DynamicApis.PathCombine(DynamicApis.PathGetDirectoryName(assembly.CodeBase.Replace("file:///", string.Empty)), assemblyName.Name + ".xml").Replace("file:\\", string.Empty);
if (await DynamicApis.FileExistsAsync(path).ConfigureAwait(false))
return path;
}
object value = Type.GetType("System.AppDomain").GetRuntimeProperty("CurrentDomain").GetValue(null);
if (value.HasProperty("BaseDirectory")) {
string baseDirectory = value.TryGetPropertyValue("BaseDirectory", "");
path = (string)DynamicApis.PathCombine(baseDirectory, assemblyName.Name + ".xml");
if (await DynamicApis.FileExistsAsync(path).ConfigureAwait(false))
return path;
return (string)DynamicApis.PathCombine(baseDirectory, "bin\\" + assemblyName.Name + ".xml");
}
return null;
} catch {
return null;
}
}
}
}