mattfrear/Swashbuckle.AspNetCore.Filters

SwaggerResponseExample override example xml comments irrelevant to status code

soroshsabz opened this issue · 8 comments

ITNOA

I have below code

        /// <summary>
        /// Add a new test data
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        /// <response code ="200">
        /// Returns the newly created item
        /// </response>
        [HttpPost]
        [Route("")]
        [ProducesResponseType(typeof(Response<TestDataViewModel>), 200)]
        [ProducesResponseType(typeof(Response<TestDataViewModel>), 400)]
        [SwaggerResponseExample(400, typeof(TestDataViewModelResponseErrorExample))]
        [AllowAnonymous]
        public async Task<ActionResult<Response<TestDataViewModel>>> Add([FromBody] TestRequest request)
        {
            return await Task.FromResult(new Response<TestDataViewModel>()
            {
                Data = new TestDataViewModel()
                {
                    Id = 1,
                    Name = "Test"
                },
                StatusCode = BSN.Commons.PresentationInfrastructure.ResponseStatusCode.OK,
                Message = "OK"
            });
        }

and my TestDataViewModelResponseErrorExample like below

    public class TestDataViewModelResponseErrorExample : IExamplesProvider<Response<TestDataViewModel>>
    {
        /// <summary>
        /// Get examples
        /// </summary>
        /// <returns></returns>
        /// <exception cref="NotImplementedException"></exception>
        public Response<TestDataViewModel> GetExamples() => new Response<TestDataViewModel>()
        {
            Data = null,
            InvalidItems = new List<InvalidItem>()
            {
                new InvalidItem()
                {
                    Name = "Id",
                    Reason = "Id is required"
                }
            },
            StatusCode = BSN.Commons.PresentationInfrastructure.ResponseStatusCode.BadRequest,
            Message = "Bad Request"
        };
    }

and my TestDataViewModel like below

    public class TestDataViewModel
    {
        /// <summary>
        /// Id
        /// </summary>
        /// <example>10</example>
        public int Id { get; set; }

        /// <summary>
        /// Name
        /// </summary>
        /// <example>Hooshang</example>
        public string Name { get; set; }
    }

if I remove [SwaggerResponseExample(400, typeof(TestDataViewModelResponseErrorExample))] and TestDataViewModelResponseErrorExample, response example show example based on xml in 200 and 400 like below

image

But when TestDataViewModelResponseErrorExample exist, all of my examples changes! I say only for 400 we want to see TestDataViewModelResponseErrorExample but I see this for 200 too. like below

image

How to say Swashbuckle.AspNetCore.Filters use example xml tags default?

ITNOA

What does that mean?

Your example solution doesn't compile for me because of your using BSN.Commons.Responses. I don't know what Response<> is.

I can't reproduce your problem, it works fine for me.

Are you calling c.ExampleFilters();? I couldn't see that in your sample solution.

Maybe try call c.ExampleFilters_PrioritizingExplicitlyDefinedExamples() instead?

@mattfrear very thanks to fast response to me

I have complete code (I test it, and compile correctly) in https://github.com/soroshsabz/TestSolution/tree/main/Source/ASPTest/AutofacHandyMVCTest


using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.Filters;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Reflection;

namespace AutofacHandyMVCTest
{
    /// <summary>
    /// Configure Swagger Options
    /// </summary>
    public class ConfigureSwaggerOptions : IConfigureNamedOptions<SwaggerGenOptions>
    {
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="provider"></param>
        public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider)
        {
            _provider = provider;
        }

        /// <summary>
        /// Configure each API discovered for Swagger Documentation
        /// </summary>
        /// <param name="options"></param>
        public void Configure(SwaggerGenOptions options)
        {
            // add swagger document for every API version discovered
            foreach (var description in _provider.ApiVersionDescriptions)
            {
                options.SwaggerDoc(
                    description.GroupName,
                    CreateVersionInfo(description));
            }

            options.ExampleFilters();

            // For adding xml commenting (see also Documentation file in project properties)
            // https://learn.microsoft.com/en-us/dotnet/core/project-sdk/msbuild-props#generatedocumentationfile
            var xmlCommentsPath = Path.Combine(AppContext.BaseDirectory, $"{Assembly.GetExecutingAssembly().GetName().Name}.xml");
            options.IncludeXmlComments(xmlCommentsPath);

            options.MapType(typeof(TimeSpan?), () => new OpenApiSchema
            {
                Type = "string",
                Example = new OpenApiString("00:00:00")
            });

            // Because capable to use AutoREST we must to disable UseAllOfToExtendReferenceSchemas
            // for more information please see https://stackoverflow.com/q/59788412/1539100
            // and https://github.com/unchase/Unchase.Swashbuckle.AspNetCore.Extensions/issues/13
            // options.UseAllOfToExtendReferenceSchemas();

            // operationId is an optional unique string used to identify an operation.
            // If provided, these IDs must be unique among all operations described in your API.
            //
            // However, AutoRest seems to use that to identify each method.
            // I found a GitHub question / issue: <see href:https://github.com/Azure/autorest/issues/2647/>
            // where people addressed this by configuring AutoRest to use tags instead of operation ID to identify method.
            //
            // <see href:https://stackoverflow.com/a/60875558/1539100/>
            options.CustomOperationIds(description => (description.ActionDescriptor as ControllerActionDescriptor)?.ActionName);
        }

        /// <summary>
        /// Configure Swagger Options. Inherited from the Interface
        /// </summary>
        /// <param name="name"></param>
        /// <param name="options"></param>
        public void Configure(string name, SwaggerGenOptions options)
        {
            Configure(options);
        }

        /// <summary>
        /// Create information about the version of the API
        /// </summary>
        /// <param name="desc"></param>
        /// <returns>Information about the API</returns>
        private OpenApiInfo CreateVersionInfo(ApiVersionDescription desc)
        {
            var info = new OpenApiInfo()
            {
                Title = "Web MVC Test Project",
                Version = desc.ApiVersion.ToString(),
                Description = "Web MVC Test Project with Autofac and Swagger",
                TermsOfService = new Uri("https://resaa.net"),
                License = new OpenApiLicense
                {
                    Name = "BSN Corporation",
                    Url = new Uri("https://resaa.net/LICENSE"),
                }

            };

            if (desc.IsDeprecated)
            {
                info.Description += " This API version has been deprecated. Please use one of the new APIs available from the explorer.";
            }

            return info;
        }

        private readonly IApiVersionDescriptionProvider _provider;
    }
}

and I call services.AddSwaggerExamplesFromAssemblyOf<Startup>(); in my Startup.cs like below

using Autofac;
using Autofac.Configuration;
using Autofac.Features.AttributeFilters;
using AutofacHandyMVCTest.Controllers;
using AutofacHandyMVCTest.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Versioning;
using Swashbuckle.AspNetCore.Filters;

namespace AutofacHandyMVCTest
{
    /// <summary>
    /// Based on <see href="https://learn.microsoft.com/en-us/aspnet/core/migration/50-to-60?#use-startup-with-the-new-minimal-hosting-model"/>
    /// </summary>
    public class Startup
    {
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="configuration"></param>
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        /// <summary>
        /// The application configuration property (appsettings.json)
        /// </summary>
        public IConfiguration Configuration { get; }

        /// <summary>
        /// Configure the application services.
        /// </summary>
        /// <param name="services"></param>
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().AddControllersAsServices();
            services.AddApiVersioning((opt) =>
            {
                opt.DefaultApiVersion = new ApiVersion(1, 0);
                opt.AssumeDefaultVersionWhenUnspecified = true;
                opt.ReportApiVersions = true;
                opt.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(),
                                                                new HeaderApiVersionReader("x-api-version"),
                                                                new MediaTypeApiVersionReader("x-api-version"));
            });

            services.AddVersionedApiExplorer(setup =>
            {
                setup.GroupNameFormat = "'v'VVV";
                setup.SubstituteApiVersionInUrl = true;
            });

            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
            services.AddEndpointsApiExplorer();
            services.AddSwaggerGen();
            services.ConfigureOptions<ConfigureSwaggerOptions>();

            // based on https://github.com/mattfrear/Swashbuckle.AspNetCore.Filters?tab=readme-ov-file#installation
            services.AddSwaggerExamplesFromAssemblyOf<Startup>();
        }

        /// <summary>
        /// Configure the application container.
        /// </summary>
        /// <param name="builder"></param>
        public void ConfigureContainer(ContainerBuilder builder)
        {
            var configurationBuilder = new ConfigurationBuilder();
            configurationBuilder.AddJsonFile("autofac.json");
            var autoFacConfigurationModule = new ConfigurationModule(configurationBuilder.Build());

            builder.RegisterModule(autoFacConfigurationModule);

            var controllers = typeof(Startup).Assembly.GetTypes().Where(t => t.BaseType == typeof(Controller)).ToArray(); // for mvc controller
            builder.RegisterTypes(controllers).WithAttributeFiltering();

            controllers = typeof(Startup).Assembly.GetTypes().Where(t => t.BaseType == typeof(ControllerBase)).ToArray(); // for api controller
            builder.RegisterTypes(controllers).WithAttributeFiltering();
        }

        /// <summary>
        /// Configure the application and the HTTP request pipeline.
        /// </summary>
        /// <typeparam name="App"></typeparam>
        /// <param name="app"></param>
        /// <param name="env"></param>
        public void ConfigureApp<App>(App app, IWebHostEnvironment env) where App : IApplicationBuilder, IEndpointRouteBuilder, IHost
        {
            // Configure the HTTP request pipeline.
            if (env.IsDevelopment())
            {
                var apiVersionDescriptionProvider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();

                app.UseSwagger();
                app.UseSwaggerUI(options =>
                {
                    foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions)
                    {
                        options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json",
                            description.GroupName.ToUpperInvariant());
                    }
                });
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            app.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        }
    }
}

My exact problem is [SwaggerResponseExample(400, typeof(TestDataViewModelResponseErrorExample))] change 400 and 200 examples both of them, but I except and I want to [SwaggerResponseExample(400, typeof(TestDataViewModelResponseErrorExample))] only change 400 example, and 200 show example base on xml comments

@mattfrear Did you have any idea about my problem?

thanks for that

You still didn't answer my first question about ITNOA.