I have a C# .NET 5.0 ASP.NET Core Web API application with "Enable OpenAPI support" selected. I want to hide the optional parameter in the below example from what shows up on the swagger page. I have found numerous posts about hiding a property or the controller but none of these solutions seem to work for just the parameter in the given code:
[HttpGet]
[Route("search")]
[Authorize]
public async Task<IActionResult> Search(string query, string optional = "")
{
return OK();
}
You can create a custom attibute and an operation filter inhering from Swashbuckle.AspNetCore.SwaggerGen.IOperationFilter to exclude the desired parameters from swagger.json generation
public class OpenApiParameterIgnoreAttribute : System.Attribute
{
}
public class OpenApiParameterIgnoreFilter : Swashbuckle.AspNetCore.SwaggerGen.IOperationFilter
{
public void Apply(Microsoft.OpenApi.Models.OpenApiOperation operation, Swashbuckle.AspNetCore.SwaggerGen.OperationFilterContext context)
{
if (operation == null || context == null || context.ApiDescription?.ParameterDescriptions == null)
return;
var parametersToHide = context.ApiDescription.ParameterDescriptions
.Where(parameterDescription => ParameterHasIgnoreAttribute(parameterDescription))
.ToList();
if (parametersToHide.Count == 0)
return;
foreach (var parameterToHide in parametersToHide)
{
var parameter = operation.Parameters.FirstOrDefault(parameter => string.Equals(parameter.Name, parameterToHide.Name, System.StringComparison.Ordinal));
if (parameter != null)
operation.Parameters.Remove(parameter);
}
}
private static bool ParameterHasIgnoreAttribute(Microsoft.AspNetCore.Mvc.ApiExplorer.ApiParameterDescription parameterDescription)
{
if (parameterDescription.ModelMetadata is Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DefaultModelMetadata metadata)
{
return metadata.Attributes.ParameterAttributes.Any(attribute => attribute.GetType() == typeof(OpenApiParameterIgnoreAttribute));
}
return false;
}
}
Put it in your controller's parameter
[HttpGet]
[Route("search")]
[Authorize]
public async Task<IActionResult> Search(string query, [OpenApiParameterIgnore] string optional = "")
{
return Ok();
}
Then configure it in Status.cs
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API Title", Version = "v1" });
c.OperationFilter<OpenApiParameterIgnoreFilter>();
});
Related
I don't know enough about C#, .NET, or the MVC pattern to know exactly what is relevant to include here, but I'm pulling my hair out with a very simple change I'm working on.
I have a controller with a Search action (method?) that looks like:
public string Search(int id)
{
return $"The id was {id}";
}
and when I hit the route I get the expected response, e.g.
$ curl https://localhost:7180/Players/Search/1
The id was 1
but when I change the variable name from id to anything else, the behavior changes and the value goes to 0 for some reason.
public string Search(int thing)
{
return $"The thing was {thing}";
}
$ curl https://localhost:7180/Players/Search/1
The thing was 0
I thought maybe it had to do with the Model itself, because the model code at least has an Id attribute
public class Player
{
public int Id { get; set; }
public string? Name { get; set; }
}
but renaming that variable to name (which seems analogous) also doesn't help.
So what concept am I missing here? Why can't I just rename that variable to whatever I want? Thanks in advance!
(I don't know how better to communicate all the different aspects of the code, so here is a link to the line in question, inside the project)
By default MVC registers (see either Program or Startup) next default route, so it can bind id parameter of method as positional part of path:
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
You can change the parameter name for example using attribute routing:
[Route("[controller]/search/{thing}")]
public string Search(int thing)
{
return $"The thing was {thing}";
}
Or using HTTP verb templates:
[HttpGet("[controller]/search/{thing}")]
public string Search(int thing)
{
return $"The thing was {thing}";
}
Check the linked docs for other options/details.
I believe this has to do with the way you've defined your route in Program.cs:
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
You'll want to add a new definition like this:
app.MapControllerRoute(
name: "default",
pattern: "Players/Search/{thing?}");
or, you could use attribute-based route definitions to move the route pattern definition closer to the actual code. See the MSFT docs for details. Basically, add app.MapControllers(); to Program.cs, then for your individual routes, do something like this:
[Route("Players/Search/{thing}")]
public string Search(int thing)
{
return $"The thing was {thing}";
}
You can decorate the method and define the parameter.
// GET api/values/5
[HttpGet("{id}")]
public virtual async Task<ActionResult<IEntity>> Get(string id)
{
var entity = await Repository.GetEntity(x => x.Id == id);
if (entity == null) return NotFound();
return Ok(entity);
}
Here is another example of an API Controller
[Route("api/[controller]")]
[ApiController]
public class UserController : ControllerBase
{
private readonly GameDataContext context;
public UserController(GameDataContext context)
{
this.context = context;
}
// GET: /<controller>/
// GET api/user
[HttpGet]
public ActionResult<IEnumerable<User>> Get()
{
return context.Users.Where(x => x.IsDeleted == false).ToArray();
}
// GET api/user/5
[HttpGet("{id}", Name = "GetUser")]
public ActionResult<User> Get(int id)
{
var user = context.Users.FirstOrDefault(x => x.Id == id && x.IsDeleted == false);
if (user != null)
{
return user;
}
return NotFound();
}
// GET api/user/username/5
[HttpGet("username/{id}", Name = "GetUserByGameId")]
public ActionResult<User> GetByUser(string gameId)
{
var user = context.Users.FirstOrDefault(x => x.UserGameId.Equals(gameId) && x.IsDeleted == false);
if (user != null)
{
return user;
}
return NotFound();
}
// POST api/user
[HttpPost]
public async Task<ActionResult> Post([FromBody] User value)
{
if (value == null)
{
return BadRequest();
}
var user = context.Users.FirstOrDefault(x => x.UserGameId.Equals(value.UserGameId) && x.IsDeleted == false);
if (user != null)
{
return BadRequest("User already exists!");
}
context.Users.Add(value);
await context.SaveChangesAsync();
return CreatedAtRoute("GetUser", new { id = value.Id }, value);
}
// PUT api/user/steamId
[HttpPut("{gameId}")]
public async Task<ActionResult> Put(string gameId, [FromBody] User value)
{
if (value == null || !value.UserGameId.Equals(gameId))
{
return BadRequest();
}
var user = context.Users.FirstOrDefault(x => x.UserGameId.Equals(gameId) && x.IsDeleted == false);
if (user == null)
{
return NotFound();
}
user.UserGameId = value.UserGameId;
user.FirstName = value.FirstName;
user.MiddleName = value.MiddleName;
user.LastName = value.LastName;
user.Email = value.Email;
context.Users.Update(user);
await context.SaveChangesAsync();
return new NoContentResult();
}
// DELETE api/user/steamId
[HttpDelete("{gameId}")]
public async Task<ActionResult> Delete(string gameId)
{
if (string.IsNullOrEmpty(gameId))
{
return BadRequest();
}
var user = context.Users.FirstOrDefault(x => x.UserGameId.Equals(gameId) && x.IsDeleted == false);
if (user == null)
{
return NotFound();
}
user.IsDeleted = true;
context.Users.Update(user);
var scores = context.Scores.Where(x => x.UserId == user.Id);
foreach (var score in scores)
{
score.IsDeleted = true;
context.Scores.Update(score);
}
await context.SaveChangesAsync();
return new NoContentResult();
}
}
I've been trying to use Namespace routing to build some APIs dynamically without the need to worry about hardcoding the routes. However, I did find an example from MSDN to use namespaces and folder structure as your API structure. Here's the sample that I have to use Namespace routing:
public class NamespaceRoutingConvention : Attribute, IControllerModelConvention
{
private readonly string _baseNamespace;
public NamespaceRoutingConvention(string baseNamespace)
{
_baseNamespace = baseNamespace;
}
public void Apply(ControllerModel controller)
{
var hasRouteAttributes = controller.Selectors.Any(selector => selector.AttributeRouteModel != null);
if (hasRouteAttributes)
{
return;
}
var namespc = controller.ControllerType.Namespace;
if (namespc == null) return;
var templateParts = new StringBuilder();
templateParts.Append(namespc, _baseNamespace.Length + 1, namespc.Length - _baseNamespace.Length - 1);
templateParts.Replace('.', '/');
templateParts.Append("/[controller]/[action]/{environment}/{version}");
var template = templateParts.ToString();
foreach (var selector in controller.Selectors)
{
selector.AttributeRouteModel = new AttributeRouteModel()
{
Template = template
};
}
}
}
And here's the controller:
namespace Backend.Controllers.Api.Project.Core
{
public class UserController : ApiBaseController
{
public UserController()
{
}
[HttpPost]
public IActionResult Login(LoginInput loginInput) // <-- loginInput properties return null
{
if (!ModelState.IsValid) return BadRequest();
return Ok(user);
}
}
}
in Startup.cs
namespace Backend
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Let's use namespaces as the routing default way for our APIs
services.AddControllers(options =>
{
options.Conventions.Add(new NamespaceRoutingConvention(typeof(Startup).Namespace + ".Controllers"));
});
}
}
}
Everything works ok except that when I trigger a POST api call to Login action the LoginInput doesn't get populated the values I'm sending through Postman i.e. {"username": "value", "password": "sample"} and it always returns null value. I'm not sure what am I doing wrong with the NamespaceRoutingConvention. Bear in mind if I remove it and hard-code the route in the controller like:
[ApiController]
[Route("api/project/core/[controller]/[action]/proda/v1")]
It works as expected. Any ideas?
Try to use this instead:
[HttpPost]
public IActionResult Login([FromBody]LoginInput loginInput)
{
if (!ModelState.IsValid) return BadRequest();
return Ok(user);
}
I think that by setting AttributeRouteModel, you're preventing the middleware invoked by having ApiControllerAttribute in the Controller to do its job, and so the defaults of treating object parameters as body is not applied.
This is a guess though, I haven't been able to find the corresponding code in the source code.
I'm trying to stream large iso files(~4Gb).
Therefore, I followed this article https://dotnetcoretutorials.com/2017/03/12/uploading-files-asp-net-core/ (Streaming Files (Large Files) is a part I was interested in).
My Controller looks like this:
[HttpPost]
[DisableFormValueModelBinding]
[Authorize]
[RequestFormLimits(MultipartBodyLengthLimit = 5368709000)]
[RequestSizeLimit(5368709000)]
[Route("{clusterId}/image_library/upload_image")]
[ProducesResponseType(typeof(AsyncOperationResult), 200)]
[ProducesResponseType(typeof(OperationResult), 400)]
[ProducesResponseType(typeof(string), 401)]
public IActionResult UploadFile()
{
bool result = Request.StreamFile("E:\\").Result; / extension method
if (result)
{
return Ok();
}
return BadRequest();
bool result= Request.StreamFile("E:\\").Result; /extension method
if (result)
{
return Ok();
}
return BadRequest();
}
The DisableFormValueModelBinding Attribute implemented as follows:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var formValueProviderFactory = context.ValueProviderFactories
.OfType<FormValueProviderFactory>()
.FirstOrDefault();
if (formValueProviderFactory != null)
{
context.ValueProviderFactories.Remove(formValueProviderFactory);
}
var jqueryFormValueProviderFactory = context.ValueProviderFactories
.OfType<JQueryFormValueProviderFactory>()
.FirstOrDefault();
if (jqueryFormValueProviderFactory != null)
{
context.ValueProviderFactories.Remove(jqueryFormValueProviderFactory);
}
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
And my Kestrel configured this way:
hostBuilder.UseKestrel(options =>
{
options.AddServerHeader = false;
options.Listen(ipAddress, ProviderSettings.API_port);
options.Listen(ipAddress, ProviderSettings.API_SSL_port, listenOptions =>
{
listenOptions.UseHttps(sslSert);
});
options.Limits.MaxRequestBodySize = null;
})
.UseContentRoot(contentRootPath)
.ConfigureLogging(logging =>
{
// Remove default Microsoft Logger
logging.ClearProviders();
});
The problem is that, when I post files on my controller method, they are getting loaded into RAM, but I need them to be streamed on hardrive by butches without devouring my RAM.
Checked my middlewares: none reconfigures Kestrel settings, the same code works fine on another project(I must be missing something).
Any ideas?
I have a simple web API2 project that uses swagger for it's documentations.
Given a simple GET endpoint that uses route parameters and query parameters such as:
[HttpGet]
[Route("api/v2/items/{itemid:int}")]
public IHttpActionResult Getv2(int itemId, [FromUri(Name ="")]DTOv1 request)
{
return Ok();
}
public class DTOv1
{
public DateTime? StartValue { get; set; }
}
This gives the following documentation:
However, I would like to be able to specify all the items in a POCO. Such as:
[HttpGet]
[Route("api/v3/items/{itemid:int}")]
public IHttpActionResult Getv3([FromUri(Name ="")]DTOv2 request)
{
return Ok();
}
public class DTOv2
{
public int ItemId { get; set; }
public DateTime? StartValue { get; set; }
}
This gives the following Incorrect documentation:
This GET endpoint works in the same way as the first example but as you can see the documentation does not, and trying to do an example will not work. Is it possible to configure swagger so that this is documented in the same way as the first example, ideally in a convention based way?
Swagger is just using the default setup:
GlobalConfiguration.Configuration
.EnableSwagger(c =>
{
c.SingleApiVersion("v1", "TestSwagger");
c.PrettyPrint();
})
.EnableSwaggerUi(c =>
{
});
EDIT:
Thanks to the response regarding adding filters by I wrote the following operation filter that works in our use case to manipulate the parameters:
private class OperationFilter : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
if (apiDescription.HttpMethod.Method == "GET")
{
var pathParams = operation.parameters.Where(x => x.#in == "path");
var toRemoveItems = new List<Parameter>();
foreach(var pathParam in pathParams)
{
toRemoveItems.AddRange(operation
.parameters
.Where(x => x.#in != "path" && x.name.EndsWith(pathParam.name)));
}
foreach(var toRemove in toRemoveItems)
{
operation.parameters.Remove(toRemove);
}
}
}
}
Following up with my suggestion from the comments about using an IDocumentFilter here is a starting point:
private class RouteTestDocumentFilter : IDocumentFilter
{
const string PATH = "/api/RouteTest/test/{itemid}";
public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry s, IApiExplorer a)
{
if (swaggerDoc.paths != null && swaggerDoc.paths.ContainsKey(PATH))
{
var get = swaggerDoc.paths[PATH].get;
if (get != null)
{
get.parameters.RemoveAt(0);
get.parameters[0].#in = "path";
get.parameters[0].required = true;
foreach (var param in get.parameters)
{
int pos = param.name.IndexOf('.');
if (pos > 0)
param.name = param.name.Substring(pos + 1);
}
}
}
}
}
For more details see my commit:
https://github.com/heldersepu/SwashbuckleTest/commit/38a31e0ee700faf91cc38d005ae1c5f4bec3e1f3
Here is how it looks on the UI:
http://swashbuckletest.azurewebsites.net/swagger/ui/index?filter=RouteTest#/RouteTest/RouteTest_Get
I'm using a custom filter (defined as follows):
if (user == null || !user.Active)
{
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary
{
{"controller", "Home"},
{"action", "NotAuthorized"}
});
}
base.OnActionExecuting(filterContext);
This is run site-wide (in RegisterGlobalFilters() within FilterConfig.cs. However, there is one page I'd like to allow access to - the NotAuthorized page. In the HomeController, I have created the following ActionResult method:
[AllowAnonymous]
public ActionResult NotAuthorized()
{
return View();
}
Being unauthorized does lead the user to this view, but it results in a redirect loop (likely because the filter is still being run on this page).
How can I allow anonymous users to access this page?
You need to check for the attribute in your custom filter.
Try:
if (!filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), false)
&& !filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), false)
&& (user == null || !user.Active))
{
//....
}
Check for the AllowAnonymousAttribute in your custom filter. Here is one, resuable way to do it.
Add the following extension method.
public static class MyExtensionMethods
{
public static bool HasAttribute(this ActionExecutingContext context, Type attribute)
{
var actionDesc = context.ActionDescriptor;
var controllerDesc = actionDesc.ControllerDescriptor;
bool allowAnon =
actionDesc.IsDefined(attribute, true) ||
controllerDesc.IsDefined(attribute, true);
return allowAnon;
}
}
Then use it in your filter.
public class MyActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// use the extension method in your filter
if (filterContext.HasAttribute(typeof(AllowAnonymousAttribute)))
{
// exit early...
return;
}
// ...or do whatever else you need to do
if (user == null || !user.Active)
{
filterContext.Result =
new RedirectToRouteResult(new RouteValueDictionary
{
{ "controller", "Home" },
{ "action", "NotAuthorized" }
});
}
base.OnActionExecuting(filterContext);
}
}
Here is a fiddle that implements a solution.