问题
I'm using xml comments to document public as well as internal and private members of my components. I would like to package the generated documentation xml files with component assemblies in order to enable "rich" (e.g. with method, exceptions and parameter descriptions) Visual Studio Intellisense with the end product. The problem with it is that the C# compiler creates documentation entries for everything (including internal classes, methods, private fields of internal enums etc.) and there seems to be no switch to "only public members" mode.
Now I don't want to go over 50 files with XX methods in each and remove all comments for private and internal members. Even if I did that, I probably would not have much success with auto-gen'd resource files, because these strongly-typed resource classes are automatically commented and non-public.
My question is: is there some option/flag that I'm overlooking? If no, are there some tools that could help separate public members from the rest (before I start to code one)?
回答1:
SandCastle Help File Builder has an option to recreate the xml files containing only the configured access modes for methods, properties, etc...
The only "downside" is that you'll have to generate a documentation.
EDIT
Since it's been a long time ago I forgot that I added a "component" to SHFB to generate the XML.
The good news is that this component is included in SHFB.
You have to add the "Intellisense Component" to the SHFB project. It will then generate the XML according to the configured SHFB project.
For more info: Intellisense Component in SHFB
回答2:
There is a tool inside the eazfuscator that can remove non-public documentation. You can see an example here
回答3:
I've given this some thought and I decided to change the way I go about solving this particular problem. Instead of finding type/member within the assembly trying to parse the XML documentation notation. I decided to simply build a string set (of XML documentation notation) for the public API that can then be used to tes wheter a member isn't public.
It's really simple. Send an assembly to the XmlDocumentationStringSet
and it will build a string set of the public API and delete the elements that aren't public.
static void Main(string[] args)
{
var el = XElement.Load("ConsoleApplication18.XML");
// obviously, improve this if necessary (might not work like this if DLL isn't already loaded)
// you can use file paths
var assemblyName = el.Descendants("assembly").FirstOrDefault();
var assembly = Assembly.ReflectionOnlyLoad(assemblyName.Value);
var stringSet = new XmlDocumentationStringSet(assembly);
foreach (var member in el.Descendants("member").ToList()) // .ToList enables removing while traversing
{
var attr = member.Attribute("name");
if (attr == null)
{
continue;
}
if (!stringSet.Contains(attr.Value))
{
member.Remove();
}
}
el.Save("ConsoleApplication18-public.XML");
}
And here's the class that builds the XML documentation names (it's a bit large but I thought I post the entire source here anyway):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
namespace ConsoleApplication18
{
public class XmlDocumentationStringSet : IEnumerable<string>
{
private HashSet<string> stringSet = new HashSet<string>(StringComparer.Ordinal);
public XmlDocumentationStringSet(Assembly assembly)
{
AddRange(assembly.GetExportedTypes());
}
public bool Contains(string name)
{
return stringSet.Contains(name);
}
/// <summary>
/// Heelloasdasdasd
/// </summary>
/// <param name="types"></param>
public void AddRange(IEnumerable<Type> types)
{
foreach (var type in types)
{
Add(type);
}
}
public void Add(Type type)
{
// Public API only
if (!type.IsVisible)
{
return;
}
var members = type.GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
foreach (var member in members)
{
Add(type, member);
}
}
StringBuilder sb = new StringBuilder();
private void Add(Type type, MemberInfo member)
{
Type nestedType = null;
sb.Length = 0;
switch (member.MemberType)
{
case MemberTypes.Constructor:
sb.Append("M:");
AppendConstructor(sb, (ConstructorInfo)member);
break;
case MemberTypes.Event:
sb.Append("E:");
AppendEvent(sb, (EventInfo)member);
break;
case MemberTypes.Field:
sb.Append("F:");
AppendField(sb, (FieldInfo)member);
break;
case MemberTypes.Method:
sb.Append("M:");
AppendMethod(sb, (MethodInfo)member);
break;
case MemberTypes.NestedType:
nestedType = (Type)member;
if (IsVisible(nestedType))
{
sb.Append("T:");
AppendNestedType(sb, (Type)member);
}
break;
case MemberTypes.Property:
sb.Append("P:");
AppendProperty(sb, (PropertyInfo)member);
break;
}
if (sb.Length > 0)
{
stringSet.Add(sb.ToString());
}
if (nestedType != null)
{
Add(nestedType);
}
}
private bool IsVisible(Type nestedType)
{
return nestedType.IsVisible;
}
private void AppendProperty(StringBuilder sb, PropertyInfo propertyInfo)
{
if (!IsVisible(propertyInfo))
{
sb.Length = 0;
return;
}
AppendType(sb, propertyInfo.DeclaringType);
sb.Append('.').Append(propertyInfo.Name);
}
private bool IsVisible(PropertyInfo propertyInfo)
{
var getter = propertyInfo.GetGetMethod();
var setter = propertyInfo.GetSetMethod();
return (getter != null && IsVisible(getter)) || (setter != null && IsVisible(setter));
}
private void AppendNestedType(StringBuilder sb, Type type)
{
AppendType(sb, type.DeclaringType);
}
private void AppendMethod(StringBuilder sb, MethodInfo methodInfo)
{
if (!IsVisible(methodInfo) || (methodInfo.IsHideBySig && methodInfo.IsSpecialName))
{
sb.Length = 0;
return;
}
AppendType(sb, methodInfo.DeclaringType);
sb.Append('.').Append(methodInfo.Name);
AppendParameters(sb, methodInfo.GetParameters());
}
private bool IsVisible(MethodInfo methodInfo)
{
return methodInfo.IsFamily || methodInfo.IsPublic;
}
private void AppendParameters(StringBuilder sb, ParameterInfo[] parameterInfo)
{
if (parameterInfo.Length == 0)
{
return;
}
sb.Append('(');
for (int i = 0; i < parameterInfo.Length; i++)
{
if (i > 0)
{
sb.Append(',');
}
var p = parameterInfo[i];
AppendType(sb, p.ParameterType);
}
sb.Append(')');
}
private void AppendField(StringBuilder sb, FieldInfo fieldInfo)
{
if (!IsVisible(fieldInfo))
{
sb.Length = 0;
return;
}
AppendType(sb, fieldInfo.DeclaringType);
sb.Append('.').Append(fieldInfo.Name);
}
private bool IsVisible(FieldInfo fieldInfo)
{
return fieldInfo.IsFamily || fieldInfo.IsPublic;
}
private void AppendEvent(StringBuilder sb, EventInfo eventInfo)
{
if (!IsVisible(eventInfo))
{
sb.Length = 0;
return;
}
AppendType(sb, eventInfo.DeclaringType);
sb.Append('.').Append(eventInfo.Name);
}
private bool IsVisible(EventInfo eventInfo)
{
return true; // hu?
}
private void AppendConstructor(StringBuilder sb, ConstructorInfo constructorInfo)
{
if (!IsVisible(constructorInfo))
{
sb.Length = 0;
return;
}
AppendType(sb, constructorInfo.DeclaringType);
sb.Append('.').Append("#ctor");
AppendParameters(sb, constructorInfo.GetParameters());
}
private bool IsVisible(ConstructorInfo constructorInfo)
{
return constructorInfo.IsFamily || constructorInfo.IsPublic;
}
private void AppendType(StringBuilder sb, Type type)
{
if (type.DeclaringType != null)
{
AppendType(sb, type.DeclaringType);
sb.Append('.');
}
else if (!string.IsNullOrEmpty(type.Namespace))
{
sb.Append(type.Namespace);
sb.Append('.');
}
sb.Append(type.Name);
if (type.IsGenericType && !type.IsGenericTypeDefinition)
{
// Remove "`1" suffix from type name
while (char.IsDigit(sb[sb.Length - 1]))
sb.Length--;
sb.Length--;
{
var args = type.GetGenericArguments();
sb.Append('{');
for (int i = 0; i < args.Length; i++)
{
if (i > 0)
{
sb.Append(',');
}
AppendType(sb, args[i]);
}
sb.Append('}');
}
}
}
public IEnumerator<string> GetEnumerator()
{
return stringSet.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
Oh, and I haven't figured out how to handle events yet, they are always visible in this example.
回答4:
@John Leidegren
I've got the same requirement, and I've found the answer to the missing bit of your code. An Event has 2 methods, Add and Remove, and is considered public if either of them is public. So it would be something like:
private bool IsVisible(EventInfo eventInfo)
{
return eventInfo.GetAddMethod(false) != null
|| eventInfo.GetRemoveMethod(false) != null;
}
although I can't think of any reason why one would be public and not the other.
回答5:
What tool are you using to generate the documentation? I use Sandcastle and that gives you the option to select members to include by accessability.
In general it seems expected that the XML would contain all info that could be needed and its up to the processing tool to select from it what is needed.
回答6:
I was facing the same problem. SHFB is slow as hell and since we have another documentation code-base we did not need it to generate the documentation for us.
I ended up using XMLStarlet plus a separate namespace for internal classes. For example, all my internal classes would reside in MyCompany.MyProduct.Internal
. Then I can use one simple command
xml ed -L -d "//member[contains(@name, 'MyCompany.MyProduct.Internal')]" MyProduct.xml
to cleanse the XML. This of course is not bullet-proof -- it does not cover internal members in public classes, and it does require some discipline to remember to put internal classes into internal. But this is the cleanest and least intrusive method which works for me. It is also a standalone EXE file easily checked into build server, no sweat.
来源:https://stackoverflow.com/questions/624466/extract-xml-comments-for-public-members-only