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

ModelBinder caused complex type to display property-by-property - 5.2.2 #536

Closed
gauravmittal opened this issue Oct 19, 2015 · 9 comments

Comments

Projects
None yet
7 participants
@gauravmittal
Copy link

commented Oct 19, 2015

Hello, I am using version Swashbuckle 5.2.2 in my WebApi project. I am getting a strange UI for endpoint where i used ModelBinder.

Models Used:

public class EmployeeDto
{
public PersonDto person {get; set;}
public string Number {get; set;}
...
}
public class PersonDto
{
public NameDto Name {get; set;}
public AddressDto Address {get; set;}
...
}
public class NameDto
{
public string FirstName {get; set;}
public string LastName{get; set;}
...
}

In PUT endpoint, i used ModelBinder:

public async Task UpdateEmployee(int id, [ModelBinder(typeof(EmployeeModelBinder))] EmployeeDto dto)

And after the above, Swagger UI is no more displaying EmployeeDto schema. It is displaying a drilled-down EmployeeDto with each property as a separate input:

image

While in POST - where there is no ModelBinder, it appears like:

image

I am using Visual Studio 2013.

We would like the PUT documentation to be same as POST.

What needs to be done, Please suggest.

@domaindrivendev

This comment has been minimized.

Copy link
Owner

commented Nov 3, 2015

Looks like a bug with WebApi's built-in metadata layer (ApiExplorer) which Swashbuckle is built on. For some reason it's reporting the parameter in question as having Source == "FromUri". The issue occurs here:

https://github.com/domaindrivendev/Swashbuckle/blob/master/Swashbuckle.Core/Swagger/SwaggerGenerator.cs#L167

I'm curious, why a model binder for PUT but not POST? What does it do?

@omniscient

This comment has been minimized.

Copy link

commented Nov 5, 2015

Hey @domaindrivendev,

We have implemented support for partial models on PUT to cover some update scenarios between 2 applications. With the model binder, you can submit incomplete JSON document on PUT and the data that you omit is going to be ignored.

@gauravmittal

This comment has been minimized.

Copy link
Author

commented Nov 5, 2015

applied a workaround. Created a DocumentFilter -> Removed all 'dto.<>' parameters -> Added a new Parameter with {.@in="body"; .schema.@ref="#/definitions/" + typeof(EmployeeDto).FullName} -> Injected this document filter in swagger config. working good so far.

@siewers

This comment has been minimized.

Copy link

commented Jun 22, 2016

I'm having this exact problem as well :(
If I apply a model binder on the type, it get's displayed as [FromUri]. If I set [FromBody] on the parameter, the model binder doesn't get invoked, but it's displayed correctly.
@gauravmittal Would it be possible for you to share your code? I would like to fix it, without having to do too many workarounds with the WebAPI configuration.

@siewers

This comment has been minimized.

Copy link

commented Jun 22, 2016

Okay, I found another, IMO better solution, working around the model binder issue.
http://stackoverflow.com/a/12641541/76048

This approach uses a custom JsonConverter instead of the model binder, which in my case was perfect. I was deserializing in the model binder anyway, so with this solution, I can skip an unnecessary step. Hope it can help anyone else :)

@togglebrain

This comment has been minimized.

Copy link

commented Nov 17, 2016

Can we keep this ticket open... facing the same issue. In some cases, model binder is required when getting values from path variables.

@kevinblake

This comment has been minimized.

Copy link

commented Jul 6, 2017

@gauravmittal Are you able to share the code for your DocumentFilter?

@gauravmittal

This comment has been minimized.

Copy link
Author

commented Jul 6, 2017

hi, sorry, i am almost two years late to post the code...but here it is...
i don't know whether it is still in effect or not, i just searched into old project code and found it...

here is the document filter:

`using System.Collections.Generic;
using System.Linq;
using System.Web.Http.Description;
using Swashbuckle.Swagger;

namespace xyz.Common.API.Utilities
{
public class PatchSupportForSwagger : IDocumentFilter
{
private List supportedModelTagsForModelBinder = null;
private Dictionary<string, string> tagToParamNameMapper = null;

    #region "Ctor(s)"
    public PatchSupportForSwagger()
    {
        PrepareModelTagsForModelBinder();
        PrepareTagToParamNameMapping();
    }
    private void PrepareModelTagsForModelBinder()
    {
        this.supportedModelTagsForModelBinder = new List<string>();
        this.supportedModelTagsForModelBinder.Add("employee");
        this.supportedModelTagsForModelBinder.Add("schedulededs");
        this.supportedModelTagsForModelBinder.Add("childsupport");
        this.supportedModelTagsForModelBinder.Add("directdeposit");
        this.supportedModelTagsForModelBinder.Add("timeoffaccrualoper");
        this.supportedModelTagsForModelBinder.Add("employeestate");
        this.supportedModelTagsForModelBinder.Add("employeerate");
        this.supportedModelTagsForModelBinder.Add("employeeshift");
    }
    private void PrepareTagToParamNameMapping()
    {
        this.tagToParamNameMapper = new Dictionary<string, string>();
        this.tagToParamNameMapper.Add("employee", "dto");
        this.tagToParamNameMapper.Add("schedulededs", "dto");
        this.tagToParamNameMapper.Add("childsupport", "dto");
        this.tagToParamNameMapper.Add("directdeposit", "dto");
        this.tagToParamNameMapper.Add("timeoffaccrualoper", "dto");
        this.tagToParamNameMapper.Add("employeestate", "dto");
        this.tagToParamNameMapper.Add("employeerate", "dto");
        this.tagToParamNameMapper.Add("employeeshift", "dto");
    }
    #endregion

    #region Implementation of IDocumentFilter
    public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer)
    {
        var pathItemKeyValuePairs = swaggerDoc.paths.Where(pa => pa.Value.put !=null).ToList();
        if (pathItemKeyValuePairs.Count > 0)
        {
            var pathItemsToTouch = pathItemKeyValuePairs.Where(pi => pi.Value.put.tags.Count > 0 && supportedModelTagsForModelBinder.Contains(pi.Value.put.tags[0].ToLower())).ToList();
            if (pathItemsToTouch.Count > 0)
            {
                foreach (var pathItemKvp in pathItemsToTouch)
                {
                    Operation putOperation = pathItemKvp.Value.put;
                    string tagToModify = putOperation.tags[0].ToLower();
                    string paramNameToGoFor = tagToParamNameMapper[tagToModify];
                    List<Parameter> parametersNew = PrepareNewParametersList(putOperation.parameters.ToList(), tagToModify,
                        paramNameToGoFor);
                    putOperation.parameters = parametersNew;
                }
            }
        }
    }
    #endregion

    private Parameter GetNewPUTParameter(string tag, string paramName)
    {
        Parameter dtoParam = new Parameter();
        switch (tag)
        {
            case "employee":
                dtoParam.description = "Employee to modify";
                dtoParam.required = true;
                dtoParam.name = paramName;
                dtoParam.schema = new Schema();
                dtoParam.schema.@ref = "#/definitions/" + typeof(xyz.Common.API.DTO.Employees.EmployeeDto).FullName;
                dtoParam.@in = "body";
                break;
            case "schedulededs":
                dtoParam.description = "ScheduledED to modify";
                dtoParam.required = true;
                dtoParam.name = paramName;
                dtoParam.schema = new Schema();
                dtoParam.schema.@ref = "#/definitions/" + typeof(xyz.Common.API.DTO.EarningsAndDeductions.ScheduledEdsDto).FullName;
                dtoParam.@in = "body";
                break;
            case "employeestate":
                dtoParam.description = "Employee to modify";
                dtoParam.required = true;
                dtoParam.name = paramName;
                dtoParam.schema = new Schema();
                dtoParam.schema.@ref = "#/definitions/" + typeof(xyz.Common.API.DTO.EmployeeStates.EmployeeStateDto).FullName;
                dtoParam.@in = "body";
                break;
                
            case "childsupport":
                dtoParam.description = "ChildSupport to modify";
                dtoParam.required = true;
                dtoParam.name = paramName;
                dtoParam.schema = new Schema();
                dtoParam.schema.@ref = "#/definitions/" + typeof(xyz.Common.API.DTO.ChildSupportCases.ChildSupportCaseDto).FullName;
                dtoParam.@in = "body";
                break;

            case "directdeposit":
                dtoParam.description = "Direct Deposit to modify";
                dtoParam.required = true;
                dtoParam.name = paramName;
                dtoParam.schema = new Schema();
                dtoParam.schema.@ref = "#/definitions/" + typeof(xyz.Common.API.DTO.DirectDeposit.DirectDepositDto).FullName;
                dtoParam.@in = "body";
                break;
            case "timeoffaccrualoper":
                dtoParam.description = "TimeOffAccrualOper to modify";
                dtoParam.required = true;
                dtoParam.name = paramName;
                dtoParam.schema = new Schema();
                dtoParam.schema.@ref = "#/definitions/" + typeof(xyz.Common.API.DTO.TimeOffAccruals.TimeOffAccrualOperDto).FullName;
                dtoParam.@in = "body";
                break;
            case "employeerate":
                dtoParam.description = "EmployeeRate to modify";
                dtoParam.required = true;
                dtoParam.name = paramName;
                dtoParam.schema = new Schema();
                dtoParam.schema.@ref = "#/definitions/" + typeof(xyz.Common.API.DTO.Employees.EmployeeRateDto).FullName;
                dtoParam.@in = "body";
                break;
            case "employeeshift":
                dtoParam.description = "Employee Shift to modify";
                dtoParam.required = true;
                dtoParam.name = paramName;
                dtoParam.schema = new Schema();
                dtoParam.schema.@ref = "#/definitions/" + typeof(xyz.Common.API.DTO.EmployeeShifts.EmployeeShiftDto).FullName;
                dtoParam.@in = "body";
                break;
        }
        
        return dtoParam;
    }
    private List<Parameter> PrepareNewParametersList(List<Parameter> existingParameters, string tag, string nameOfTheParameterToModifyDocFor)
    {
        List<Parameter> parametersNew = existingParameters.Where(parameter => !parameter.name.StartsWith(nameOfTheParameterToModifyDocFor)).ToList();
        Parameter dtoParameter = GetNewPUTParameter(tag, nameOfTheParameterToModifyDocFor);
        parametersNew.Add(dtoParameter);
        return parametersNew;
    }
}

}`

and in swagger config:

c.DocumentFilter<Utilities.PatchSupportForSwagger>();

hope this helps.

again sorry for extremly late reply, i know it might make the answer irrelevent now...
i changed my primary project name to xyz in the code...the idea was to use fully qualified name of the class...

hope this helps.

Thanks.

@manidula

This comment has been minimized.

Copy link

commented Aug 29, 2018

Here is the solution I used

public class SwaggerOperationalFilter : IOperationFilter
{
     public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
     {
            if (operation.operationId.Equals("Controller_ActionName"))
            {
                operation.parameters = new List<Parameter>
                {
                    new Parameter
                    {
                        @in = "body",
                        name = "request",
                        schema = new Schema
                        {
                            @ref = "#/definitions/BatchRequestDto"
                        }
                    }
                };
            }

     // Register it 
      schemaRegistry.GetOrRegister(typeof(BatchRequestDto), "BatchRequestDto");
   }
}

And Call this class in Swagger Config as

c.OperationFilter<SwaggerOperationalFilter>();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.