Adding a WEB API method ruins my SWAGGER UI - asp.net-web-api2

This first method is fine. But when I add the second method the body of the SWAGGER UI is a bunch of html gibberish. And I creating the route the wrong way?
// GET api/checklist/1288
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
var model = _checkListService.Get(id);
return Ok(model);
}
// http://localhost:64783/api/checklist/GetDelinquentItems?id=1288
[Route("GetDelinquentItems")]
public async Task<IActionResult> GetDelinquentItems(int id)
{
var model = _checkListService.GetDelinquentItems(id);
return Ok(model);
}

That 'html gibberish' (indeed not the most elegant way to show an error) still contains some useful information. The first line says:
500 internal server error
and in the last three lines you can read:
Ambiguos HTTP method for action...CheckListController.GetDelinquentItems... Actions require explicit HttpMethod binding for Swagger
therefore another
[HttpGet("{id}")]
before the GetDelinquentItems() method should solve the problem.

Related

How to return a status code from an endpoint that can then be handled by app.UseStatusCodePages() middleware?

If I return StatusCode(403) or any other error code from an endpoint, any configuration of app.UseStatusCodePages<whatever> will be ignored.
I believe this is because the StatusCode(<whatever>) will automatically create a result object, and UseStatusCodePages only kicks in if there is an error status code and no content.
So how do I set a status code result in an IActionResult type endpoint and then return without setting any content so that UseStatusCodePages will handle the job of providing a suitable resonse?
As far as I know, the UseStatusCodePages will just be fired when the action result is the StatusCodeResult.
If you put some value inside the status codes, it will return the object result which will not trigger the UseStatusCodePages.
So I suggest you could directly use StatusCodeResult(403), then if you want to put some value to the StatusCodeResult, I suggest you could put it inside the httpcontext's item.
More details, you could refer to below codes:
public IActionResult OnGet()
{
HttpContext.Items.Add("test","1");
return StatusCode(403);
}
Program.cs:
app.UseStatusCodePages(async statusCodeContext =>
{
var status = statusCodeContext.HttpContext.Items["test"];
// using static System.Net.Mime.MediaTypeNames;
statusCodeContext.HttpContext.Response.ContentType = Text.Plain;
await statusCodeContext.HttpContext.Response.WriteAsync(
$"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});
Result:
The issue was that I have the ApiController attribute on the endpoint controller. One of the things this attribute does is to automatically create a ProblemDetails response body for any failed requests, and it is this that prevents UseStatusCodePages from having any effect.
The solution is to either remove the ApiController attribute if you do not require any of its features, or alternatively its behaviour of automatically creating ProblemDetails responses can be disabled using the following configuration in Program.cs (or Startup.cs in old style projects).
builder.Services.AddControllers().ConfigureApiBehaviorOptions(options =>
{
options.SuppressMapClientErrors = true;
});

Loding pages by posting parameters

The subject might not be clear since I couldn't find a better way to express it.
I am developing a web application using ASP.NET Core 6.0 with Razor Pages. Our previous application was an SPA using Ext JS where any call to server was returning only data and where I was also able to make any kind of call (GET/POST) to get the data.
For example, in the above picture from my old application, I make an ajax call with POST to get the list of periods when I open this page. I make a POST because I am sending the period type in my request payload. Sure I can pass these parameters in a GET request, however my other views have many criteria, so passing these criteria in the query string is not what I want. So, I decided to make it a standard to make my calls with POST method if there are any criteria payload, make GET request only when fething an entity with a simple key parameter (like Id) or GET any list that doesn't have any criteria.
Now, I am quite confused how to do same thing in my new ASP.NET Core Razor Pages web application. Normally, the menu items navigate to the page using link as below, which makes a GET request:
<a asp-area="System" asp-page="/ProfessionList">#AppLocalizer["Profession List"]</a>
<a asp-area="System" asp-page="/PeriodList">#AppLocalizer["Profession List"]</a>
In order to make a POST request, I replaced the menu item for period list as following which makes a POST request with a default periodType payload:
<a asp-area="System" asp-page="/ProfessionList">#AppLocalizer["Profession List"]</a>
<form asp-area="System" asp-page="/PeriodList" method="post">
<input type="hidden" name="periodType" value="1" hidden />
<button type="submit" >#AppLocalizer["Period List"]</button>
</form>
And the corresponding PeriodType.cshtml.cs file is as following:
[Authorize]
public class PeriodListModel: BaseEntityListPageModel<List<JsonPeriodEx>> {
public PeriodListModel(ILogger<BaseEntityListPageModel<List<JsonPeriodEx>>> logger, WebApi webApi) : base(logger, webApi) {
}
public IActionResult OnGet() {
PageData = JsonConvert.DeserializeObject<List<JsonPeriodEx>>(TempData["PageData"].ToString());
return Page();
}
public async Task<IActionResult> OnPostAsync(int periodType) {
var jsonResult = await _WebApi.DoPostAsync<List<JsonPeriodEx>>("/PeriodEx/GetList", new[] { new { Property = "periodType", Value = periodType } });
if (jsonResult.IsLoggedOut)
return RedirectToPage("/Login", new { area = "Account" });
if (jsonResult.Success) {
PageData = jsonResult.Data;
TempData["PageData"] = JsonConvert.SerializeObject(PageData);
return RedirectToPage("/PeriodList");
} else {
return RedirectToPage("/Error");
}
}
}
OnPostAsync successfully binds to the posted periodType parameter and gets the list of periods. Now, at the end of a successful call I want to follow the Post/Redirect/Get pattern and redirect to OnGet with the data from OnPostAsync, which is stored in TempData.
Now, according to the above scenario, is my approach, explained above, correct or should I implement it differently?
Thanks in advance
For these cases I would prefer TempData. Much easier and less code.
public async Task OnGet()
{
TempData["myParamToPass"] = 999;
...
}
public async Task OnPostReadData()
{
if (TempData.ContainsKey("myParamToPass"))
{
var myParamToPassValue = TempData.Peek("myParamToPass") as int?;
...
}
...
}

Alternate Model Binding On Controllers Post Action

EDIT: You can safely clone https://github.com/vslzl/68737969 and run the example. If you send
{
"intPropB":3
}
as POST body to: http://localhost:5000/api/Test
you'll see it binds clearly B object to A object.
#EDIT END
I'd like to implement alternate model binding using asp.net core 5 As you will see below, I have two alternative classes to bind at a single endpoint. Each request can contains only a valid A model or B model and A and B has different properties.
[HttpPost(Name = Add)]
public async Task<IActionResult> AddAsync(CancellationToken ct)
{
var aModel= new A();
var bModel= new B();
if (await TryUpdateModelAsync<A>(aModel))
{
_logger.LogDebug($"Model binded to A");
return Ok(aModel);
}
else if (await TryUpdateModelAsync<B>(bModel))
{
_logger.LogDebug($"Model binded to B");
return Ok(bModel);
}
_logger.LogDebug("Nothing binded!");
return BadRequest();
}
But this approach failed. Is there a proper way to implement this kind of solution?
By the way I'm using this to reduce complexity of my endpoints, I want to update a record partially and by doing this, each model will map the same record but with different logics.
Any suggestions will be appreciated. Thanks.
Couldn't manage to provide elegant way to do this but here is what I've done so far, It works but seems a little ugly in my opinion.
I changed the action method to this:
[HttpPost(Name = Add)]
[AllowAnonymous]
public IActionResult AddSync([FromBody] JObject body)
{
var aModel = JsonConvert.DeserializeObject<A>(body.ToString());
var bModel = JsonConvert.DeserializeObject<B>(body.ToString());
ModelState.Clear();
var aValid = TryValidateModel(aModel);
ModelState.Clear();
var bValid = TryValidateModel(bModel);
// some logic here...
return Ok(new {aModel,bModel, aValid, bValid });
}
I think It's not a good idea to interfere with ModelState this way.
But here is what I've done,
get the raw body of the request as JObject (requires Newtonsoft.Json package)
Parsed that json value to candidate objects
Check their validity via modelstate and decide which one to use.

ProducesAttribute causes "No output formatter was found for content types"

Consider this simple controller action:
[HttpGet("{imageId}")]
[ResponseCache(Duration = 604800)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Produces("image/jpeg")]
public async Task<IActionResult> GetImageAsync(int imageId)
{
if (imageId <= 0)
{
return NotFound();
}
byte[] imageBytes = await _imageProvider.GetImageAsync(
imageId,
Request.HttpContext.RequestAborted).ConfigureAwait(false);
if (imageBytes is null)
{
return NotFound();
}
return File(imageBytes, MediaTypeNames.Image.Jpeg);
}
This method works fine, however in telemetry I am getting this for every call:
Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor: Warning: No output formatter was found for content types 'image/jpeg, image/jpeg' to write the response.
Simply commenting out the ProducesAttribute prevents the telemetry from being logged.
I want to keep ProducesAttribute because it enables my Swagger UI page to work. In Swagger it shows this API has an expected output media type of image/jpeg. If I remove the attribute it changes to text/plain and it doesn't render correctly.
So the question is, how can I fix this controller action to not create a bunch of unnecessary telemetry, while also allowing Swagger UI to work correctly?

How to work around "A catch-all parameter can only appear as the last segment of the route template."

If I have a controller with an action method that uses attribute based routing and declare it like this, all is well:
[HttpGet]
[Route("/dev/info/{*somevalue}")]
public IActionResult Get(string somevalue) {
return View();
}
I can route to the above action method for example by specifying a url that ends in /dev/info/hello-world or /dev/info/new-world
However my business requirement is to have a urls that look like this: /dev/hello-world/info or /dev/new-world/info And there is an endless set of such urls that all need to route to the same action method on the controller.
I thought to set up the attribute based route on the action method as follows:
[HttpGet]
[Route("/dev/{*somevalue}/info/")]
public IActionResult Get(string somevalue) {
return View();
}
But when I do that I get the following error as soon as the web project runs:
An unhandled exception occurred while processing the request.
RouteCreationException: The following errors occurred with attribute routing information:
For action: 'App.SomeController.Get (1-wwwSomeProject)'
Error: A catch-all parameter can only appear as the last segment of the route template.
Parameter name: routeTemplate
Microsoft.AspNetCore.Mvc.Internal.AttributeRoute.GetRouteInfos(IReadOnlyList actions)
There has to be some way to work around this error. Know a way?
Middleware is the way to achieve this.
If you need an api response is easy to implement inline.
if (app.Environment.IsDevelopment())
{
app.Use(async (context, next) =>
{
Console.WriteLine(context.Request.Path);
if (context.Request.Path.ToString().EndsWith("/info"))
{
// some logic
await context.Response.WriteAsync("Terminal Middleware.");
return;
}
await next(context);
});
}
If you need to call a controller you can simply edit request path via middleware to achieve your requirement.
You can find an example here: https://stackoverflow.com/a/50010787/3120219
It is possible to achieve this by using the regular expression:
[HttpGet]
[Route(#"/dev/{somevalue:regex(^.*$)}/info/")]
public IActionResult Get(string somevalue)
{
return View();
}
About routing constrain using the regular expressions see in the documentation: Route constraint reference
The regular expression tokens explanation:
Token
Explanation
^
Asserts position at start of a line
.
Matches any character (except for line terminators)
*
Matches the previous token between zero and unlimited times, as many times as possible
$
Asserts position at the end of a line
If it's required to have the “world”suffix in the second segment then add this suffix to the pattern like the following: [Route(#"/dev/{somevalue:regex(^.*world$)}/info/")].