-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Bug]: Description of a property shows the summary of the underlying class instead of the property summary #3240
Comments
I have a very vague memory of noticing this before I was a maintainer of Swashbuckle, and digging into it and finding it was something to do with how the internals of Microsoft.OpenApi works, but I could be misremembering. |
@martincostello I'm surprise it has not been reported before (at least, I couldn't find any issues with a similar description). Seems like an important omission. |
@alekdavis it seems that it was reported and fixed see #2379. |
@jgarciadelanoceda Ah, sweet, I added a call to Thanks a lot. I have not seen this one mentioned anywhere, so didn't know it was needed. |
Ugh, it worked in the sample project, but not in my actual app. Not sure what the difference is. Here is my code snippet: // The <inheritdoc/> filter only applies to properties.
logger.Information("Including XML comments from inherited documents.");
options.IncludeXmlCommentsFromInheritDocs(includeRemarks: true);
options.UseAllOfToExtendReferenceSchemas();
// Load XML documentation into Swagger.
// https://github.com/domaindrivendev/Swashbuckle.WebApi/issues/93
List<string> xmlFiles = [.. Directory.GetFiles(AppContext.BaseDirectory,"*.xml",SearchOption.TopDirectoryOnly)];
logger.Information("Reading documentation files:");
if (xmlFiles != null && xmlFiles.Count > 0)
{
xmlFiles.ForEach(xmlFile =>
{
logger.Information("- {xmlFile}", Path.GetFileName(xmlFile));
XDocument xmlDoc = XDocument.Load(xmlFile);
options.IncludeXmlComments(() => new XPathDocument(xmlDoc.CreateReader()), true);
// options.UseAllOfToExtendReferenceSchemas();
options.SchemaFilter<DescribeEnumMembers>(xmlDoc);
});
} I tried adding the call for each doc file (commented out in the sample), but it didn't help. Not sure what I'm doing wrong. |
Please provide a minimal example, if it's possible a gh repo with the minimal things possible |
Thanks @jgarciadelanoceda. I am totally puzzled: my minimal app works fine, my actual app does not, but the actual app uses a common library for setting up Swagger and another demo app that uses this library also works fine. There is absolutely no difference in Swagger setting code between the two (except they get different inputs and XML sources). I spent a couple hours and no matter what I tried, I cannot figure why common code in one case has the problem and in another one does not. Since it's an actual app, I cannot share the code, but here are some snippets, maybe they can give some hints. My complex object summary: /// <summary>
/// Holds properties and methods that may be needed for specific operations on the object,
/// such as REST PATCH calls.
/// </summary>
public class Meta XML file for the object docs (common library) produces (with namespace redacted): <member name="T:XXX.XXX.Common.Metadata.Meta">
<summary> Holds properties and methods that may be needed for specific operations on the object, such as REST PATCH calls. </summary>
</member> The property summary that uses Meta: /// <summary>
/// MUST REPLACE THE META SUMMARY.
/// </summary>
[JsonProperty("meta", Order = -100000)]
public virtual Meta? Meta { get; set; } XML file for the property (it's in the app file) produces (with namespace redacted): <member name="P:XXX.XXX.XXX.XXX.Entitlement.Meta">
<summary> MUST REPLACE THE META SUMMARY. </summary>
</member> The shared Swagger initialization logic: /// <summary>
/// Implements extension methods invoked on startup.
/// </summary>
public static class ServiceCollectionSwaggerExtensions {
/// <summary>
/// Initializes Swagger configuration.
/// </summary>
/// <param name="services">
/// Exposed application services.
/// </param>
/// <param name="logger">
/// Logger.
/// </param>
/// <param name="environment">
/// Used to check the deployment environment and for local debugging so these:
/// (1) Add the localhost server URL so you can debug locally
/// (2) Replace all OAuth flows with implicit so you can use SwaggerUI
/// </param>
/// <param name="apiTenantId">
/// ID of the Azure tenant hosting the API (OAuth provider).
/// </param>
/// <param name="apiClientId">
/// ID of the API's service principal in Azure.
/// </param>
/// <param name="exampleTypes">
/// Data types from assemblies that need to be included in the Swagger examples.
/// If not specified, all types implementing the IExamplesProvider or IMultipleExamplesProvider
/// interface will be included.
/// </param>
/// <param name="versionLabel">
/// The version label of the API, such as 'v1'.
/// </param>
/// <param name="title">
/// The API title (pass <c>null</c> to get it from the assembly Product attribute).
/// </param>
/// <param name="version">
/// The API version (pass <c>null</c> to get it from the assembly Version attribute).
/// </param>
/// <param name="description">
/// The API description (pass <c>null</c> to get it from the assembly Description attribute).
/// </param>
/// <param name="contactName">
/// Name of the contact for the Swagger documentation.
/// </param>
/// <param name="contactUrl">
/// The contact URL for the Swagger documentation.
/// </param>
/// <param name="contactEmail">
/// The contact email for the Swagger documentation.
/// </param>
/// <param name="serverUrl">
/// The server URL to be included in the drop-down box (can be <c>null</c>).
/// </param>
/// <param name="scopes">
/// The list of all supported scopes.
/// </param>
public static void ConfigureSwagger
(
this IServiceCollection services,
Serilog.ILogger logger,
IWebHostEnvironment environment,
string apiTenantId,
string apiClientId,
Type[]? exampleTypes,
string versionLabel /* v1 */,
string? title,
string? version,
string? description,
string contactName,
string contactUrl,
string contactEmail,
string? serverUrl,
string[] scopes
)
{
logger.Information("Started Swagger initialization.");
if (exampleTypes == null || exampleTypes.Length == 0)
{
IEnumerable<Type> types = GetExampleTypes();
if (types != null)
{
exampleTypes = types.ToArray();
}
}
if (exampleTypes == null || exampleTypes.Length == 0)
{
logger.Information("No Swagger example types found in the running application.");
}
else
{
logger.Information(
"Adding examples for types:");
for (int i = 0; i < exampleTypes.Length; i++)
{
logger.Information(
"- {exampleType}", exampleTypes[i]);
}
}
_ = services.AddSwaggerExamplesFromAssemblyOf(exampleTypes);
logger.Information("Generating documentation.");
_ = services.AddSwaggerGen(
options =>
{
logger.Information("Initializing documentation.");
options.SwaggerDoc(versionLabel,
new OpenApiInfo
{
Title = string.IsNullOrEmpty(title) ? AssemblyInfo.Product : title,
Version = string.IsNullOrEmpty(version) ? AssemblyInfo.Version : version,
Description = string.IsNullOrEmpty(description) ? AssemblyInfo.Description : description,
Contact = new OpenApiContact()
{
Name = contactName,
Url = new Uri(contactUrl),
Email = contactEmail
},
}
);
// Necessary for including annotations from SwaggerResponse attributes in Swagger documentation.
logger.Information("Enabling annotations.");
options.EnableAnnotations();
// See "Add custom serializer to Swagger in my .Net core API":
// https://stackoverflow.com/questions/59902076/add-custom-serializer-to-swagger-in-my-net-core-api#answer-64812850
logger.Information("Using example filters.");
options.ExampleFilters();
// See "Swagger Swashbuckle Asp.NET Core: show details about every enum is used":
// https://stackoverflow.com/questions/65312198/swagger-swashbuckle-asp-net-core-show-details-about-every-enum-is-used#answer-65318486
logger.Information("Using schema filters for enum values.");
options.SchemaFilter<EnumSchemaFilter>();
// Add localhost first because that's the default when debugging.
// See "How Do You Access the `applicationUrl` Property Found in launchSettings.json from Asp.NET Core 3.1 Startup class?":
// https://stackoverflow.com/questions/59398439/how-do-you-access-the-applicationurl-property-found-in-launchsettings-json-fro#answer-60489767
if (environment.IsDevelopment())
{
logger.Information("Adding localhost servers:");
string[]? localHosts = System.Environment.GetEnvironmentVariable("ASPNETCORE_URLS")?.Split(";");
if (localHosts != null && localHosts.Length > 0)
{
foreach (string localHost in localHosts)
{
logger.Information($"- {localHost}");
options.AddServer(new OpenApiServer() { Url = localHost });
}
}
}
if (!string.IsNullOrEmpty(serverUrl))
{
logger.Information("Adding server:");
logger.Information("- {serverUrl}", serverUrl);
options.AddServer(new OpenApiServer() { Url = serverUrl });
}
Uri? authorizationUrl = null;
Uri? tokenUrl = null;
OpenApiOAuthFlow? authorizationCodeFlow = null;
OpenApiOAuthFlow? clientCredentialsFlow = null;
OpenApiOAuthFlow? implicitFlow = null;
if (!string.IsNullOrEmpty(apiTenantId))
{
logger.Information("Setting authorization URL:");
authorizationUrl = new Uri($"https://login.microsoftonline.com/{apiTenantId}/oauth2/v2.0/authorize");
logger.Information("- {authorizationUrl}", authorizationUrl);
logger.Information("Setting token URL:");
tokenUrl = new Uri($"https://login.microsoftonline.com/{apiTenantId}/oauth2/v2.0/token");
logger.Information("- {tokenUrl}", tokenUrl);
}
Dictionary<string, string> userScopes = [];
if (!string.IsNullOrEmpty(apiClientId))
{
if (scopes != null && scopes.Length > 0)
{
foreach (string scope in scopes)
{
string scopeFqn = Scope.ToFullyQualifiedName(apiClientId, scope);
if (userScopes.ContainsKey(scopeFqn))
{
continue;
}
userScopes.Add(scopeFqn, scope);
}
}
string defaultScope = ".default";
string defaultScopeFqn = Scope.ToFullyQualifiedName(apiClientId, defaultScope);
userScopes.TryAdd(defaultScopeFqn, defaultScope);
}
Dictionary<string, string> clientScopes = [];
if (string.IsNullOrEmpty(apiClientId))
{
clientScopes.Add(Scope.ToFullyQualifiedName(apiClientId, ".default"), "All assigned roles");
}
if (authorizationUrl != null)
{
logger.Information("Setting up authorization code flow for user scopes.");
authorizationCodeFlow = new OpenApiOAuthFlow()
{
AuthorizationUrl = authorizationUrl,
TokenUrl = tokenUrl,
Scopes = userScopes
};
logger.Information("Setting up implicit flow for user scopes.");
implicitFlow = new OpenApiOAuthFlow()
{
AuthorizationUrl = authorizationUrl,
Scopes = userScopes
};
}
if (tokenUrl != null)
{
logger.Information("Setting up client credential flow for client scopes.");
clientCredentialsFlow = new OpenApiOAuthFlow()
{
TokenUrl = tokenUrl,
Scopes = clientScopes
};
}
// OAuth authentication scheme:
// 'oauth2' is needed for Apigee to work on localhost.
// 'oauth2ClientCreds' is required by Apigee.
string securityScheme = environment.IsDevelopment() ?
"oauth2" : "oauth2";
// "oauth2" : "oauth2ClientCreds";
logger.Information("Adding security definitions for the OAuth flows.");
options.AddSecurityDefinition(securityScheme, new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows()
{
AuthorizationCode = environment.IsDevelopment() ? null : authorizationCodeFlow,
ClientCredentials = environment.IsDevelopment() ? null : clientCredentialsFlow,
Implicit = environment.IsDevelopment() ? implicitFlow : null
}
});
logger.Information("Adding OpenAPI security requirement.");
options.AddSecurityRequirement(
// OpenApiSecurityRequirement extends Dictionary.
// This code is using hash table initialization.
// That's why there are so many curly braces.
// We're creating a hash table with only one key/value pair.
new OpenApiSecurityRequirement() {
{
// This is the Dictionary Key
new OpenApiSecurityScheme {
Reference = new OpenApiReference {
Type = ReferenceType.SecurityScheme,
Id = "oauth2"
},
Scheme = "oauth2",
Name = "oauth2",
In = ParameterLocation.Header
},
// This is the dictionary value.
new List<string>()
}
});
// The <inheritdoc/> filter only applies to properties.
logger.Information("Including XML comments from inherited documents.");
options.UseAllOfToExtendReferenceSchemas();
options.IncludeXmlCommentsFromInheritDocs(includeRemarks: true);
// Load XML documentation into Swagger.
// https://github.com/domaindrivendev/Swashbuckle.WebApi/issues/93
List<string> xmlFiles = [.. Directory.GetFiles(AppContext.BaseDirectory,"*.xml",SearchOption.TopDirectoryOnly)];
logger.Information("Reading documentation files:");
if (xmlFiles != null && xmlFiles.Count > 0)
{
xmlFiles.ForEach(xmlFile =>
{
logger.Information("- {xmlFile}", Path.GetFileName(xmlFile));
XDocument xmlDoc = XDocument.Load(xmlFile);
options.UseAllOfToExtendReferenceSchemas();
options.IncludeXmlComments(() => new XPathDocument(xmlDoc.CreateReader()), true);
options.SchemaFilter<DescribeEnumMembers>(xmlDoc);
});
}
// Apparently, there are bugs in Swashbuckle or Swagger that cause issues with enum handling.
// Got this workaround from:
// https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1329#issuecomment-566914371
logger.Information("Implementing a workaround for the enum type handling bug.");
foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (Type t in a.GetTypes())
{
if (t.IsEnum)
{
options.MapType(t, () => new OpenApiSchema
{
Type = "string",
Enum = t.GetEnumNames().Select(
name => new OpenApiString(name)).Cast<IOpenApiAny>().ToList(),
Nullable = true
});
}
}
}
// Order controllers and endpoints alphabetically. From:
// https://stackoverflow.com/questions/46339078/how-can-i-change-order-the-operations-are-listed-in-a-group-in-swashbuckle
logger.Information("Sorting controllers and endpoints.");
options.OrderActionsBy(
(apiDesc) => $"{apiDesc.ActionDescriptor.RouteValues["controller"]}_{apiDesc.RelativePath?.ToLower()}_{apiDesc.HttpMethod?.ToLower()}"
);
}
);
logger.Information("Adding Newtonsoft JSON.NET support to Swagger.");
// https://stackoverflow.com/questions/68337082/swagger-ui-is-not-using-newtonsoft-json-to-serialize-decimal-instead-using-syst
// https://github.com/domaindrivendev/Swashbuckle.AspNetCore?tab=readme-ov-file#systemtextjson-stj-vs-newtonsoft
services.AddSwaggerGenNewtonsoftSupport(); // explicit opt-in - needs to be placed after AddSwaggerGen()
// Fix for controller sort order from https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2772
logger.Information("Implementing a fix for the top-level controller sort order.");
services.Configure<SwaggerUIOptions>(options =>
{
// Controller or tag level (top level group in UI)
options.ConfigObject.AdditionalItems["tagsSorter"] = "alpha";
// Within a controller, operations are the endpoints. You may not need this one
options.ConfigObject.AdditionalItems["operationsSorter"] = "alpha";
});
logger.Information("Completed Swagger initialization.");
}
/// <summary>
/// Returns all types implementing example interfaces loaded by the running application.
/// </summary>
/// <returns>
/// Collection of types.
/// </returns>
private static IEnumerable<Type> GetExampleTypes()
{
return AppDomain.CurrentDomain.GetAssemblies()
.Where(a => a.IsDynamic == false)
.SelectMany(a => a.GetTypes())
.Where(t => t.GetInterfaces()
.Any(i => i.IsGenericType &&
(i.GetGenericTypeDefinition() == typeof(IExamplesProvider<>) ||
i.GetGenericTypeDefinition() == typeof(IMultipleExamplesProvider<>))));
}
} I am out of ideas at this point because, as I mentioned, the same common logic produces another demo with no issues, so there must be something about my actual project and I'm not sure what it could be. I'll take a break and get back to it when I get a new idea to try (so far, nothing I did helped). |
Even more bizarre: I checked other complex properties and they have the correct documentation from the XML comments of the properties (not classes). For some reason, it's just one |
@jgarciadelanoceda Okay, I think I figured it out. Here is what happens:
In summary, it appears that the latest loaded documentation (XML file) that has a summary of the class or property of the same class type wins. Does this make sense? At least my explanation? Do you need a sample project to test this or is it clear enough? |
@jgarciadelanoceda I implemented a workaround by sorting the XML documentation files in the order of their create dates (the logic here is that the shared files will most likely have the older timestamp than the application files). In my long code sample above, I changed the XML file loading logic to this: DirectoryInfo directoryInfo = new(AppContext.BaseDirectory);
// Get all XML files in the directory (from oldest to newest).
List<FileInfo> xmlFiles = directoryInfo.GetFiles("*.xml").OrderBy(file => file.CreationTime).ToList();
logger.Information("Reading documentation files:");
if (xmlFiles != null && xmlFiles.Count > 0)
{
xmlFiles.ForEach(xmlFile =>
{
logger.Information("- {xmlFile}", Path.GetFileName(xmlFile.Name));
XDocument xmlDoc = XDocument.Load(xmlFile.FullName);
options.UseAllOfToExtendReferenceSchemas();
options.IncludeXmlComments(() => new XPathDocument(xmlDoc.CreateReader()), true);
options.SchemaFilter<DescribeEnumMembers>(xmlDoc);
});
} If did the trick for me, but I cannot guarantee it will work in every case, so it would be better if the issue were addressed in Swashbuckle code and XML load order did not matter. |
I'll look into it. It's possible that #3136 is related |
@jgarciadelanoceda It does sound related, but the point is that a class/type summary should not overwrite a property of that type summary regardless of the documentation source file order of loading. Property summary should always win. I guess if the property summary is missing, then the class/type summary would work, but that would be a fallback logic. |
Sorry for the delay, I couldn't do it for now, because I have been taking care of a new child, I will look at it when I can |
Describe the bug
In the Swagger documentation, descriptions of properties derived from classes always show the information from the
<summary>
of the class, not the<summary>
of the property. For example, here is the definition of the slightly altered WeatherForecast demo project'sWeatherForecast
class:The
Temperature
property is a complex type defined as:Notice that the summary of the
Temperature
property in theWeatherForecast
class (Forecasted temperature in the specified city.) is different from the summary of theTemperature
class definition (Holds temperature in Celsius and Fahrenheit.
), but when displayed in the Swagger documentation, the summary of the property is discarded, so only the class summary is displayed:The problem is that the class summary is not specific enough to describe a property derived from this class. It may not be obvious in this example, but say, I have a
meta
class that defines the details of apatch
operation for patching objects and I want to specify which patch operations are supported for each class. This can be only be done in the summary of the property, not in the class summary (because the class does not know where and how it will be used), right? But there seems to be no way to do it.Attached is the sample project.
Expected behavior
Swagger description of a schema's (class) property must always contain the summary of this property as defined in the containing schema (class), not the summary of the class (schema) that this property is derived from.
Actual behavior
Swagger description of a schema's (class) property always shows the summary of the class (schema) that this property is derived from, not the property's summary.
Steps to reproduce
Use the attached project.
TestSwagger.zip
Exception(s) (if any)
No response
Swashbuckle.AspNetCore version
7.2.0
.NET Version
9.0.102
Anything else?
No response
The text was updated successfully, but these errors were encountered: