Skip to content
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

Swashbuckle/Swagger integration #27

Open
JamesCrann opened this issue Feb 15, 2019 · 8 comments
Open

Swashbuckle/Swagger integration #27

JamesCrann opened this issue Feb 15, 2019 · 8 comments
Assignees

Comments

@JamesCrann
Copy link

Hi,

I am having great success in using hybrid model binding to bind from route and body to a single model.
However the downside is that swashbuckle no longer functions correctly when generating swagger documentation.

public class AddOrUpdatePhoneNumbersRequest
    {
        [HybridBindProperty(Source.Route)]
        public string PersonId { get; set; }

        [HybridBindProperty(Source.Body)]
        public string Mobile { get; set; }

        [HybridBindProperty(Source.Body)]
        public string Home { get; set; }

        [HybridBindProperty(Source.Body)]
        public string Work { get; set; }

        [HybridBindProperty(Source.Body)]
        public PhoneNumberType PreferredPhoneNumberType { get; set; }
    }

Instead of the document example being displayed we just see the empty {} - also noting that the body thinks its coming from the query

image

Has anybody previously looked at adding a swashbuckle filter to correctly display the properties bound on the body?

@billbogaiv billbogaiv self-assigned this Mar 5, 2019
@billbogaiv
Copy link
Owner

I'll have to spin-up a sample project to see what's going on.

Will try and get to it this week.

@billbogaiv
Copy link
Owner

billbogaiv commented Mar 26, 2019

@JamesCrann: here's a sample that works with Swashbuckle.AspNetCore v4.0.1. v5.x works completely different. I may eventually incorporate this logic in some kind of HybridModelBinding.Swashbuckle package. If I do, I'll make sure to reference this issue.

Somewhere in Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddSwaggerGen(x =>
    {
        // x.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" });

        x.OperationFilter<HybridOperationFilter>();
    });
}

Quickly hacked-together filter to make Hybrid-sources appear the same as [FromBody] sources

public class HybridOperationFilter : IOperationFilter
{
    public void Apply(Operation operation, OperationFilterContext context)
    {
        var hybridParameters = context.ApiDescription.ParameterDescriptions
            .Where(x => x.Source.Id == "Hybrid")
            .Select(x => new
            {
                name = x.Name,
                schema = context.SchemaRegistry.GetOrRegister(x.Type)
            }).ToList();

        for (var i = 0; i < operation.Parameters.Count; i++)
        {
            for (var j = 0; j < hybridParameters.Count; j++)
            {
                if (hybridParameters[j].name == operation.Parameters[i].Name)
                {
                    var name = operation.Parameters[i].Name;
                    var isRequired = operation.Parameters[i].Required;

                    operation.Parameters.RemoveAt(i);

                    operation.Parameters.Insert(i, new BodyParameter()
                    {
                        Name = name,
                        Required = isRequired,
                        Schema = hybridParameters[j].schema,
                    });
                }
            }
        }
    }
}

Some relevant references:

https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/v4.0.1/src/Swashbuckle.AspNetCore.SwaggerGen/Generator/SwaggerGenerator.cs#L259-L271

https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/v5.0.0-rc2/src/Swashbuckle.AspNetCore.SwaggerGen/Generator/SwaggerGenerator.cs#L221-L227

@dkimbell13
Copy link

dkimbell13 commented Nov 29, 2019

v5.x version... At least its working for me...

public class HybridOperationFilter : IOperationFilter
{
    public void Apply(Operation operation, OperationFilterContext context)
    {
        var hybridParameters = context.ApiDescription.ParameterDescriptions
            .Where(x => x.Source.Id == "Hybrid")
            .Select(x => new { name = x.Name }).ToList();

        for (var i = 0; i < operation.Parameters.Count; i++)
        {
            for (var j = 0; j < hybridParameters.Count; j++)
            {
                if (hybridParameters[j].name == operation.Parameters[i].Name)
                {
                    var name = operation.Parameters[i].Name;
                    var isRequired = operation.Parameters[i].Required;
                    var hybridMediaType = new OpenApiMediaType { Schema = operation.Parameters[i].Schema };

                    operation.Parameters.RemoveAt(i);

                    operation.RequestBody = new OpenApiRequestBody
                    {
                        Content = new Dictionary<string, OpenApiMediaType>
                        {
                            //You are not limited to "application/json"...
                            //If you add more just ensure they use the same hybridMediaType
                            { "application/json", hybridMediaType }
                        },
                        Required = isRequired
                    };
                }
            }
        }
    }
}

NOTE: Since you probably don't want the properties that are going to be mapped from other sources to show up in your Swashbuckle UI schema definition you might want to implement the following two classes also...

    [AttributeUsage(AttributeTargets.Property)]
    public class SwaggerIgnoreAttribute: Attribute { }

And...

public class SwaggerIgnoreFilter : ISchemaFilter
    {
        public void Apply(OpenApiSchema schema, SchemaFilterContext context)
        {
            if (!(context.ApiModel is ApiObject))
            {
                return;
            }

            var model = context.ApiModel as ApiObject;

            if (schema?.Properties == null || model?.ApiProperties == null)
            {
                return;
            }

            var excludedProperties = model.Type
                .GetProperties()
                .Where(
                    t => t.GetCustomAttribute<SwaggerIgnoreAttribute>() != null
                );

            var excludedSchemaProperties = model.ApiProperties
                   .Where(
                        ap => excludedProperties.Any(
                            pi => pi.Name == ap.MemberInfo.Name
                        )
                    );

            foreach (var propertyToExclude in excludedSchemaProperties)
            {
                schema.Properties.Remove(propertyToExclude.ApiName);
            }
        }
    }

Don't forget to add the following to your Startup.cs => services.AddSwaggerGen() method.

    c.SchemaFilter<SwaggerIgnoreFilter>();
    c.OperationFilter<HybridOperationFilter>();

Add the Parameter summary comments, to your Action method, for the Hybrid model properties that are NOT coming from the RequestBody, and add them to the Action method signature...

        /// <summary>
        /// Create/Update  Record
        /// </summary>
        /// <param name="filetype">FileType</param>
        /// <param name="filenumber">FileNumber</param>
        /// <param name="recordVM"><see cref="RecordViewModel"/></param>
        /// <returns><see cref="OkObjectResult"/> or an Error Result</returns>
        [HttpPost("~/api/record/{filetype}/{filenumber}")]
        public IActionResult Upsert(
            [FromHybrid, Required]RecordViewModel recordVM, 
            [Required]string filetype, 
            [Required]string filenumber)
        {
            return Ok(_recordService.Upsert(recordVM));
        }

And add the SwaggerIgnore Attribute (Defined above) to the Hybrid model properties that are NOT coming from the RequestBody

    public class RecordViewModel
    {
        [Required]
        public string Text { get; set; } //Will be added by the HybridModelBinder from the Request Body
        [Required]
        public DateTime TransactionDate { get; set; }  //Will be added by the HybridModelBinder from the Request Body
        [Required]
        [MaxLength(10)]
        public string UserId { get; set; }  //Will be added by the HybridModelBinder from the Request Body
        [Required]
        [SwaggerIgnore]
        public string FileType { get; set; } //Will be added by the HybridModelBinder from the Path
        [Required]
        [SwaggerIgnore]
        public string FileNumber { get; set; } //Will be added by the HybridModelBinder from the Path
    }

@billbogaiv
Copy link
Owner

Thanks, @dkimbell13, for the update/code sample. I haven't forgotten about this issue. Been going through and doing some house-cleaning on the project today. I've got a list of TODOs getting the project ready for a 1.0-release 🎉 and will circle-back once that's complete.

@stevendarby
Copy link

Thanks for these filters, they work well.

If anyone knows a way to source the route parameter comments from the model property, that would be great. I see the comment above about leaving the parameters on the method and commenting them there, but the parameters are completely unused (due to being bound to the model) and I'd love to be able to remove them from the method.

@bryanllewis
Copy link

I have a similar situation, but with the query string rather than the body. I have a query model with the Id coming from the route path and the fields and include properties coming from the query string.

    public class GetQuery
    {
        [HybridBindProperty(Source.Route)]
        public long Id { get; set; }
        [HybridBindProperty(Source.QueryString)]
        public string Fields { get; set; }
        [HybridBindProperty(Source.QueryString)]
        public string Include { get; set; }
    }

The API controller:

    [HttpGet("{id}", Name = nameof(GetAsync))]
    public async Task<ActionResult> GetAsync([FromHybrid] GetQuery query, CancellationToken ct = default)
    {
     ...
    }

Currently, SwaggerUI is getting generated like this:
HybridSwagger_Actual

But my goal would be for it to look like this:
HybridSwagger_Goal

Any assistance would be appreciated.

@bricejar
Copy link

bricejar commented Nov 26, 2020

Same issues here... I wish there was a workaround.

@Misiu
Copy link
Contributor

Misiu commented Mar 1, 2022

Any updates on this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants