4

I have controller method with ObjectId params:

[ProducesResponseType(200, Type = typeof(Test))]
[HttpGet]
[Route("{id}")]
public IActionResult Get(ObjectId id)
{...

For this API method swagger generates a form with both of complex ObjectId model and string Id instead of single string param: swagger generated form How I can remove extra fields and keep only string Id?

razon
  • 3,882
  • 2
  • 33
  • 46
  • Why just not use `public IActionResult Get(String id)`? MongoDB automatically changes String to ObjectId – Valijon Jan 18 '20 at 11:28
  • 1
    @Valijon I used string Id before, but faced with different problems with validation, interceptors and other custom logic – razon Jan 18 '20 at 13:11

3 Answers3

2

I had same problem but slightly bigger. In my API methods I use ObjectId:

  • as route parameters public IActionResult Get(ObjectId id),
  • inside classes as query parameters public IActionResult Get([FromQuery] ClassWithObjectId filter),
  • in POST bodies public IActionResult Post([FromBody] ClassWithObjectId form)
  • in response objects

Also I have <summary> tags alongside with ObjectId properties and I want to show it in swagger description. So it's hard to force Swashbuckle respect all this cases, but I think I made it!

We need two filters:

  • ObjectIdOperationFilter to force Swashbuckle respect ObjectId in properties (route, query)
  • ObjectIdSchemaFilter to force Swashbuckle respect ObjectId in bodies (both in requests and responses)

On swagger setup:

//piece of code to add all XML Documentation files. Ignore if you don't need them
var swaggerFiles = new string[] { "SwaggerAPI.xml", "SwaggerApplicationAPI.xml" }
  .Select(fileName => Path.Combine(System.AppContext.BaseDirectory, fileName))
  .Where(filePath => File.Exists(filePath));

foreach (var filePath in swaggerFiles)
  options.IncludeXmlComments(filePath);

//we have to pass swaggerFiles here to add description to ObjectId props
//don't pass them if you won't
options.OperationFilter<ObjectIdOperationFilter>(swaggerFiles);
options.SchemaFilter<ObjectIdSchemaFilter>();

ObjectIdOperationFilter.cs:
(all methods to work with XML I took from Swashbuckle.AspNetCore repository)

public class ObjectIdOperationFilter : IOperationFilter
{
    //prop names we want to ignore
    private readonly IEnumerable<string> objectIdIgnoreParameters = new[]
    {
        "Timestamp",
        "Machine",
        "Pid",
        "Increment",
        "CreationTime"
    };

    private readonly IEnumerable<XPathNavigator> xmlNavigators;

    public ObjectIdOperationFilter(IEnumerable<string> filePaths)
    {
        xmlNavigators = filePaths != null
            ? filePaths.Select(x => new XPathDocument(x).CreateNavigator())
            : Array.Empty<XPathNavigator>();
    }

    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        //for very parameter in operation check if any fields we want to ignore
        //delete them and add ObjectId parameter instead
        foreach (var p in operation.Parameters.ToList())
            if (objectIdIgnoreParameters.Any(x => p.Name.EndsWith(x)))
            {
                var parameterIndex = operation.Parameters.IndexOf(p);
                operation.Parameters.Remove(p);
                var dotIndex = p.Name.LastIndexOf(".");
                if (dotIndex > -1)
                {
                    var idName = p.Name.Substring(0, dotIndex);
                    if (!operation.Parameters.Any(x => x.Name == idName))
                    {
                        operation.Parameters.Insert(parameterIndex, new OpenApiParameter()
                        {
                            Name = idName,
                            Schema = new OpenApiSchema()
                            {
                                Type = "string",
                                Format = "24-digit hex string"
                            },
                            Description = GetFieldDescription(idName, context),
                            Example = new OpenApiString(ObjectId.Empty.ToString()),
                            In = p.In,
                        });
                    }
                }
            }
    }

    //get description from XML
    private string GetFieldDescription(string idName, OperationFilterContext context)
    {
        var name = char.ToUpperInvariant(idName[0]) + idName.Substring(1);
        var classProp = context.MethodInfo.GetParameters().FirstOrDefault()?.ParameterType?.GetProperties().FirstOrDefault(x => x.Name == name);
        var typeAttr = classProp != null
            ? (DescriptionAttribute)classProp.GetCustomAttribute<DescriptionAttribute>()
            : null;
        if (typeAttr != null)
            return typeAttr?.Description;

        if (classProp != null)
            foreach (var xmlNavigator in xmlNavigators)
            {
                var propertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(classProp);
                var propertySummaryNode = xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{propertyMemberName}']/summary");
                if (propertySummaryNode != null)
                    return XmlCommentsTextHelper.Humanize(propertySummaryNode.InnerXml);
            }

        return null;
    }
}

ObjectIdSchemaFilter.cs:

public class ObjectIdSchemaFilter : ISchemaFilter
{
    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
    {
        if (context.Type == typeof(ObjectId))
        {
            schema.Type = "string";
            schema.Format = "24-digit hex string";
            schema.Example = new OpenApiString(ObjectId.Empty.ToString());
        }
    }
}

And it works! test

Repository with all this filters here

Victor Trusov
  • 1,057
  • 9
  • 19
1

Find out what answer from another story also adresses this issue:

services.AddMvc(options =>
{                    
    ...
    options.ModelMetadataDetailsProviders.Add(
        new BindingSourceMetadataProvider(typeof(ObjectId), BindingSource.Special));
});
  • 1
    I just ran into this issue as well. This solution worked for me, but it appears that if you have `[FromQuery]`, `[FromBody]`, etc. it will still try to break it up into a structured object. – p.s.w.g Feb 28 '20 at 22:41
0

It is possible to filter output fields for swagger form generator:

public class SwaggerOperationFilter : IOperationFilter
{
    private readonly IEnumerable<string> objectIdIgnoreParameters = new[]
    {
        nameof(ObjectId.Timestamp),
        nameof(ObjectId.Machine),
        nameof(ObjectId.Pid),
        nameof(ObjectId.Increment),
        nameof(ObjectId.CreationTime)
    };

    public void Apply(Operation operation, OperationFilterContext context)
    {
        operation.Parameters = operation.Parameters.Where(x =>
            x.In != "query" || objectIdIgnoreParameters.Contains(x.Name) == false
        ).ToList();
    }
}

and use this filter in Startup.cs:

public void ConfigureServices(IServiceCollection services)
    {
        ...
        services.AddSwaggerGen(options =>
        {
            ...
            options.OperationFilter<SwaggerOperationFilter>();
        });
...

As the result, we have only Id field: enter image description here

razon
  • 3,882
  • 2
  • 33
  • 46