RicoSuter / NSwag

The Swagger/OpenAPI toolchain for .NET, ASP.NET Core and TypeScript.
http://NSwag.org
MIT License
6.79k stars 1.29k forks source link

After upgrade core 2.2=>3.1, class with inheritance is shown as object #3141

Open statler opened 4 years ago

statler commented 4 years ago

In 2.2, the controllers accepting a paramater of DataSourceLoadOptions correctly showed the structure of the inherited DataSourceLoadOptionsBase. This is no longer the case in 3.1 - it is now just shown as an object. Any ideas how I can fix this?

[ModelBinder(BinderType = typeof(DataSourceLoadOptionsBinder))]
public class DataSourceLoadOptions : DataSourceLoadOptionsBase
{
    public IList CustomQueryParams { get; set; }

}

public class DataSourceLoadOptionsBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var loadOptions = new DataSourceLoadOptions();
        DataSourceLoadOptionsParser.Parse(loadOptions, key => bindingContext.ValueProvider.GetValue(key).FirstOrDefault());
        var _customQueryParams = bindingContext.ValueProvider.GetValue("customQueryParams").FirstOrDefault();
        if (_customQueryParams != null)
        {
            loadOptions.CustomQueryParams = JsonConvert.DeserializeObject<IList>(_customQueryParams, new JsonSerializerSettings
            {
                DateParseHandling = DateParseHandling.None
            });
        }
        bindingContext.Result = ModelBindingResult.Success(loadOptions);
        return Task.CompletedTask;
    }
}
statler commented 4 years ago

Narrowing this down, if I remove

    [ModelBinder(BinderType = typeof(DataSourceLoadOptionsBinder))]

this works properly. So it is an issue with the processing of the attribute. Is there any way to just get it to ignore the ModelBinder attribute and process it as if it was not there?

statler commented 4 years ago

Example project attached

DataSourceLoadOptionsProblem.zip

RicoSuter commented 4 years ago

It seems that ASP.NET Core API Explorer reports a wrong type if this is added... not sure whether this is a bug or an expected feature... needs investigation.

statler commented 4 years ago

Thanks Rico Any idea on a workaround? Is there an easy way to use a SchemaProcessor or something to strip the attribute or replace the type? I just need to get it working again

statler commented 4 years ago

I have tried debugging this, and it looks like API Explorer does not populate the parameterdescriptions in the apiDescriptionGroupCollectionProvider.ApiDescriptionGroups when modelbinding is used. Any ideas on how we can deal with this?

statler commented 4 years ago

I have a horrible hacky solution that gets the job done, but surely there is a better way to do this? The problem is that the ApiDescriptionsGroup is incorrectly annotate for the DataSourceLoadOptions parameter type. The only solution I have at the moment is to copy the OperationParameterProcessor into a custom IOperationProcessor and remove all of the parameters for the specified parameter type. Then I recreate the parameters from a dummy method that uses the parameter's modelbinder type.

Did I mention it is horrible and hacky? How else could this be done better?

        services.AddSingleton<IOperationProcessor, MyOperationParameterProcessor>();
        //services.AddSingleton<IConfigureOptions<AspNetCoreOpenApiDocumentGeneratorSettings>, nswagOptions>();
        services.AddSwaggerDocument(x => { });

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Namotion.Reflection;
using NJsonSchema;
using NJsonSchema.Annotations;
using NJsonSchema.Generation;
using NJsonSchema.Infrastructure;
using NSwag;
using NSwag.Generation.AspNetCore;
using NSwag.Generation.Processors;
using NSwag.Generation.Processors.Contexts;

namespace DevExtremeAspNetCoreApp2
{
    public class MyOperationParameterProcessor : IOperationProcessor
    {
        private const string MultipartFormData = "multipart/form-data";

        public AspNetCoreOpenApiDocumentGeneratorSettings _settings;
        IApiDescriptionGroupCollectionProvider _apiDescriptionGroupCollectionProvider;

        public MyOperationParameterProcessor(IApiDescriptionGroupCollectionProvider apiDescriptionGroupCollectionProvider)
        {
            _apiDescriptionGroupCollectionProvider = apiDescriptionGroupCollectionProvider;
        }

        /// <summary>Processes the specified method information.</summary>
        /// <param name="operationProcessorContext"></param>
        /// <returns>true if the operation should be added to the Swagger specification.</returns>
        public bool Process(OperationProcessorContext operationProcessorContext)
        {
            //_settings = operationProcessorContext.Settings;
            if (!(operationProcessorContext is AspNetCoreOperationProcessorContext context))
            {
                return false;
            }

            var apiDescription = (operationProcessorContext as AspNetCoreOperationProcessorContext).ApiDescription;
            var incorrectParameters = apiDescription.ParameterDescriptions.Where(x => x.Type == typeof(DataSourceLoadOptions)).ToList();
            if (incorrectParameters.Count > 0)
            {
                var existinParamsToReplace = context.OperationDescription.Operation.Parameters.Where(x=>x.Type == JsonObjectType.Object);
                for (int i = existinParamsToReplace.Count()-1; i >=0 ; i--)
                {
                    var p = context.OperationDescription.Operation.Parameters[i];
                    context.OperationDescription.Operation.Parameters.Remove(p);
                }
                apiDescription.ParameterDescriptions.Clear();
                var correctParameterContainer = _apiDescriptionGroupCollectionProvider.ApiDescriptionGroups.Items[0].Items.FirstOrDefault(x => x.RelativePath == "api/SampleData/GetRight");
                if (correctParameterContainer!=null)
                {
                    foreach (ApiParameterDescription apiParameterDescription in correctParameterContainer.ParameterDescriptions)
                    {
                        (operationProcessorContext as AspNetCoreOperationProcessorContext).ApiDescription.ParameterDescriptions.Add(apiParameterDescription);
                    }
                }
            }

            _settings = operationProcessorContext.Settings as AspNetCoreOpenApiDocumentGeneratorSettings;
            var httpPath = context.OperationDescription.Path;
            var parameters = context.ApiDescription.ParameterDescriptions;
            var methodParameters = context.MethodInfo.GetParameters();

            var position = 1;
            foreach (var apiParameter in parameters.Where(p => p.Source != null))
            {
                // TODO: Provide extension point so that this can be implemented in the ApiVersionProcessor class
                var versionProcessor = _settings.OperationProcessors.TryGet<ApiVersionProcessor>();
                if (versionProcessor != null &&
                    versionProcessor.IgnoreParameter &&
                    apiParameter.ModelMetadata?.DataTypeName == "ApiVersion")
                {
                    continue;
                }

                // In Mvc < 2.0, there isn't a good way to infer the attributes of a parameter with a IModelNameProvider.Name
                // value that's different than the parameter name. Additionally, ApiExplorer will recurse in to complex model bound types
                // and expose properties as top level parameters. Consequently, determining the property or parameter of an Api is best
                // effort attempt.
                var extendedApiParameter = new ExtendedApiParameterDescription
                {
                    ApiParameter = apiParameter,
                    Attributes = Enumerable.Empty<Attribute>(),
                    ParameterType = apiParameter.Type
                };

                ParameterInfo parameter = null;

                var propertyName = apiParameter.ModelMetadata?.PropertyName;
                var property = !string.IsNullOrEmpty(propertyName) ?
                    apiParameter.ModelMetadata.ContainerType?.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance) :
                    null;

                if (property != null)
                {
                    extendedApiParameter.PropertyInfo = property;
                    extendedApiParameter.Attributes = property.GetCustomAttributes();
                }
                else
                {
                    var parameterDescriptor = apiParameter.TryGetPropertyValue<ParameterDescriptor>("ParameterDescriptor");
                    var parameterName = parameterDescriptor?.Name ?? apiParameter.Name;
                    parameter = methodParameters.FirstOrDefault(m => m.Name.ToLowerInvariant() == parameterName.ToLowerInvariant());
                    if (parameter != null)
                    {
                        extendedApiParameter.ParameterInfo = parameter;
                        extendedApiParameter.Attributes = parameter.GetCustomAttributes();
                    }
                    else
                    {
                        parameterName = apiParameter.Name;
                        property = operationProcessorContext.ControllerType.GetProperty(parameterName, BindingFlags.Public | BindingFlags.Instance);
                        if (property != null)
                        {
                            extendedApiParameter.PropertyInfo = property;
                            extendedApiParameter.Attributes = property.GetCustomAttributes();
                        }
                    }
                }

                if (apiParameter.Type == null)
                {
                    extendedApiParameter.ParameterType = typeof(string);

                    if (apiParameter.Source == BindingSource.Path)
                    {
                        // ignore unused implicit path parameters
                        if (!httpPath.ToLowerInvariant().Contains("{" + apiParameter.Name.ToLowerInvariant() + ":") &&
                            !httpPath.ToLowerInvariant().Contains("{" + apiParameter.Name.ToLowerInvariant() + "}"))
                        {
                            continue;
                        }

                        extendedApiParameter.Attributes = extendedApiParameter.Attributes.Concat(new[] { new NotNullAttribute() });
                    }
                }

                if (extendedApiParameter.Attributes.GetAssignableToTypeName("SwaggerIgnoreAttribute", TypeNameStyle.Name).Any())
                {
                    continue;
                }

                OpenApiParameter operationParameter = null;
                if (apiParameter.Source == BindingSource.Path ||
                    (apiParameter.Source == BindingSource.Custom &&
                     httpPath.Contains($"{{{apiParameter.Name}}}")))
                {
                    operationParameter = CreatePrimitiveParameter(context, extendedApiParameter);
                    operationParameter.Kind = OpenApiParameterKind.Path;
                    operationParameter.IsRequired = true; // apiParameter.RouteInfo?.IsOptional == false;

                    context.OperationDescription.Operation.Parameters.Add(operationParameter);
                }
                else if (apiParameter.Source == BindingSource.Header)
                {
                    operationParameter = CreatePrimitiveParameter(context, extendedApiParameter);
                    operationParameter.Kind = OpenApiParameterKind.Header;

                    context.OperationDescription.Operation.Parameters.Add(operationParameter);
                }
                else if (apiParameter.Source == BindingSource.Query)
                {
                    operationParameter = CreatePrimitiveParameter(context, extendedApiParameter);
                    operationParameter.Kind = OpenApiParameterKind.Query;

                    context.OperationDescription.Operation.Parameters.Add(operationParameter);
                }
                else if (apiParameter.Source == BindingSource.Body)
                {
                    operationParameter = AddBodyParameter(context, extendedApiParameter);
                }
                else if (apiParameter.Source == BindingSource.Form)
                {
                    if (_settings.SchemaType == SchemaType.Swagger2)
                    {
                        operationParameter = CreatePrimitiveParameter(context, extendedApiParameter);
                        operationParameter.Kind = OpenApiParameterKind.FormData;
                        context.OperationDescription.Operation.Parameters.Add(operationParameter);
                    }
                    else
                    {
                        var schema = CreateOrGetFormDataSchema(context);
                        schema.Properties[extendedApiParameter.ApiParameter.Name] = CreateFormDataProperty(context, extendedApiParameter, schema);
                    }
                }
                else
                {
                    if (TryAddFileParameter(context, extendedApiParameter) == false)
                    {
                        operationParameter = CreatePrimitiveParameter(context, extendedApiParameter);
                        operationParameter.Kind = OpenApiParameterKind.Query;

                        context.OperationDescription.Operation.Parameters.Add(operationParameter);
                    }
                }

                if (operationParameter != null)
                {
                    operationParameter.Position = position;
                    position++;

                    if (_settings.SchemaType == SchemaType.OpenApi3)
                    {
                        operationParameter.IsNullableRaw = null;
                    }

                    if (parameter != null)
                    {
                        ((Dictionary<ParameterInfo, OpenApiParameter>)operationProcessorContext.Parameters)[parameter] = operationParameter;
                    }
                }
            }

            ApplyOpenApiBodyParameterAttribute(context.OperationDescription, context.MethodInfo);
            RemoveUnusedPathParameters(context.OperationDescription, httpPath);
            UpdateConsumedTypes(context.OperationDescription);
            EnsureSingleBodyParameter(context.OperationDescription);

            return true;
        }

        private void ApplyOpenApiBodyParameterAttribute(OpenApiOperationDescription operationDescription, MethodInfo methodInfo)
        {
            dynamic bodyParameterAttribute = methodInfo.GetCustomAttributes()
                .FirstAssignableToTypeNameOrDefault("OpenApiBodyParameterAttribute", TypeNameStyle.Name);

            if (bodyParameterAttribute != null)
            {
                if (operationDescription.Operation.RequestBody == null)
                {
                    operationDescription.Operation.RequestBody = new OpenApiRequestBody();
                }

                var mimeTypes = ObjectExtensions.HasProperty(bodyParameterAttribute, "MimeType") ?
                    new string[] { bodyParameterAttribute.MimeType } : bodyParameterAttribute.MimeTypes;

                foreach (var mimeType in mimeTypes)
                {
                    operationDescription.Operation.RequestBody.Content[mimeType] = new OpenApiMediaType
                    {
                        Schema = mimeType == "application/json" ? JsonSchema.CreateAnySchema() : new JsonSchema
                        {
                            Type = _settings.SchemaType == SchemaType.Swagger2 ? JsonObjectType.File : JsonObjectType.String,
                            Format = _settings.SchemaType == SchemaType.Swagger2 ? null : JsonFormatStrings.Binary,
                        }
                    };
                }
            }
        }

        private void EnsureSingleBodyParameter(OpenApiOperationDescription operationDescription)
        {
            if (operationDescription.Operation.ActualParameters.Count(p => p.Kind == OpenApiParameterKind.Body) > 1)
            {
                throw new InvalidOperationException($"The operation '{operationDescription.Operation.OperationId}' has more than one body parameter.");
            }
        }

        private void UpdateConsumedTypes(OpenApiOperationDescription operationDescription)
        {
            if (operationDescription.Operation.ActualParameters.Any(p => p.IsBinary || p.ActualSchema.IsBinary))
            {
                operationDescription.Operation.TryAddConsumes("multipart/form-data");
            }
        }

        private void RemoveUnusedPathParameters(OpenApiOperationDescription operationDescription, string httpPath)
        {
            operationDescription.Path = "/" + Regex.Replace(httpPath, "{(.*?)(:(([^/]*)?))?}", match =>
            {
                var parameterName = match.Groups[1].Value.TrimEnd('?');
                if (operationDescription.Operation.ActualParameters.Any(p => p.Kind == OpenApiParameterKind.Path && string.Equals(p.Name, parameterName, StringComparison.OrdinalIgnoreCase)))
                {
                    return "{" + parameterName + "}";
                }

                return string.Empty;
            }).Trim('/');
        }

        private bool TryAddFileParameter(
            OperationProcessorContext context, ExtendedApiParameterDescription extendedApiParameter)
        {
            var typeInfo = _settings.ReflectionService.GetDescription(extendedApiParameter.ParameterType.ToContextualType(extendedApiParameter.Attributes), _settings);

            var isFileArray = IsFileArray(extendedApiParameter.ApiParameter.Type, typeInfo);

            var attributes = extendedApiParameter.Attributes
                .Union(extendedApiParameter.ParameterType.GetTypeInfo().GetCustomAttributes());

            var hasSwaggerFileAttribute = attributes.FirstAssignableToTypeNameOrDefault("SwaggerFileAttribute", TypeNameStyle.Name) != null;

            if (typeInfo.Type == JsonObjectType.File ||
                typeInfo.Format == JsonFormatStrings.Binary ||
                hasSwaggerFileAttribute ||
                isFileArray)
            {
                AddFileParameter(context, extendedApiParameter, isFileArray);
                return true;
            }

            return false;
        }

        private void AddFileParameter(OperationProcessorContext context, ExtendedApiParameterDescription extendedApiParameter, bool isFileArray)
        {
            if (_settings.SchemaType == SchemaType.Swagger2)
            {
                var operationParameter = CreatePrimitiveParameter(context, extendedApiParameter);
                operationParameter.Type = JsonObjectType.File;
                operationParameter.Kind = OpenApiParameterKind.FormData;

                if (isFileArray)
                {
                    operationParameter.CollectionFormat = OpenApiParameterCollectionFormat.Multi;
                }

                context.OperationDescription.Operation.Parameters.Add(operationParameter);
            }
            else
            {
                var schema = CreateOrGetFormDataSchema(context);
                schema.Type = JsonObjectType.Object;
                schema.Properties[extendedApiParameter.ApiParameter.Name] = CreateFormDataProperty(context, extendedApiParameter, schema);
            }
        }

        private JsonSchema CreateOrGetFormDataSchema(OperationProcessorContext context)
        {
            if (context.OperationDescription.Operation.RequestBody == null)
            {
                context.OperationDescription.Operation.RequestBody = new OpenApiRequestBody();
            }

            var requestBody = context.OperationDescription.Operation.RequestBody;
            if (!requestBody.Content.ContainsKey(MultipartFormData))
            {
                requestBody.Content[MultipartFormData] = new OpenApiMediaType
                {
                    Schema = new JsonSchema()
                };
            }

            if (requestBody.Content[MultipartFormData].Schema == null)
            {
                requestBody.Content[MultipartFormData].Schema = new JsonSchema();
            }

            return requestBody.Content[MultipartFormData].Schema;
        }

        private static JsonSchemaProperty CreateFormDataProperty(OperationProcessorContext context, ExtendedApiParameterDescription extendedApiParameter, JsonSchema schema)
        {
            return context.SchemaGenerator.GenerateWithReferenceAndNullability<JsonSchemaProperty>(
               extendedApiParameter.ApiParameter.Type.ToContextualType(extendedApiParameter.Attributes), context.SchemaResolver);
        }

        private bool IsFileArray(Type type, JsonTypeDescription typeInfo)
        {
            var isFormFileCollection = type.Name == "IFormFileCollection";
            if (isFormFileCollection)
            {
                return true;
            }

            if (typeInfo.Type == JsonObjectType.Array && type.GenericTypeArguments.Any())
            {
                var description = _settings.ReflectionService.GetDescription(type.GenericTypeArguments[0].ToContextualType(), _settings);
                if (description.Type == JsonObjectType.File || description.Format == JsonFormatStrings.Binary)
                {
                    return true;
                }
            }

            return false;
        }

        private OpenApiParameter AddBodyParameter(OperationProcessorContext context, ExtendedApiParameterDescription extendedApiParameter)
        {
            OpenApiParameter operationParameter;

            var contextualParameterType = extendedApiParameter.ParameterType
                .ToContextualType(extendedApiParameter.Attributes);

            var typeDescription = _settings.ReflectionService.GetDescription(contextualParameterType, _settings);
            var isNullable = _settings.AllowNullableBodyParameters && typeDescription.IsNullable;
            var operation = context.OperationDescription.Operation;

            var parameterType = extendedApiParameter.ParameterType;
            if (parameterType.Name == "XmlDocument" || parameterType.InheritsFromTypeName("XmlDocument", TypeNameStyle.Name))
            {
                operation.TryAddConsumes("application/xml");
                operationParameter = new OpenApiParameter
                {
                    Name = extendedApiParameter.ApiParameter.Name,
                    Kind = OpenApiParameterKind.Body,
                    Schema = new JsonSchema
                    {
                        Type = JsonObjectType.String,
                        IsNullableRaw = isNullable
                    },
                    IsNullableRaw = isNullable,
                    IsRequired = extendedApiParameter.IsRequired(_settings.RequireParametersWithoutDefault),
                    Description = extendedApiParameter.GetDocumentation()
                };
            }
            else if (parameterType.IsAssignableToTypeName("System.IO.Stream", TypeNameStyle.FullName))
            {
                operation.TryAddConsumes("application/octet-stream");
                operationParameter = new OpenApiParameter
                {
                    Name = extendedApiParameter.ApiParameter.Name,
                    Kind = OpenApiParameterKind.Body,
                    Schema = new JsonSchema
                    {
                        Type = JsonObjectType.String,
                        Format = JsonFormatStrings.Binary,
                        IsNullableRaw = isNullable
                    },
                    IsNullableRaw = isNullable,
                    IsRequired = extendedApiParameter.IsRequired(_settings.RequireParametersWithoutDefault),
                    Description = extendedApiParameter.GetDocumentation()
                };
            }
            else // body from type
            {
                operationParameter = new OpenApiParameter
                {
                    Name = extendedApiParameter.ApiParameter.Name,
                    Kind = OpenApiParameterKind.Body,
                    IsRequired = extendedApiParameter.IsRequired(_settings.RequireParametersWithoutDefault),
                    IsNullableRaw = isNullable,
                    Description = extendedApiParameter.GetDocumentation(),
                    Schema = context.SchemaGenerator.GenerateWithReferenceAndNullability<JsonSchema>(
                        contextualParameterType, isNullable, schemaResolver: context.SchemaResolver)
                };
            }

            operation.Parameters.Add(operationParameter);
            return operationParameter;
        }

        private OpenApiParameter CreatePrimitiveParameter(
            OperationProcessorContext context,
            ExtendedApiParameterDescription extendedApiParameter)
        {
            var contextualParameter = extendedApiParameter.ParameterType.ToContextualType(extendedApiParameter.Attributes);

            var description = extendedApiParameter.GetDocumentation();
            var operationParameter = context.DocumentGenerator.CreatePrimitiveParameter(
                extendedApiParameter.ApiParameter.Name, description, contextualParameter);

            if (extendedApiParameter.ParameterInfo?.HasDefaultValue == true)
            {
                var defaultValue = context.SchemaGenerator
                    .ConvertDefaultValue(contextualParameter, extendedApiParameter.ParameterInfo.DefaultValue);

                if (_settings.SchemaType == SchemaType.Swagger2)
                {
                    operationParameter.Default = defaultValue;
                }
                else if (operationParameter.Schema.HasReference)
                {
                    if (_settings.AllowReferencesWithProperties)
                    {
                        operationParameter.Schema = new JsonSchema
                        {
                            Default = defaultValue,
                            Reference = operationParameter.Schema,
                        };
                    }
                    else
                    {
                        operationParameter.Schema = new JsonSchema
                        {
                            Default = defaultValue,
                            OneOf = { operationParameter.Schema },
                        };
                    }
                }
                else
                {
                    operationParameter.Schema.Default = defaultValue;
                }
            }

            operationParameter.IsRequired = extendedApiParameter.IsRequired(_settings.RequireParametersWithoutDefault);
            return operationParameter;
        }

        private class ExtendedApiParameterDescription
        {
            public ApiParameterDescription ApiParameter { get; set; }

            public ParameterInfo ParameterInfo { get; set; }

            public PropertyInfo PropertyInfo { get; set; }

            public Type ParameterType { get; set; }

            public IEnumerable<Attribute> Attributes { get; set; } = Enumerable.Empty<Attribute>();

            public bool IsRequired(bool requireParametersWithoutDefault)
            {
                var isRequired = false;

                // available in asp.net core >= 2.2
                if (ApiParameter.HasProperty("IsRequired"))
                {
                    isRequired = ApiParameter.TryGetPropertyValue("IsRequired", false);
                }
                else
                {
                    // fallback for asp.net core <= 2.1
                    if (ApiParameter.Source == BindingSource.Body)
                    {
                        isRequired = true;
                    }
                    else if (ApiParameter.ModelMetadata != null &&
                             ApiParameter.ModelMetadata.IsBindingRequired)

                    {
                        isRequired = true;
                    }
                    else if (ApiParameter.Source == BindingSource.Path &&
                             ApiParameter.RouteInfo != null &&
                             ApiParameter.RouteInfo.IsOptional == false)
                    {
                        isRequired = true;
                    }
                }

                return isRequired || (requireParametersWithoutDefault && ParameterInfo?.HasDefaultValue != true);
            }

            public string GetDocumentation()
            {
                var parameterDocumentation = string.Empty;
                if (ParameterInfo != null)
                {
                    parameterDocumentation = ParameterInfo.ToContextualParameter().GetDescription();
                }
                else if (PropertyInfo != null)
                {
                    parameterDocumentation = PropertyInfo.ToContextualProperty().GetDescription();
                }

                return parameterDocumentation;
            }
        }
    }
}
statler commented 4 years ago

We have a response from the core team https://github.com/dotnet/aspnetcore/issues/27671. This wont be changing.

Can we please improve the treatment though, because at the moment the swagger UI is broken. If you use the 'Try It Out' function, then it tries to name the parameter instead of just passing the options

E.G www.my.site/mycontroller?ParameterName={objectjson}

where it should be www.my.site/mycontroller?{objectjson}

jeremyVignelles commented 4 years ago

where it should be www.my.site/mycontroller?{objectjson}

Is that something that is supported by OpenApi? This looks like a really special use case that I wouldn't recommend using as it doesn't look like a real query string. If you really want to do that, be prepared to face many issues and override many things...

statler commented 4 years ago

That wasn't verbatim. I was trying to paraphrase. Lets say the definition of the controller is

public async Task<IActionResult> GetScheduleItem(MyOptions myParameter)

and MyOptions definition is:

 [ModelBinder(BinderType = typeof(MyOptions Binder))]
public class MyOptions : MyOptionsBase

Lets say MyOptionsBasehas 2 string properties Property1 and Property2.

Using 'try it out' in .Net Core 2.2 and NSwag would create a query based on MyOptionsBase e.g.

www.my.site/mycontroller?Property1="Prop1value"&Property2="Prop2value"

Whereas in 3.1 it is sent as:

www.my.site/mycontroller?myParameter={"Property1":"Prop1value","Property2":"Prop2value"}

which fails to bind. The submitted query is just invalid. No matter what you enter in the 'Try it out', the modelbinder means the query will not execute

Is that clear enough?

statler commented 3 years ago

This is actually needs to be dealt with unfortunately. This is generating incorrect code as well as the swagger ui issues.