-
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]: Query string parameter example repeats the example in the description and in the value #3089
Comments
I am using additional packages to address various issues (e.g. using enum names instead of numeric values, etc): Swashbuckle.AspNetCore {6.8.1} |
Here is my full Swagger setup logic: 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);
#pragma warning disable CA1864 // Prefer the 'IDictionary.TryAdd(TKey, TValue)' method
if (!userScopes.ContainsKey(defaultScopeFqn))
{
userScopes.Add(defaultScopeFqn, defaultScope);
}
#pragma warning restore CA1864 // Prefer the 'IDictionary.TryAdd(TKey, TValue)' method
}
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.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.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<>))));
}
} |
This behaviour that you are having does not seem to be caused by SwashBuckle, because I just created the same scenaro that you have. |
I will give it a try. Thanks for checking. Will report back once I figure it out. |
@martincostello Okay, I just created an ASP.NET Core API with the weather forecast controller example generated by Visual Studio and made the following changes: (1) Commented controller, method, and returned object class, (2) added query string property to the builder.Services.AddSwaggerGen
(
options =>
{
List<string> xmlFiles = [.. Directory.GetFiles(AppContext.BaseDirectory,"*.xml",SearchOption.TopDirectoryOnly)];
if (xmlFiles != null && xmlFiles.Count > 0)
{
xmlFiles.ForEach(xmlFile =>
{
XDocument xmlDoc = XDocument.Load(xmlFile);
options.IncludeXmlComments(() => new XPathDocument(xmlDoc.CreateReader()), true);
});
}
}
); So, my whole using System.Xml.Linq;
using System.Xml.XPath;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen
(
options =>
{
List<string> xmlFiles = [.. Directory.GetFiles(AppContext.BaseDirectory,"*.xml",SearchOption.TopDirectoryOnly)];
if (xmlFiles != null && xmlFiles.Count > 0)
{
xmlFiles.ForEach(xmlFile =>
{
XDocument xmlDoc = XDocument.Load(xmlFile);
options.IncludeXmlComments(() => new XPathDocument(xmlDoc.CreateReader()), true);
});
}
}
);
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run(); And the controller file looks like this: using Microsoft.AspNetCore.Mvc;
namespace TestSwagger.Controllers;
/// <summary>
/// Weather service.
/// </summary>
[ApiController]
[Route("[controller]")]
public class WeatherForecastController:ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
/// <summary>
/// Weather service.
/// </summary>
/// <param name="logger">
/// Logger.
/// </param>
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
/// <summary>
/// Returns weather info for the city.
/// </summary>
/// <param name="city" example="Seattle">
/// City.
/// </param>
/// <returns>
/// Weather info.
/// </returns>
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get
(
[FromQuery] string city
)
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
} There is only one Nuget package: And the result is what I originally reported: I can share the whole solution if it helps but maybe this info will give you enough data. |
I have just reproduced it.. Not in the repo of SwashBuckle but instead in other repo.. Weird that is not reproduced in the SB repo |
Describe the bug
If I define a controller parameter as a query string value (e.g. using the
FromQuery
attribute) and specify theexample
attribute of theparam
element in the XML documentation, the specified example will be displayed in two places: in the text description of the parameter (Example : somevalue) and in the HTML element (in the actual field).Expected behavior
I think just setting the value of the element should be sufficient, so there is no need to provide additional Example text.
Actual behavior
The example values is pre-populated in the field element and also in the text description
Steps to reproduce
Define a controller query string parameter and document it in XML with the example attribute, similar to the following (removed all non-essential elements):
The Swagger documentation will show the example in two places:
Exception(s) (if any)
No response
Swashbuckle.AspNetCore version
6.8.1
.NET Version
8
Anything else?
Not sure if this info is useful, but for path parameters, ther example values are not duplicated.
The text was updated successfully, but these errors were encountered: