Initial version
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
using System.Reflection;
|
||||
using System.Xml.Linq;
|
||||
using EonaCat.DoxaApi.Attributes;
|
||||
using EonaCat.DoxaApi.Models;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
|
||||
namespace EonaCat.DoxaApi.Generation
|
||||
{
|
||||
public sealed class ApiDocumentGenerator
|
||||
{
|
||||
private readonly IReadOnlyList<ActionDescriptor> _actions;
|
||||
private readonly DoxaApiOptions _options;
|
||||
private readonly Dictionary<string, XmlDocReader?> _xmlReadersByAssembly = new();
|
||||
|
||||
public ApiDocumentGenerator(IReadOnlyList<ActionDescriptor> actions, DoxaApiOptions options)
|
||||
{
|
||||
_actions = actions;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public ApiDocument Generate()
|
||||
{
|
||||
var doc = new ApiDocument
|
||||
{
|
||||
Info = new ApiInfo
|
||||
{
|
||||
Title = _options.Title,
|
||||
Description = _options.Description,
|
||||
Version = _options.Version
|
||||
},
|
||||
Servers = _options.Servers.ToList()
|
||||
};
|
||||
|
||||
var schemaRegistry = new Dictionary<string, SchemaModel>();
|
||||
var schemaBuilder = new SchemaBuilder(schemaRegistry);
|
||||
var groups = new Dictionary<string, ApiGroup>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var action in _actions)
|
||||
{
|
||||
if (action is not ControllerActionDescriptor cad)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cad.MethodInfo.GetCustomAttribute<DoxaApiHiddenAttribute>() is not null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cad.ControllerTypeInfo.GetCustomAttribute<DoxaApiHiddenAttribute>() is not null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var groupName = ResolveGroupName(cad);
|
||||
if (!groups.TryGetValue(groupName, out var group))
|
||||
{
|
||||
group = new ApiGroup { Name = groupName };
|
||||
groups[groupName] = group;
|
||||
}
|
||||
|
||||
var endpoint = BuildEndpoint(cad, schemaBuilder);
|
||||
if (endpoint is not null)
|
||||
{
|
||||
group.Endpoints.Add(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
doc.Groups = groups.Values
|
||||
.OrderBy(g => g.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
foreach (var group in doc.Groups)
|
||||
{
|
||||
group.Endpoints = group.Endpoints
|
||||
.OrderBy(e => e.Path, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(e => MethodSortOrder(e.Method))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
doc.Schemas = schemaRegistry;
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
private static int MethodSortOrder(string method) => method switch
|
||||
{
|
||||
"GET" => 0,
|
||||
"POST" => 1,
|
||||
"PUT" => 2,
|
||||
"PATCH" => 3,
|
||||
"DELETE" => 4,
|
||||
_ => 5
|
||||
};
|
||||
|
||||
private string ResolveGroupName(ControllerActionDescriptor cad)
|
||||
{
|
||||
var methodAttr = cad.MethodInfo.GetCustomAttribute<DoxaApiGroupAttribute>();
|
||||
if (methodAttr is not null)
|
||||
{
|
||||
return methodAttr.Name;
|
||||
}
|
||||
|
||||
var classAttr = cad.ControllerTypeInfo.GetCustomAttribute<DoxaApiGroupAttribute>();
|
||||
if (classAttr is not null)
|
||||
{
|
||||
return classAttr.Name;
|
||||
}
|
||||
|
||||
var name = cad.ControllerName;
|
||||
return name;
|
||||
}
|
||||
|
||||
private ApiEndpoint? BuildEndpoint(ControllerActionDescriptor cad, SchemaBuilder schemaBuilder)
|
||||
{
|
||||
var httpMethod = cad.ActionConstraints?
|
||||
.OfType<Microsoft.AspNetCore.Mvc.ActionConstraints.HttpMethodActionConstraint>()
|
||||
.FirstOrDefault()?.HttpMethods.FirstOrDefault() ?? "GET";
|
||||
|
||||
var path = "/" + (cad.AttributeRouteInfo?.Template?.TrimStart('/') ?? cad.ActionName);
|
||||
|
||||
var xmlReader = GetXmlReader(cad.MethodInfo.DeclaringType!.Assembly);
|
||||
var xmlDoc = xmlReader?.GetMethodDoc(cad.MethodInfo);
|
||||
|
||||
var summaryAttr = cad.MethodInfo.GetCustomAttribute<DoxaApiSummaryAttribute>();
|
||||
var descAttr = cad.MethodInfo.GetCustomAttribute<DoxaApiDescriptionAttribute>();
|
||||
var exampleAttr = cad.MethodInfo.GetCustomAttribute<DoxaApiExampleAttribute>();
|
||||
var obsoleteAttr = cad.MethodInfo.GetCustomAttribute<ObsoleteAttribute>();
|
||||
|
||||
var endpoint = new ApiEndpoint
|
||||
{
|
||||
OperationId = $"{cad.ControllerName}_{cad.ActionName}",
|
||||
Method = httpMethod.ToUpperInvariant(),
|
||||
Path = path,
|
||||
Summary = summaryAttr?.Summary ?? xmlDoc?.Summary ?? HumanizeName(cad.ActionName),
|
||||
Description = descAttr?.Description ?? xmlDoc?.Remarks,
|
||||
Deprecated = obsoleteAttr is not null,
|
||||
Tags = new List<string> { ResolveGroupName(cad) }
|
||||
};
|
||||
|
||||
foreach (var param in cad.Parameters)
|
||||
{
|
||||
var bindingSource = param.BindingInfo?.BindingSource;
|
||||
var inLocation = bindingSource?.Id switch
|
||||
{
|
||||
"Path" => "path",
|
||||
"Query" => "query",
|
||||
"Header" => "header",
|
||||
"Body" => "body",
|
||||
"Form" => "form",
|
||||
_ => InferLocationFromPath(param.Name, path)
|
||||
};
|
||||
|
||||
if (inLocation == "body")
|
||||
{
|
||||
endpoint.RequestBody = new RequestBodyModel
|
||||
{
|
||||
Required = true,
|
||||
Schema = schemaBuilder.Build(param.ParameterType),
|
||||
Example = exampleAttr?.Json
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
var paramDoc = xmlDoc?.Params.GetValueOrDefault(param.Name);
|
||||
|
||||
endpoint.Parameters.Add(new ApiParameter
|
||||
{
|
||||
Name = param.Name,
|
||||
In = inLocation,
|
||||
Required = inLocation == "path" || IsRequiredParam(param),
|
||||
Description = paramDoc,
|
||||
Schema = schemaBuilder.Build(param.ParameterType)
|
||||
});
|
||||
}
|
||||
|
||||
var returnType = cad.MethodInfo.ReturnType;
|
||||
var responseSchema = schemaBuilder.Build(returnType);
|
||||
var successCode = httpMethod.ToUpperInvariant() switch
|
||||
{
|
||||
"POST" => "201",
|
||||
"DELETE" => "204",
|
||||
_ => "200"
|
||||
};
|
||||
|
||||
if (responseSchema.Type != "void")
|
||||
{
|
||||
endpoint.Responses.Add(new ResponseModel
|
||||
{
|
||||
StatusCode = successCode,
|
||||
Description = xmlDoc?.Returns ?? "Success",
|
||||
Schema = responseSchema
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
endpoint.Responses.Add(new ResponseModel
|
||||
{
|
||||
StatusCode = successCode,
|
||||
Description = "Success"
|
||||
});
|
||||
}
|
||||
|
||||
if (endpoint.Parameters.Any(p => p.Required) || endpoint.RequestBody is not null)
|
||||
{
|
||||
endpoint.Responses.Add(new ResponseModel { StatusCode = "400", Description = "Invalid request" });
|
||||
}
|
||||
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
private static bool IsRequiredParam(ParameterDescriptor param)
|
||||
{
|
||||
if (param is ControllerParameterDescriptor cpd)
|
||||
{
|
||||
var nullableUnderlying = Nullable.GetUnderlyingType(cpd.ParameterInfo.ParameterType);
|
||||
bool hasDefault = cpd.ParameterInfo.HasDefaultValue;
|
||||
bool isNullableType = nullableUnderlying is not null || !cpd.ParameterInfo.ParameterType.IsValueType;
|
||||
return !hasDefault && !isNullableType;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string InferLocationFromPath(string paramName, string path)
|
||||
{
|
||||
return path.Contains("{" + paramName + "}", StringComparison.OrdinalIgnoreCase) ? "path" : "query";
|
||||
}
|
||||
|
||||
private static string HumanizeName(string name)
|
||||
{
|
||||
|
||||
var chars = new List<char>();
|
||||
for (int i = 0; i < name.Length; i++)
|
||||
{
|
||||
if (i > 0 && char.IsUpper(name[i]) && !char.IsUpper(name[i - 1]))
|
||||
{
|
||||
chars.Add(' ');
|
||||
}
|
||||
|
||||
chars.Add(name[i]);
|
||||
}
|
||||
return new string(chars.ToArray());
|
||||
}
|
||||
|
||||
private XmlDocReader? GetXmlReader(Assembly assembly)
|
||||
{
|
||||
var key = assembly.GetName().Name ?? assembly.FullName ?? "unknown";
|
||||
if (_xmlReadersByAssembly.TryGetValue(key, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
var location = assembly.Location;
|
||||
if (string.IsNullOrEmpty(location))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var xmlPath = Path.ChangeExtension(location, ".xml");
|
||||
XmlDocReader? reader = null;
|
||||
if (File.Exists(xmlPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
reader = new XmlDocReader(XDocument.Load(xmlPath));
|
||||
}
|
||||
catch
|
||||
{
|
||||
reader = null;
|
||||
}
|
||||
}
|
||||
|
||||
_xmlReadersByAssembly[key] = reader;
|
||||
return reader;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace EonaCat.DoxaApi.Generation
|
||||
{
|
||||
public sealed class DoxaApiOptions
|
||||
{
|
||||
public string Title { get; set; } = "DoxaApi Documentation";
|
||||
public string? Description { get; set; }
|
||||
public string Version { get; set; } = "v1";
|
||||
public string RoutePrefix { get; set; } = "doxa";
|
||||
public List<string> Servers { get; set; } = new();
|
||||
public string Theme { get; set; } = "auto";
|
||||
public string AccentColor { get; set; } = "#6366f1";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using EonaCat.DoxaApi.Models;
|
||||
|
||||
namespace EonaCat.DoxaApi.Generation
|
||||
{
|
||||
public sealed class SchemaBuilder
|
||||
{
|
||||
private readonly Dictionary<string, SchemaModel> _registry;
|
||||
private readonly HashSet<Type> _inProgress = new();
|
||||
|
||||
public SchemaBuilder(Dictionary<string, SchemaModel> registry)
|
||||
{
|
||||
_registry = registry;
|
||||
}
|
||||
|
||||
public SchemaModel Build(Type type)
|
||||
{
|
||||
|
||||
type = UnwrapAsyncAndActionResult(type);
|
||||
|
||||
var underlying = Nullable.GetUnderlyingType(type);
|
||||
bool nullable = underlying is not null;
|
||||
if (underlying is not null)
|
||||
{
|
||||
type = underlying;
|
||||
}
|
||||
|
||||
var schema = BuildCore(type);
|
||||
schema.Nullable = nullable || schema.Nullable;
|
||||
return schema;
|
||||
}
|
||||
|
||||
private static Type UnwrapAsyncAndActionResult(Type type)
|
||||
{
|
||||
if (type == typeof(void))
|
||||
{
|
||||
return type;
|
||||
}
|
||||
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
var def = type.GetGenericTypeDefinition();
|
||||
if (def == typeof(Task<>) || def.Name.StartsWith("ValueTask`"))
|
||||
{
|
||||
return UnwrapAsyncAndActionResult(type.GetGenericArguments()[0]);
|
||||
}
|
||||
|
||||
if (def.Name is "ActionResult`1")
|
||||
{
|
||||
return UnwrapAsyncAndActionResult(type.GetGenericArguments()[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (type == typeof(Task))
|
||||
{
|
||||
return typeof(void);
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
private SchemaModel BuildCore(Type type)
|
||||
{
|
||||
if (type == typeof(void))
|
||||
{
|
||||
return new SchemaModel { Type = "void" };
|
||||
}
|
||||
|
||||
if (type == typeof(string) || type == typeof(char))
|
||||
{
|
||||
return new SchemaModel { Type = "string" };
|
||||
}
|
||||
|
||||
if (type == typeof(bool))
|
||||
{
|
||||
return new SchemaModel { Type = "boolean" };
|
||||
}
|
||||
|
||||
if (type == typeof(byte) || type == typeof(sbyte) || type == typeof(short) ||
|
||||
type == typeof(ushort) || type == typeof(int) || type == typeof(uint) ||
|
||||
type == typeof(long) || type == typeof(ulong))
|
||||
{
|
||||
return new SchemaModel { Type = "integer", Format = type.Name.ToLowerInvariant() };
|
||||
}
|
||||
|
||||
if (type == typeof(float) || type == typeof(double) || type == typeof(decimal))
|
||||
{
|
||||
return new SchemaModel { Type = "number", Format = type.Name.ToLowerInvariant() };
|
||||
}
|
||||
|
||||
if (type == typeof(DateTime) || type == typeof(DateTimeOffset))
|
||||
{
|
||||
return new SchemaModel { Type = "string", Format = "date-time" };
|
||||
}
|
||||
|
||||
if (type == typeof(DateOnly))
|
||||
{
|
||||
return new SchemaModel { Type = "string", Format = "date" };
|
||||
}
|
||||
|
||||
if (type == typeof(TimeOnly) || type == typeof(TimeSpan))
|
||||
{
|
||||
return new SchemaModel { Type = "string", Format = "time" };
|
||||
}
|
||||
|
||||
if (type == typeof(Guid))
|
||||
{
|
||||
return new SchemaModel { Type = "string", Format = "uuid" };
|
||||
}
|
||||
|
||||
if (type == typeof(Uri))
|
||||
{
|
||||
return new SchemaModel { Type = "string", Format = "uri" };
|
||||
}
|
||||
|
||||
if (type == typeof(object))
|
||||
{
|
||||
return new SchemaModel { Type = "object" };
|
||||
}
|
||||
|
||||
if (type.IsEnum)
|
||||
{
|
||||
return new SchemaModel
|
||||
{
|
||||
Type = "enum",
|
||||
EnumValues = Enum.GetNames(type).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
if (IsDictionary(type, out var valueType))
|
||||
{
|
||||
return new SchemaModel
|
||||
{
|
||||
Type = "object",
|
||||
Items = Build(valueType!)
|
||||
};
|
||||
}
|
||||
|
||||
var elementType = GetEnumerableElementType(type);
|
||||
if (elementType is not null)
|
||||
{
|
||||
return new SchemaModel
|
||||
{
|
||||
Type = "array",
|
||||
Items = Build(elementType)
|
||||
};
|
||||
}
|
||||
|
||||
var refName = FriendlyTypeName(type);
|
||||
|
||||
if (_registry.ContainsKey(refName))
|
||||
{
|
||||
return new SchemaModel { Type = "object", RefName = refName };
|
||||
}
|
||||
|
||||
if (_inProgress.Contains(type))
|
||||
{
|
||||
return new SchemaModel { Type = "object", RefName = refName };
|
||||
}
|
||||
|
||||
_inProgress.Add(type);
|
||||
|
||||
var props = new Dictionary<string, SchemaModel>();
|
||||
var required = new List<string>();
|
||||
|
||||
foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
if (prop.GetIndexParameters().Length > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!prop.CanRead)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var propSchema = Build(prop.PropertyType);
|
||||
props[ToCamelCase(prop.Name)] = propSchema;
|
||||
|
||||
if (!propSchema.Nullable && Nullable.GetUnderlyingType(prop.PropertyType) is null
|
||||
&& (prop.PropertyType.IsValueType || IsNonNullableReferenceProperty(prop)))
|
||||
{
|
||||
required.Add(ToCamelCase(prop.Name));
|
||||
}
|
||||
}
|
||||
|
||||
_registry[refName] = new SchemaModel
|
||||
{
|
||||
Type = "object",
|
||||
Properties = props,
|
||||
Required = required.Count > 0 ? required : null
|
||||
};
|
||||
|
||||
_inProgress.Remove(type);
|
||||
|
||||
return new SchemaModel { Type = "object", RefName = refName };
|
||||
}
|
||||
|
||||
private static bool IsNonNullableReferenceProperty(PropertyInfo prop)
|
||||
{
|
||||
|
||||
try
|
||||
{
|
||||
var ctx = new NullabilityInfoContext();
|
||||
var info = ctx.Create(prop);
|
||||
return info.ReadState == NullabilityState.NotNull;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Type? GetEnumerableElementType(Type type)
|
||||
{
|
||||
if (type == typeof(string))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type.IsArray)
|
||||
{
|
||||
return type.GetElementType();
|
||||
}
|
||||
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
var def = type.GetGenericTypeDefinition();
|
||||
if (def == typeof(List<>) || def == typeof(IList<>) || def == typeof(ICollection<>) ||
|
||||
def == typeof(IEnumerable<>) || def == typeof(IReadOnlyList<>) || def == typeof(IReadOnlyCollection<>) ||
|
||||
def == typeof(HashSet<>) || def == typeof(Queue<>) || def == typeof(Stack<>))
|
||||
{
|
||||
return type.GetGenericArguments()[0];
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var iface in type.GetInterfaces())
|
||||
{
|
||||
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
||||
{
|
||||
return iface.GetGenericArguments()[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string))
|
||||
{
|
||||
return typeof(object);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsDictionary(Type type, out Type? valueType)
|
||||
{
|
||||
valueType = null;
|
||||
if (!type.IsGenericType)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var def = type.GetGenericTypeDefinition();
|
||||
if (def == typeof(Dictionary<,>) || def == typeof(IDictionary<,>) || def == typeof(IReadOnlyDictionary<,>))
|
||||
{
|
||||
valueType = type.GetGenericArguments()[1];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string FriendlyTypeName(Type type)
|
||||
{
|
||||
if (!type.IsGenericType)
|
||||
{
|
||||
return type.Name;
|
||||
}
|
||||
|
||||
var name = type.Name[..type.Name.IndexOf('`')];
|
||||
var args = string.Join(",", type.GetGenericArguments().Select(FriendlyTypeName));
|
||||
return $"{name}<{args}>";
|
||||
}
|
||||
|
||||
private static string ToCamelCase(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name) || char.IsLower(name[0]))
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
return char.ToLowerInvariant(name[0]) + name[1..];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace EonaCat.DoxaApi.Generation
|
||||
{
|
||||
internal sealed class MethodXmlDoc
|
||||
{
|
||||
public string? Summary { get; set; }
|
||||
public string? Remarks { get; set; }
|
||||
public string? Returns { get; set; }
|
||||
public Dictionary<string, string> Params { get; } = new();
|
||||
}
|
||||
|
||||
internal sealed class XmlDocReader
|
||||
{
|
||||
private readonly Dictionary<string, MethodXmlDoc> _members = new();
|
||||
|
||||
public XmlDocReader(XDocument document)
|
||||
{
|
||||
var members = document.Root?.Element("members")?.Elements("member");
|
||||
if (members is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var member in members)
|
||||
{
|
||||
var name = member.Attribute("name")?.Value;
|
||||
if (name is null || !name.StartsWith("M:"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var doc = new MethodXmlDoc
|
||||
{
|
||||
Summary = CleanText(member.Element("summary")?.Value),
|
||||
Remarks = CleanText(member.Element("remarks")?.Value),
|
||||
Returns = CleanText(member.Element("returns")?.Value)
|
||||
};
|
||||
|
||||
foreach (var paramEl in member.Elements("param"))
|
||||
{
|
||||
var pName = paramEl.Attribute("name")?.Value;
|
||||
if (pName is not null)
|
||||
{
|
||||
doc.Params[pName] = CleanText(paramEl.Value) ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
_members[name] = doc;
|
||||
}
|
||||
}
|
||||
|
||||
public MethodXmlDoc? GetMethodDoc(MethodInfo method)
|
||||
{
|
||||
var key = BuildMemberKey(method);
|
||||
return _members.GetValueOrDefault(key);
|
||||
}
|
||||
|
||||
private static string? CleanText(string? raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var lines = raw.Split('\n').Select(l => l.Trim());
|
||||
return string.Join(" ", lines).Trim();
|
||||
}
|
||||
|
||||
private static string BuildMemberKey(MethodInfo method)
|
||||
{
|
||||
var sb = new StringBuilder("M:");
|
||||
sb.Append(method.DeclaringType!.FullName!.Replace('+', '.'));
|
||||
sb.Append('.');
|
||||
sb.Append(method.Name);
|
||||
|
||||
var parameters = method.GetParameters();
|
||||
if (parameters.Length > 0)
|
||||
{
|
||||
sb.Append('(');
|
||||
sb.Append(string.Join(",", parameters.Select(p => XmlTypeName(p.ParameterType))));
|
||||
sb.Append(')');
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string XmlTypeName(Type type)
|
||||
{
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
var def = type.GetGenericTypeDefinition();
|
||||
var name = def.FullName![..def.FullName!.IndexOf('`')];
|
||||
var args = string.Join(",", type.GetGenericArguments().Select(XmlTypeName));
|
||||
return $"{name}{{{args}}}";
|
||||
}
|
||||
|
||||
return type.FullName?.Replace('+', '.') ?? type.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user