swagger parameter on method with parameters from body but no model binding - c#

I am looking into adding an OpenAPI spec to my web API project, but I am running into various obstacles that I am not able to resolve.
API endpoint: /api/some_controller/some_method/id
The content body needs to come from the http body, but I do not want automatic binding using [FromBody] as I need to stream and process the data as-is (auditing, etc).
I added swagger to my project but as expected it does not show a body parameter.
The following DOES generate a proper swagger API definition:
public void some_method([FromBody]MyType mytype);
But as stated before, I need the raw data without model binding.
I am at a loss on how to solve this. Do I need to augment the API explorer somehow? Do I need to add options to swagger? Is there some way to have the [FromBody] attribute that does not actually bind? How can I do this?

I managed to get this to work using an extra custom attribute and an IOperationFilter
[AttributeUsage(AttributeTargets.Method)]
public class OpenApiRequestBodyType: Attribute
{
public Type BodyType { get; }
public string [] ContentTypes { get; }
public OpenApiRequestBodyType(Type type, string[] contentTypes = null)
{
BodyType = type;
ContentTypes = contentTypes;
}
}
public class SwaggerBodyTypeOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var bodyTypeAttribute = context.ApiDescription.CustomAttributes().OfType<OpenApiRequestBodyType>().FirstOrDefault();
if (bodyTypeAttribute != null)
{
var schema = context.SchemaGenerator.GenerateSchema(bodyTypeAttribute.BodyType, context.SchemaRepository);
operation.RequestBody = new OpenApiRequestBody();
string[] contentTypes;
if (bodyTypeAttribute.ContentTypes != null)
contentTypes = bodyTypeAttribute.ContentTypes;
else
contentTypes = operation.Responses.Where(x => x.Key =="200").SelectMany(x=>x.Value.Content).Select(x=>x.Key).ToArray();
foreach (var contentType in contentTypes)
{
operation.RequestBody.Content.Add(KeyValuePair.Create(contentType, new OpenApiMediaType { Schema = schema }));
}
}
}
}
Then I simply tag the method:
[OpenApiRequestBodyType(typeof(my_custom_type))]
and in the Startup:
services.AddSwaggerGen(c =>
{
c.OperationFilter<SwaggerBodyTypeOperationFilter>();
}
I am still not sure if there is no better way to do this.... but at least it works for me...

Related

How to hide a request field in Swagger with .Net Web API on GET Request [duplicate]

I'm using Swashbuckle to generate swagger documentation\UI for a webapi2 project. Our models are shared with some legacy interfaces so there are a couple of properties I want to ignore on the models. I can't use JsonIgnore attribute because the legacy interfaces also need to serialize to JSON so I don't want to ignore the properties globally, just in the Swashbuckle configuration.
I found a method of doing this documented here:
https://github.com/domaindrivendev/Swashbuckle/issues/73
But this appears to be out of date with the current Swashbuckle release.
The method recommended for the old version of Swashbuckle is using an IModelFilter implementation as follows:
public class OmitIgnoredProperties : IModelFilter
{
public void Apply(DataType model, DataTypeRegistry dataTypeRegistry, Type type)
{
var ignoredProperties = … // use reflection to find any properties on
// type decorated with the ignore attributes
foreach (var prop in ignoredProperties)
model.Properties.Remove(prop.Name);
}
}
SwaggerSpecConfig.Customize(c => c.ModelFilter<OmitIgnoredProperties>());
But I'm unsure how to configure Swashbuckle to use the IModelFilter in the current version? I'm using Swashbuckle 5.5.3.
If you need to do this but without using JsonIgnore (maybe you still need to serialize/deserialize the property) then just create a custom attribute.
[AttributeUsage(AttributeTargets.Property)]
public class SwaggerExcludeAttribute : Attribute
{
}
Then a schema filter similar to Johng's
public class SwaggerExcludeFilter : ISchemaFilter
{
#region ISchemaFilter Members
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
{
if (schema?.properties == null || type == null)
return;
var excludedProperties = type.GetProperties()
.Where(t =>
t.GetCustomAttribute<SwaggerExcludeAttribute>()
!= null);
foreach (var excludedProperty in excludedProperties)
{
if (schema.properties.ContainsKey(excludedProperty.Name))
schema.properties.Remove(excludedProperty.Name);
}
}
#endregion
}
Don't forget to register the filter
c.SchemaFilter<SwaggerExcludeFilter>();
Solution for .NET Core 3.1 and .NET Standard 2.1:
Use JsonIgnore from System.Text.Json.Serialization namespace.
( JsonIgnore from Newtonsoft.Json will NOT work )
public class Test
{
[System.Text.Json.Serialization.JsonIgnore]
public int HiddenProperty { get; set; }
public int VisibleProperty { get; set; }
}
If you mark field/property as internal or protected or private, it will be ignored automatically by swashbuckle in swagger documentation.
Update: Obviously, those properties/fields won't be populated in request/response.
The code below is very much based on #Richard's answer, but I am including it as a new answer because it has three completely new, useful features which I have added:
Runs on .NET Core on the latest version of Swashbuckle (v5)
Allows the SwaggerIgnore attribute to be applied to fields not just to properties
Handles the fact that property and field names may have been overridden using the JsonProperty attribute
EDIT: Now correctly handles camelCasing of originally TitleCased fields or properties (prompted by #mattruma's answer)
So the revised code is:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class SwaggerIgnoreAttribute : Attribute
{
}
internal static class StringExtensions
{
internal static string ToCamelCase(this string value)
{
if (string.IsNullOrEmpty(value)) return value;
return char.ToLowerInvariant(value[0]) + value.Substring(1);
}
}
public class SwaggerIgnoreFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext schemaFilterContext)
{
if (schema.Properties.Count == 0)
return;
const BindingFlags bindingFlags = BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.Instance;
var memberList = schemaFilterContext.SystemType // In v5.3.3+ use Type instead
.GetFields(bindingFlags).Cast<MemberInfo>()
.Concat(schemaFilterContext.SystemType // In v5.3.3+ use Type instead
.GetProperties(bindingFlags));
var excludedList = memberList.Where(m =>
m.GetCustomAttribute<SwaggerIgnoreAttribute>()
!= null)
.Select(m =>
(m.GetCustomAttribute<JsonPropertyAttribute>()
?.PropertyName
?? m.Name.ToCamelCase()));
foreach (var excludedName in excludedList)
{
if (schema.Properties.ContainsKey(excludedName))
schema.Properties.Remove(excludedName);
}
}
}
and in Startup.cs:
services.AddSwaggerGen(c =>
{
...
c.SchemaFilter<SwaggerIgnoreFilter>();
...
});
The AspNetCore solution looks like:
public class SwaggerExcludeSchemaFilter : ISchemaFilter
{
public void Apply(Schema schema, SchemaFilterContext context)
{
if (schema?.Properties == null)
{
return;
}
var excludedProperties = context.SystemType.GetProperties().Where(t => t.GetCustomAttribute<SwaggerExcludeAttribute>() != null);
foreach (PropertyInfo excludedProperty in excludedProperties)
{
if (schema.Properties.ContainsKey(excludedProperty.Name))
{
schema.Properties.Remove(excludedProperty.Name);
}
}
}
}
Well, with a bit of poking I found a way to do this using ISchemaFilter:
public class ApplyCustomSchemaFilters : ISchemaFilter
{
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
{
var excludeProperties = new[] {"myProp1", "myProp2", "myProp3"};
foreach(var prop in excludeProperties)
if (schema.properties.ContainsKey(prop))
schema.properties.Remove(prop);
}
}
then when calling httpConfiguration.EnableSwagger I set the SwaggerDocsConfig to use this SchemaFilter as follows:
c.SchemaFilter<ApplyCustomSchemaFilters>();
Hope this helps someone. I'd still be curious on whether it's possible to use the IModelFilter somehow though.
For people like me who are using .Net Core and are using the build in app.UseSwaggerUi3WithApiExplorer()
Use [JsonIgnore] tag using Newtonsoft.Json;
public class Project
{
[Required]
public string ProjectName { get; set; }
[JsonIgnore]
public string SomeValueYouWantToIgnore { get; set; }
}
It will be excluded from your documentation.
I have here a working example with DotNetCore 3 and Swashbuckle 5. It took me a few hours to get it in place so I thought to come back to this thread which helped me but didn't solve my issue.
Create a dummy custom attribute:
[AttributeUsage(AttributeTargets.Property)]
public class SwaggerExcludeAttribute : Attribute { }
Create a SchemaFilter which will be used by swagger to generate the API Model Schema
public class SwaggerExcludeFilter : 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<SwaggerExcludeAttribute>() != 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);
}
}
}
Then, inside the Startup.cs file add this to the swagger configuration
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
c.SchemaFilter<SwaggerExcludeFilter>();
});
You can now use the custom attribute on a property that you want to exclude from the API Mode Shema like this
public class MyApiModel
{
[SwaggerExclude]
public Guid Token { get; set; }
public int Id { get; set; }
public string Name { get; set; }
}
Based on Stef Heyenrath's answer.
Attribute to mark properties to exclude from the Swagger documentation.
[AttributeUsage(AttributeTargets.Property)]
public class SwaggerExcludeAttribute : Attribute
{
}
The filter to exclude the properties from the Swagger documentation.
public class SwaggerExcludeSchemaFilter : ISchemaFilter
{
public void Apply(Schema schema, SchemaFilterContext context)
{
if (schema?.Properties == null)
{
return;
}
var excludedProperties =
context.SystemType.GetProperties().Where(
t => t.GetCustomAttribute<SwaggerExcludeAttribute>() != null);
foreach (var excludedProperty in excludedProperties)
{
var propertyToRemove =
schema.Properties.Keys.SingleOrDefault(
x => x.ToLower() == excludedProperty.Name.ToLower());
if (propertyToRemove != null)
{
schema.Properties.Remove(propertyToRemove);
}
}
}
}
The schema.Properties.Keys are camelCase, while the properties themselves are PascalCase. Tweaked the method to convert both to lower case and compare to see what should be excluded.
Swashbuckle now has support for Newtonsoft.
https://github.com/domaindrivendev/Swashbuckle.AspNetCore#systemtextjson-stj-vs-newtonsoft
dotnet add package --version 5.3.1 Swashbuckle.AspNetCore.Newtonsoft
`services.AddSwaggerGenNewtonsoftSupport(); // explicit opt-in - needs tobe placed after AddSwaggerGen();`
You can use the Swashbuckle.AspNetCore.Annotations package, it allows you to mark that some properties are only displayed in the input parameters, and some are only displayed in the output.
for example, if you want to hide the AlertId in the input parameter of the post, you just need to do this by the [SwaggerSchema]:
public class Alert
{
[SwaggerSchema(ReadOnly = true)]
public string AlertId { get; set; }
public string Type { get; set; }
}
See more about it in the Documentation
Here is what I used with Newtonsoft.Json.JsonIgnoreAttribute:
internal class ApplySchemaVendorExtensions : Swashbuckle.Swagger.ISchemaFilter
{
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
{
foreach (var prop in type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
.Where(p => p.GetCustomAttributes(typeof(Newtonsoft.Json.JsonIgnoreAttribute), true)?.Any() == true))
if (schema?.properties?.ContainsKey(prop.Name) == true)
schema?.properties?.Remove(prop.Name);
}
}
Referring to https://stackoverflow.com/a/58193046/11748401 answer, for creating a filter you can simply use the following code:
public class SwaggerExcludeFilter : ISchemaFilter
{
public void Apply(OpenApiSchema model, SchemaFilterContext context)
{
var excludeProperties = context.ApiModel.Type?.GetProperties().Where(prop => Attribute.IsDefined(prop, typeof(SwaggerExcludeAttribute)));
if (excludeProperties != null)
{
foreach (var property in excludeProperties)
{
// Because swagger uses camel casing
var propertyName = $"{ToLowerInvariant(property.Name[0])}{property.Name.Substring(1)}";
if (model.Properties.ContainsKey(propertyName))
{
model.Properties.Remove(propertyName);
}
}
}
}
}
This is an older question, but an low-effort, intermediate solution has since become available in Swashbuckle.
Hiding legacy properties from documentation doesn't do much to discourage usage of these properties - it just delays discovery. After all, they're still part of the model. In fact, leaving them undocumented means consumers have no way of knowing they shouldn't use them!
Rather than have them go undocumented, you should simply consider marking them [Obsolete].
Swashbuckle will then mark them as deprecated in the swagger.json. In the UI, this will hide them in the Example Value sections, and in the Schema sections, they will show as grayed out with strikethrough on the names.
If you still want them to be completely hidden from the documentation, you can then set in SwaggerGeneratorOptions.IgnoreObsoleteProperties = true.
This was not a possible solution at the time this question was originally asked. The deprecated flag is a feature of OpenAPI v3, which was not released until 2017.
(Based on mutex's answer.)
I added another line to not have problems with NullReferenceException.
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
{
var excludeProperties = new[] { "myProp1", "myProp2, myProp3"};
foreach (var prop in excludeProperties)
if(schema.properties != null) // This line
if (schema.properties.ContainsKey(prop))
schema.properties.Remove(prop);
}
If you want to delete all schemas
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
{
schema.properties = null;
}
I get inspired by the blog of Ignoring properties from controller action model in Swagger using JsonIgnore.
I'm using .net core 2.1 and Swashbuckle.AspNetCore 5.3.1.
The code below solved the problem.
Add a new filter
public class SwaggerJsonIgnoreFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var ignoredProperties = context.MethodInfo.GetParameters()
.SelectMany(p => p.ParameterType.GetProperties()
.Where(prop => prop.GetCustomAttribute<JsonIgnoreAttribute>() != null))
.ToList();
if (!ignoredProperties.Any()) return;
foreach (var property in ignoredProperties)
{
operation.Parameters = operation.Parameters
.Where(p => (!p.Name.Equals(property.Name, StringComparison.InvariantCulture)))
.ToList();
}
}
}
Use the Filter in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
......
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo { Title = "CustomApi", Version = "v1" });
options.OperationFilter<SwaggerJsonIgnoreFilter>();
});
......
}
In my case I wanted to keep my Application Layer DTOs clean (without any annotation like JsonIngore) but still being able to use them in my Controllers Web APIs.
So, in my Application Layer I have a DTO like this:
public class CreateItemCommand {
public Guid ContainerId { get; set; }
public string Name { get; set; }
}
And my API design for creating an item is something like:
POST /containers/{containerId}/items
As the ContainerId is coming from the api route, I don't want the asp.net core trying to bind it into the command DTO and I don't want swashbuckle listing it neither.
So my solution is to inherit the original DTO in the API layer like this:
public class CreateItemCommandMod : CreateItemCommand {
#pragma warning disable IDE0051
private new ContainerID { get; }
#pragma warning restore IDE0051
}
...
[HttpPost("{containerId}/items}")]
public Task Create(
[FromRoute] Guid containerId,
[FromBody] CreateItemCommandMod command,
) => useCase.Create(command.Apply(r => r.ContainerId = containerId));
The useCase.Create from the ApplicationLayer expects the base class CreateItemCommand.
.Apply is just a very simple extension method that i've made to easily set the routing parameter value into the correspondent dto property.
I needed more control to remove properties which were declared elsewhere and couldn't easly use a removal attribute.
The filter created removed all items which it came accross from my excludes list:
public class SwaggerExcludeFilter : ISchemaFilter
{
private static readonly List<string> excludes = new List<string>()
{
"StoredProcedureName", "ValidationErrors", "changeTracker",
"code", "customerId", "IsDebug",
};
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (schema?.Properties == null || context == null)
return;
// Find all properties by name which need to be removed
// and not shown on the swagger spec.
schema.Properties
.Where(prp => excludes.Any(exc => string.Equals(exc, prp.Key, StringComparison.OrdinalIgnoreCase)))
.Select(prExclude => prExclude.Key)
.ToList()
.ForEach(key => schema.Properties.Remove(key));
}
}
In startup or program.cs for you .Net 6 fans.
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info
{
Version = "2.5",
Title = "My Swagger Doc G",
});
c.SchemaFilter<SwaggerExcludeFilter>();
...
Very userful solution form #Jay Shah, but if you using N'Tier architecture you can not reach protected or private DAL data from BL. to solve this, you can make this prop's acces modifier as "protected internal"
public class Test
{
protected internal int HiddenProperty { get; set; }
}
with this you can access aforementioned data from BL but not PL. or API layer.
I'm using dotnet core 3.1 and Swashbuckle 6.3.1, here is updated version with the similar logic for using ISchemaFilter to filter properties marked with customed attribute SwaggerExcludeAttribute
public class SwaggerExcludeFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
var type = context.Type;
if (!schema.Properties.Any() || type == null)
{
return;
}
var excludedPropertyNames = type
.GetProperties()
.Where(
t => t.GetCustomAttribute<SwaggerExcludeAttribute>() != null
).Select(d => d.Name).ToList();
if (!excludedPropertyNames.Any())
{
return;
}
var excludedSchemaPropertyKey = schema.Properties
.Where(
ap => excludedPropertyNames.Any(
pn => pn.ToLower() == ap.Key
)
).Select(ap => ap.Key);
foreach (var propertyToExclude in excludedSchemaPropertyKey)
{
schema.Properties.Remove(propertyToExclude);
}
}
}

How to configure NewtonsoftJson with MinimalApi in .NET 6.0

I have net6.0 project with minimal api and I would like to use NetwtonsoftJson instead of built in System.Text.Json library for serialization and deserialization.
At the moment I have this configurations for JsonOptions and that works as expected
builder.Services.Configure<JsonOptions>(options =>
{
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
});
If I try to change to something equivalent that uses Newtonsoft.Json.JsonSerializerSettings like below I am not getting same behavior. Instead it looks like it uses default System.Text.Json configuration.
builder.Services.Configure<JsonSerializerSettings>(options =>
{
options.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
options.Converters.Add(
new StringEnumConverter
{
NamingStrategy = new Newtonsoft.Json.Serialization.CamelCaseNamingStrategy()
});
});
In net5.0 I know I could use this
services.AddControllers().AddNewtonsoftJson((options) => //options); // OR
services.AddMvc().AddNewtonsoftJson((options) => //options);
However, if I use it like above in my net6.0 project then I am not using anymore MinimalApi ?
From my understanding Minimal APIs rely on some conventions regarding type binding. From what I can see they search for method with next signature - ValueTask<TModel?> BindAsync(HttpContext context, ParameterInfo parameter) on the type otherwise will try to use httpContext.Request.ReadFromJsonAsync which internally uses System.Text.Json and that can't be changed, so services.Add...().AddNewtonsoftJson((options) => //options); approach will not work.
To use Newtonsoft.Json you can try next (other than directly handling request via app.MapPost("/pst", (HttpContext c) => c.Request...)):
If you have control over all your classes which needs to be deserialized using it you can inherit them all from some generic base class which will have the method with needed signature (also you can use interface with implemented static method):
public class BaseModel<TModel>
{
public static async ValueTask<TModel?> BindAsync(HttpContext context, ParameterInfo parameter)
{
if (!context.Request.HasJsonContentType())
{
throw new BadHttpRequestException(
"Request content type was not a recognized JSON content type.",
StatusCodes.Status415UnsupportedMediaType);
}
using var sr = new StreamReader(context.Request.Body);
var str = await sr.ReadToEndAsync();
return JsonConvert.DeserializeObject<TModel>(str);
}
}
And usage:
class PostParams : BaseModel<PostParams>
{
[JsonProperty("prop")]
public int MyProperty { get; set; }
}
// accepts json body {"prop": 2}
app.MapPost("/pst", (PostParams po) => po.MyProperty);
Note that BaseModel<TModel> implemenation in this example is quite naive and possibly can be improved (check out HttpRequestJsonExtensions.ReadFromJsonAsync at least).
If you don't have control over the models or don't want to inherit them from some base you can look into creating wrappers:
public class Wrapper<TModel>
{
public Wrapper(TModel? value)
{
Value = value;
}
public TModel? Value { get; }
public static async ValueTask<Wrapper<TModel>?> BindAsync(HttpContext context, ParameterInfo parameter)
{
if (!context.Request.HasJsonContentType())
{
throw new BadHttpRequestException(
"Request content type was not a recognized JSON content type.",
StatusCodes.Status415UnsupportedMediaType);
}
using var sr = new StreamReader(context.Request.Body);
var str = await sr.ReadToEndAsync();
return new Wrapper<TModel>(JsonConvert.DeserializeObject<TModel>(str));
}
}
And usage changes to:
class PostParams
{
[JsonProperty("prop")]
public int MyProperty { get; set; }
}
// accepts json body {"prop": 2}
app.MapPost("/pst", (Wrapper<PostParams> po) => po.Value.MyProperty);
Some extra useful links:
MVC model binders - by David Fowler. Though I was not able to make it work for services.AddControllers().AddNewtonsoftJson((options) => //options);
ParameterBinder - similar approach by Damian Edwards

Hiding Route Property in Swagger

Is there a way I can hide a custom property of the route in Swagger JSON definition? I have another application that invokes the API and it reads Swagger to get more information.
For example, I want to indicate that route requires Active Directory. The calling application will execute a validation to make sure AD is available.
I looked at OperationsFilter but that inserts a Parameter into the Swagger definition. I also considered using Tag but it doesn't accomplish what I need, and it messes with UI.
We use Swagger Annotations in .NET Core 2.0 project. Ideally, I want to do this through annotations but haven't found anything I can use.
[HttpGet]
[Route("api/")]
[SwaggerOperation(
Summary = "Some Service",
Description = "Some Service",
OperationId = "getMyStuff",
Tags = new[] { "MyStuff" }
)]
[SwaggerResponse(200, "Response object returned")]
[SwaggerResponse(400, "Response object returned")]
public ActionResult<Response> GetStuff()
{...code here...}
I found an answer. Basically, I am adding an extension that is route specific.
Define custom attribute class
// allow mutliple attributes specified in data annotations
[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
// model for custom attribute class
public class CustomAttribute : Attribute
{
public string ParameterName { get; set; }
public string Value { get; set; }
public string Description { get; set; }
public CustomAttribute(string parameterName, string value, string description = null)
{
this.ParameterName = parameterName;
this.Value = value;
this.Description = description;
}
}
Create swagger operations filter
public class CustomAttributeOperationFilter : IOperationFilter
{
// variables
MethodInfo _methodInfo;
public void Apply(Operation operation, OperationFilterContext context)
{
context.ApiDescription.TryGetMethodInfo(out _methodInfo);
var attributes = _methodInfo.GetCustomAttributes<CustomAttribute>();
foreach (var atrib in attributes)
operation.Extensions.Add(atrib.ParameterName, atrib.Value);
}
}
In startups.cs update swagger registration to include custom operations filter
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("helloworld", new Info {Title = "HelloWorld API"});
c.EnableAnnotations();
c.OperationFilter<CustomAttributeOperationFilter>();
});
Finally, specify custom attributes on the API definition
[HttpGet]
[CustomAttribute("x-accept-apples, "yes")]
[CustomAttribute("x-accept-pears, "no")]
public ActionResult<Response> Get()
{
return Ok();
}

Provide documentation for ASP.NET Web Api method with a dynamic query string

I'm using Help Pages for ASP.NET Web API to create documentation for our web api. Everything is working fine using the XML documentation comments. However, for one method I can't figure out how to supply documentation for a dynamic query string.
The method uses the GetQueryNameValuePairs() of the request to select the key-value pairs of the query string to a model. For example ?1=foo&2=bar will result in a list of two objects with Id set to 1 and 2 and Value to 'foo' and 'bar', respectively.
I've tried adding the <param> tag to the XML comment, but this is ignored since the method does not contain a matching parameter.
Any help would be appreciated.
You could try extending the help page generation process. When you create your ASP.NET Web API project, the help page-related code is downloaded as source, not as a .dll, so you can extend it with any custom logic you'd like.
Here's what I would do:
Create an attribute class and decorate my special method with that (e.g. [DynamicQueryParameter("Param1",typeof(string))])
Modify the HelPageConfigurationExtensions.cs to query these attributes from the actions as well and add them manually to the UriParameters collection of the model. I would probably do this in the GenerateUriParameters() method.
[Edit] I actually had some time, so I put together the solution myself, because, you know, it's fun :)
So create an an attribute:
public class DynamicUriParameterAttribute : Attribute
{
public string Name { get; set; }
public Type Type { get; set; }
public string Description { get; set; }
}
You can decorate your action methods with this:
[DynamicUriParameter(Description = "Some description", Name ="Some name", Type =typeof(string))]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
Then I modified the HelpPageConfigurationExtensions.GenerateApiModel() like this:
private static HelpPageApiModel GenerateApiModel(ApiDescription apiDescription, HttpConfiguration config)
{
HelpPageApiModel apiModel = new HelpPageApiModel()
{
ApiDescription = apiDescription,
};
ModelDescriptionGenerator modelGenerator = config.GetModelDescriptionGenerator();
HelpPageSampleGenerator sampleGenerator = config.GetHelpPageSampleGenerator();
GenerateUriParameters(apiModel, modelGenerator);
// add this part
var attrs = apiDescription.ActionDescriptor.GetCustomAttributes<DynamicUriParameterAttribute>();
foreach (var attr in attrs)
{
apiModel.UriParameters.Add(
new ParameterDescription
{
Name = attr.Name,
Documentation = attr.Description,
TypeDescription = modelGenerator.GetOrCreateModelDescription(attr.Type)
}
);
}
// until here
GenerateRequestModelDescription(apiModel, modelGenerator, sampleGenerator);
GenerateResourceDescription(apiModel, modelGenerator);
GenerateSamples(apiModel, sampleGenerator);
return apiModel;
}

ASP.NET Swagger IEnumerable<T>

In Swagger UI I get a model like:
Inline Model [
Inline Model 1
]
Inline Model 1 {
Id (string, optional),
ConnectionString (string, optional),
ConnectionState (string, optional)
}
for a REST Get method like:
public IEnumerable<Device> Get()
{
return new List<Device>();
}
Why is it not displayed correctly?
Adding Swagger Config from comments
public class SwaggerConfig
{
public static void Register()
{
var thisAssembly = typeof(SwaggerConfig).Assembly;
GlobalConfiguration.Configuration .EnableSwagger(c => { c.SingleApiVersion("v1", "api"); }) .EnableSwaggerUi(c => { });
}
}
public class Device
{
public string Id { get; set; }
public string ConnectionString { get; set; }
public string ConnectionState { get; set; }
}
In C# Asp.Net web api, I did this:
1- In SwaggerConfig.cs
.EnableSwagger(c =>
{//add this line
c.SchemaFilter<ApplyModelNameFilter>();
}
2- add a class that implements ISchemaFilter:
class ApplyModelNameFilter : ISchemaFilter
{
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
{
schema.title = type.Name;
}
}
I got the idea from here
It seems like Swagger and/or NSwag do not handle Generic List/IList/IEnumerable types very well as a base type, perhaps because some frameworks that may try to connect with Swagger don't understand them.
I have worked around this by wrapping my List in another object. So, in your case, you may need to do something like:
public ListResponseObject<T>()
{
public IEnumerable<T> ResponseList {get; set;}
}
And then return from your controller like this:
public ListResponseObject<Device> Get()
{
return new ListResponseObject<Device>{ResponseList = new List<Device>()};
}
Not as simple... but should get it through Swagger better.
We've leveraged this to our advantage. We've applied this technique to all controllers (or something similar) so we have a more standardized response. We can also do this:
public ListResponseObject<T>() : ResponseObject<T>
{
public IEnumerable<T> ResponseList {get; set;}
}
public ResponseObject<T>()
{
public string Message {get; set;}
public string Status {get; set;}
}
And now you have a container that will make downstream handling a little easier.
Not an exact answer, but a work-around that's worked for us. YMMV
UPDATE: Here's a response to a question I posted in the NSwag GitHub issues:
I think its correct as is. Currently swagger and json schema do not support generics (only for arrays) and thus all generic types are expanded to non-generic/specific types... altough the models should be correct but you may end up with lots of classes...
An enhancement for supporting generics is planned but this will be not compliant with swagger and only work with nswag... (No support in swagger ui)

Categories