Compare commits

...

6 Commits

Author SHA1 Message Date
Gregory Paidis
4bbe613ea6 Add a SE-like example 2024-04-10 14:58:17 +02:00
Gregory Paidis
a3a542a32f Fix rule adoc 2024-04-10 14:58:09 +02:00
Gregory Paidis
f554b6cbb5 Mark as noncompliant the edge cases 2024-04-10 14:42:16 +02:00
Gregory Paidis
b3111107ed Add c# examples 2024-04-09 17:16:38 +02:00
Gregory Paidis
01baaa9f00 Refactor rule+metadata 2024-04-09 14:41:08 +02:00
gregory-paidis-sonarsource
1fdb02e153 Create rule S6963 2024-04-08 10:53:44 +00:00
6 changed files with 851 additions and 0 deletions

View File

@ -0,0 +1,434 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
WebApplication app = null;
app.UseSwagger(); // Necessary for the rule to raise
[ApiController]
public class AllTargetCodes : Controller
{
#region COMPLIANT
[HttpGet("foo")]
public IActionResult ReturnsOk() => Ok(); // Compliant - 200
[HttpGet("foo")]
public IActionResult ReturnsOk2() => Ok(null); // Compliant - 200
[HttpGet("foo")]
public IActionResult ReturnsContent() => Content(""); // Compliant - 200
[HttpGet("foo")]
public IActionResult ReturnsContent2() => Content("", ""); // Compliant - 200
[HttpGet("foo")]
public IActionResult ReturnsContent3() => Content("", default(MediaTypeHeaderValue)); // Compliant - 200
[HttpGet("foo")]
public IActionResult ReturnsContent4() => Content("", "", null); // Compliant - 200
public IActionResult ReturnFile() => File("", ""); // Compliant - 200
public IActionResult ReturnPhysicalFile() => PhysicalFile("", ""); // Compliant - 200
#endregion
#region 2XX
[HttpGet("foo")]
public IActionResult ReturnsCreated() => // Noncompliant - 201
Created(); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsCreated2() => // Noncompliant - 201
Created("uri", null); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsCreated3() => // Noncompliant - 201
Created(default(Uri), null); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsCreatedAtAction() => // Noncompliant - 201
CreatedAtAction("actionName", null); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsCreatedAtAction2() => // Noncompliant - 201
CreatedAtAction("actionName", null, null); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsCreatedAtAction3() => // Noncompliant - 201
CreatedAtAction("actionName", "controllerName", null, null); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsCreatedAtRoute() => // Noncompliant - 201
CreatedAtRoute("routeName", null); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsCreatedAtRoute2() => // Noncompliant - 201
CreatedAtRoute(null, null); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsCreatedAtRoute3() => // Noncompliant - 201
CreatedAtRoute("routeName", null, null); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsAccepted() => // Noncompliant - 202
Accepted(); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsAccepted2() => // Noncompliant - 202
Accepted(default(object)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsAccepted3() => // Noncompliant - 202
Accepted(default(Uri)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsAccepted4() => // Noncompliant - 202
Accepted("uri"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsAccepted5() => // Noncompliant - 202
Accepted("uri", null); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsAccepted6() => // Noncompliant - 202
Accepted(default(Uri), null); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsAcceptedAtAction() => // Noncompliant - 202
AcceptedAtAction("actionName"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsAcceptedAtAction2() => // Noncompliant - 202
AcceptedAtAction("actionName", "controllerName"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsAcceptedAtAction3() => // Noncompliant - 202
AcceptedAtAction("actionName", default(object)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsAcceptedAtAction4() => // Noncompliant - 202
AcceptedAtAction("actionName", "controllerName", null); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsAcceptedAtAction5() => // Noncompliant - 202
AcceptedAtAction("actionName", "controllerName", null, null); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsAcceptedAtRoute() => // Noncompliant - 202
AcceptedAtRoute(default(object)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsAcceptedAtRoute2() => // Noncompliant - 202
AcceptedAtRoute("routeName"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsAcceptedAtRoute3() => // Noncompliant - 202
AcceptedAtRoute("routeName", null); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsAcceptedAtRoute4() => // Noncompliant - 202
AcceptedAtRoute(default(object), null); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsAcceptedAtRoute5() => // Noncompliant - 202
AcceptedAtRoute("routeName", null, null); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsNoContent() => // Noncompliant - 204
NoContent(); // Secondary
#endregion
#region 3XX
[HttpGet("foo")]
public IActionResult ReturnsRedirectPermanent() => // Noncompliant - 301
RedirectPermanent("url"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsLocalRedirectPermanent() => // Noncompliant - 301
LocalRedirectPermanent("url"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnRedirectToActionPermanent() => // Noncompliant - 301
RedirectToActionPermanent(""); // Secondary
[HttpGet("foo")]
public IActionResult ReturnRedirectToActionPermanent2() => // Noncompliant - 301
RedirectToActionPermanent("", default(object)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnRedirectToActionPermanent3() => // Noncompliant - 301
RedirectToActionPermanent("", ""); // Secondary
[HttpGet("foo")]
public IActionResult ReturnRedirectToActionPermanent4() => // Noncompliant - 301
RedirectToActionPermanent("", "", default(object)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnRedirectToActionPermanent5() => // Noncompliant - 301
RedirectToActionPermanent("", "", ""); // Secondary
[HttpGet("foo")]
public IActionResult ReturnRedirectToActionPermanent6() => // Noncompliant - 301
RedirectToActionPermanent("", "", "", ""); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToRoutePermanent() => // Noncompliant - 302
RedirectToRoutePermanent("url"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToRoutePermanent2() => // Noncompliant - 302
RedirectToRoutePermanent(default(object)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToRoutePermanent3() => // Noncompliant - 302
RedirectToRoutePermanent("", default(object)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToRoutePermanent4() => // Noncompliant - 302
RedirectToRoutePermanent("", ""); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToRoutePermanent5() => // Noncompliant - 302
RedirectToRoutePermanent("", "", ""); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToPagePermanent() => // Noncompliant - 301
RedirectToPagePermanent("url"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToPagePermanent2() => // Noncompliant - 301
RedirectToPagePermanent("", default(object)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToPagePermanent3() => // Noncompliant - 301
RedirectToPagePermanent("", ""); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToPagePermanent4() => // Noncompliant - 301
RedirectToPagePermanent("", "", ""); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToPagePermanent5() => // Noncompliant - 301
RedirectToPagePermanent("", "", "", ""); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToPage() => // Noncompliant - 302
RedirectToPage("url"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToPage2() => // Noncompliant - 302
RedirectToPage("", default(object)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToPage3() => // Noncompliant - 302
RedirectToPage("", ""); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToPage4() => // Noncompliant - 302
RedirectToPage("", "", default(object)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToPage5() => // Noncompliant - 302
RedirectToPage("", "", ""); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToPage6() => // Noncompliant - 302
RedirectToPage("", "", "", ""); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirect() => // Noncompliant - 302
Redirect("url"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsLocalRedirect() => // Noncompliant - 302
LocalRedirect("url"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToAction() => // Noncompliant - 302
RedirectToAction(); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToAction2() => // Noncompliant - 302
RedirectToAction("actionName"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToAction3() => // Noncompliant - 302
RedirectToAction("actionName", default(object)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToAction4() => // Noncompliant - 302
RedirectToAction("actionName", "controllerName"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToAction5() => // Noncompliant - 302
RedirectToAction("actionName", "controllerName", default(object)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToAction6() => // Noncompliant - 302
RedirectToAction("actionName", "controlloerName", "fragment"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToAction7() => // Noncompliant - 302
RedirectToAction("", "", "", ""); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToRoute() => // Noncompliant - 302
RedirectToRoute("url"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToRoute2() => // Noncompliant - 302
RedirectToRoute(default(object)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToRoute3() => // Noncompliant - 302
RedirectToRoute("", default(object)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToRoute4() => // Noncompliant - 302
RedirectToRoute("", ""); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToRoute5() => // Noncompliant - 302
RedirectToRoute("", "", ""); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToRoutePreserveMethod() => // Noncompliant - 307
RedirectToRoutePreserveMethod(); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectPreserveMethod() => // Noncompliant - 307
RedirectPreserveMethod("url"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsLocalRedirectPreserveMethod() => // Noncompliant - 307
LocalRedirectPreserveMethod(""); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToActionPreserveMethod() => // Noncompliant - 307
RedirectToActionPreserveMethod(); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToPagePreserveMethod() => // Noncompliant - 307
RedirectToPagePreserveMethod(""); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectPermanentPreserveMethod() => // Noncompliant - 308
RedirectPermanentPreserveMethod("url"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsLocalRedirectPermanentPreserveMethod() => // Noncompliant - 308
LocalRedirectPermanentPreserveMethod("url"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToActionPermanentPreserveMethod() => // Noncompliant - 308
RedirectToActionPermanentPreserveMethod(); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToRoutePermanentPreserveMethod() => // Noncompliant - 308
RedirectToRoutePermanentPreserveMethod(); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsRedirectToPagePermanentPreserveMethod() => // Noncompliant - 308
RedirectToPagePermanentPreserveMethod(""); // Secondary
#endregion
#region 4XX
[HttpGet("foo")]
public IActionResult ReturnsBadRequest() => // Noncompliant - 400
BadRequest(); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsValidationProblem() => // Noncompliant - 400
ValidationProblem(); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsValidationProblem2() => // Noncompliant - 400
ValidationProblem(default(ValidationProblemDetails)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsValidationProblem3() => // Noncompliant - 400
ValidationProblem(default(ModelStateDictionary)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsUnauthorized() => // Noncompliant - 401
Unauthorized(); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsChallenge() => // Noncompliant - [depends on the configured IAuthenticationService, usually 401 or 403]
Challenge(); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsChallenge2() => // Noncompliant - [depends on the configured IAuthenticationService, usually 401 or 403]
Challenge("scheme1", "scheme2"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsChallenge3() => // Noncompliant - [depends on the configured IAuthenticationService, usually 401 or 403]
Challenge(default(AuthenticationProperties)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsChallenge4() => // Noncompliant - [depends on the configured IAuthenticationService, usually 401 or 403]
Challenge(default(AuthenticationProperties), "scheme1", "scheme2"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsForbid() => // Noncompliant - usually 403
Forbid(); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsForbid2() => // Noncompliant - usually 403
Forbid("scheme1", "scheme2"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsForbid3() => // Noncompliant - usually 403
Forbid(default(AuthenticationProperties)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsForbid4() => // Noncompliant - usually 403
Forbid(default(AuthenticationProperties), "scheme1", "scheme2"); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsNotFound() => // Noncompliant - 404
NotFound(); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsNotFound2() => // Noncompliant - 404
NotFound(null); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsValidationProblem4() => // Noncompliant - 404
ValidationProblem(statusCode: 404); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsConflict() => // Noncompliant - 409
Conflict(); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsConflict2() => // Noncompliant - 409
Conflict(default(object)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsConflict3() => // Noncompliant - 409
Conflict(default(ModelStateDictionary)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsUnprocessableEntity() => // Noncompliant - 422
UnprocessableEntity(); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsUnprocessableEntity2() => // Noncompliant - 422
UnprocessableEntity(default(object)); // Secondary
[HttpGet("foo")]
public IActionResult ReturnsUnprocessableEntity3() => // Noncompliant - 422
UnprocessableEntity(default(ModelStateDictionary)); // Secondary
#endregion
}

View File

@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Mvc;
WebApplication app = null;
// UseSwagger is not called
namespace Things
{
[ApiController]
public class Baseline : ControllerBase
{
[HttpGet("foo")]
public IActionResult NoAttribute() => BadRequest(); // Compliant, not in Swagger context
}
}

View File

@ -0,0 +1,259 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
WebApplication app = null;
app.UseSwagger(); // Necessary for the rule to raise
namespace Things
{
[ApiController]
public class Baseline : ControllerBase
{
[HttpGet("foo")]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public IActionResult HasCorrectAttribute() => BadRequest(); // Compliant
[HttpGet("foo")]
[ProducesResponseType(400)]
public IActionResult HasCorrectAttributeWithNumber() => BadRequest(); // Compliant
[HttpGet("foo")]
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
public IActionResult HasCorrectAttributeWithTpe() => BadRequest(); // Compliant
[HttpGet("foo")]
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest, "application/xml")]
public IActionResult HasCorrectAttributeWithContentType() => BadRequest(); // Compliant
[HttpGet("foo")]
[ProducesResponseType<int>(StatusCodes.Status400BadRequest)]
public IActionResult HasCorrectAttributeGeneric() => BadRequest(); // Compliant
[HttpGet("foo")]
[ProducesResponseType<int>(StatusCodes.Status400BadRequest, "application/json")]
public IActionResult HasCorrectAttributeWithContentTypeGeneric() => BadRequest(); // Compliant
[HttpGet("foo")]
public IActionResult NoAttribute() => // Noncompliant {{Annotate this method with ProducesResponseType for "BadRequest".}}
// ^^^^^^^^^^^
BadRequest(); // Secondary
// ^^^^^^^^^^
[HttpGet("foo")]
[ProducesResponseType(StatusCodes.Status418ImATeapot)]
public IActionResult HasWrongAttribute() => // Noncompliant {{Annotate this method with ProducesResponseType for "BadRequest".}}
// ^^^^^^^^^^^^^^^^^
BadRequest(); // Secondary
// ^^^^^^^^^^
[HttpGet("foo")]
[ProducesResponseType(418)]
public IActionResult HasAttributeWithWrongNumber() => // Noncompliant {{Annotate this method with ProducesResponseType for "BadRequest".}}
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^
BadRequest(); // Secondary
// ^^^^^^^^^^
[HttpGet("foo")]
[ProducesResponseType(typeof(object), StatusCodes.Status418ImATeapot)]
public IActionResult HasWrongAttributeWithType() => // Noncompliant
BadRequest(); // Secondary
[HttpGet("foo")]
[ProducesResponseType(typeof(object), StatusCodes.Status418ImATeapot), "application/xml"]
public IActionResult HasWrongAttributeWithContentType() => // Noncompliant
BadRequest(); // Secondary
[HttpGet("foo")]
[ProducesResponseType<int>(StatusCodes.Status418ImATeapot)]
public IActionResult HasWrongAttributeGeneric() => // Noncompliant
BadRequest(); // Secondary
[HttpGet("foo")]
[ProducesResponseType<int>(StatusCodes.Status418ImATeapot, "application/json")]
public IActionResult HasWrongAttributeWithContentTypeGeneric() => // Noncompliant
BadRequest(); // Secondary
[Route("foo")]
public IActionResult NoAttributeWithRoute() => // Noncompliant {{Annotate this method with ProducesResponseType for "BadRequest".}}
BadRequest(); // Secondary
/// <response code="400">Some text</response>
[HttpGet("foo")]
public IActionResult AnnotatedWithXml() => BadRequest();
[HttpGet("foo")]
/// <response code="400">this does not work</response>
public IActionResult AnnotatedErroneouslyWithXml() => // Noncompliant, response has to be above the attribute.
BadRequest();
[HttpGet("foo")]
protected IActionResult IsNotPublic() => BadRequest(); // Compliant, only public methods are considered
}
[ApiController]
internal class NotPublic : ControllerBase
{
[HttpGet("foo")]
public IActionResult NoAttribute() => BadRequest(); // Compliant, only public classes are considered
}
[NonController]
[ApiController]
public class NotAController : ControllerBase
{
[HttpGet("foo")]
public IActionResult NoAttribute() => BadRequest(); // Compliant, excluded by the attribute
}
public class NotApiController : ControllerBase
{
[HttpGet("foo")]
public IActionResult NoAttribute() => BadRequest(); // Compliant, only classes annotated with "ApiController" are considered
}
[ApiController]
public class NestedMethodCall: ControllerBase
{
[HttpGet("foo")]
public IActionResult NoAttribute() => Passthrough(); // Compliant FN, we only consider the current method
private IActionResult Passthrough() => BadRequest();
}
[ApiController]
public class MultipleErrorCodes : Controller
{
[HttpGet("foo")]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public IActionResult MissOne(bool condition) => // Noncompliant {{Annotate this method with ProducesResponseType for "NotFound".}}
// ^^^^^^^
condition ? NotFound() : BadRequest(); // Secondary
// ^^^^^^^^
[HttpGet("foo")]
[ProducesResponseType(StatusCodes.Status302Found)]
public IActionResult MissesMultiple(bool condition) // Noncompliant [badRequest] {{Annotate this method with ProducesResponseType for "BadRequest".}}
// ^^^^^^^^^^^^^^
// ^^^^^^^^^^^^^^ @-2 [notFound] {{Annotate this method with ProducesResponseType for "NotFound".}}
{
if (condition)
{
return NotFound(); // Secondary [notFound]
// ^^^^^^^^
}
else if (condition)
{
return BadRequest(); // Secondary [badRequest]
// ^^^^^^^^^^
}
return Redirect("url");
}
}
[ApiController]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public class AnnotatedAtControllerLevel : ControllerBase
{
[HttpGet("foo")]
public IActionResult Valid() => BadRequest(); // Compliant
[HttpGet("foo")]
public IActionResult Invalid() => // Noncompliant {{Annotate this method with ProducesResponseType for "NotFound".}}
// ^^^^^^^
NotFound(); // Secondary
// ^^^^^^^^
}
[ApiController]
public class MixedAnnotations : Controller
{
/// <response code="404">Some text</response>
[HttpGet("foo")]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public IActionResult MarkedWithBothXmlAndAttribute(bool condition) => // Compliant
condition ? BadRequest() : NotFound();
/// <response code="307">Not found</response>
/// <response code="409">Conflict</response>
[HttpGet("foo")]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public IActionResult MarkedWithBothXmlAndAttribute(int condition) => // Noncompliant {{Annotate this method with ProducesResponseType for "NotFound".}}
condition switch
{
1 => RedirectPreserveMethod("url"),
2 => NotFound(), // Secondary
// ^^^^^^^^
3 => BadRequest(),
4 => Conflict(),
};
}
[ApiController]
public class NotReturns : ControllerBase
{
[HttpGet("foo")]
public IActionResult SimpleOk(bool condition) // Compliant, only return statements are checked
{
var x1 = BadRequest();
IActionResult x2 = condition ? NotFound() : NoContent();
var lambda = () => Redirect("url");
return Ok();
}
[HttpGet("foo")]
public IActionResult NestedErrorCodes() // Noncompliant {{Annotate this method with ProducesResponseType for "NotFound".}}
{
return Ok(BadRequest()); // Conmpliant, we only consider the top node of the return expression
return Ok(Redirect("url")); // Compliant, same as above
return NotFound(); // Secondary
// ^^^^^^^^
}
}
// For the implementation: If this seems too cumbersome, consider dropping it and documenting it as FN
[ApiController]
public class ExplicitErrorCodes_NewObjects : Controller
{
[HttpGet("foo")]
public ObjectResult NewObjectResult() => // Noncompliant
new ObjectResult(42) { StatusCode = StatusCodes.Status418ImATeapot }; // Secondary
[HttpGet("foo")]
public StatusCodeResult NewStatusCodeResult() => // Noncompliant
new StatusCodeResult(statusCode: 42); // Secondary
[HttpGet("foo")]
public IActionResult NewContentResult() => // Noncompliant
new ContentResult { StatusCode = StatusCodes.Status418ImATeapot }; // Secondary
[HttpGet("foo")]
public IActionResult NewRedirectResult() => // Noncompliant
new RedirectResult("url", permanent: true, preserveMethod: false); // Secondary
}
[ApiController]
public class ExplicitErrorCodes_Methods : ControllerBase
{
[HttpGet("foo")]
public IActionResult StatusCodeMethod() => // Noncompliant
StatusCode(StatusCodes.Status404NotFound); // Secondary
[HttpGet("foo")]
public IActionResult StatusCodeMethodWithValue() => // Noncompliant
StatusCode(statusCode: 42, null);
[HttpGet("foo")]
public IActionResult ProblemMethodWithStatusCode() => // Noncompliant
Problem(statusCode: StatusCodes.Status418ImATeapot); // Secondary
[HttpGet("foo")]
public IActionResult ProblemMethodWithoutStatusCode() => // Compliant
Problem();
}
}

View File

@ -0,0 +1,24 @@
{
"title": "Actions that return non-200 status codes should be annotated with ProducesResponseTypeAttribute",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "5min"
},
"tags": [
"asp.net"
],
"defaultSeverity": "Major",
"ruleSpecification": "RSPEC-6963",
"sqKey": "S6963",
"scope": "Main",
"defaultQualityProfiles": ["Sonar way"],
"quickfix": "targeted",
"code": {
"impacts": {
"MAINTAINABILITY": "HIGH"
},
"attribute": "CLEAR"
}
}

View File

@ -0,0 +1,118 @@
In an https://learn.microsoft.com/en-us/aspnet/core[ASP.NET Core] https://en.wikipedia.org/wiki/Web_API[Web API], controller actions can return any https://en.wikipedia.org/wiki/List_of_HTTP_status_codes[HTTP status code]. If a controller action returns an unexpected status code, annotating the action with the https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesresponsetypeattribute[`++[ProducesResponseType]++`] attribute is recommended.
== Why is this an issue?
HTTP response status codes indicate the result of an HTTP request to the API's clients. The HTTP standard groups the status codes in the following way:
* `1xx` codes represent informational responses.
* `2xx` codes represent successful responses.
* `3xx` codes represent redirection.
* `4xx` codes represent client errors.
* `5xx` codes represent server errors.
For example, it is usually typical to return:
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200[200 OK] for successful requests
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/301[301 Moved Permanently] for requests that need to be redirected to a new URL, given by the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location[Location] header
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403[403 Forbidden] for unsuccessful requests that are not authorized to access the resource
When building a Web API, any combination of https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods[HTTP request method] and https://developer.mozilla.org/en-US/docs/Web/HTTP/Status[HTTP response status code] can be used. However, when deviating from the conventional HTTP status codes, or when something goes wrong and it is necessary to return a 4xx status code, it is essential to communicate the API's behavior to the clients.
If the application uses https://swagger.io/[Swagger], the API documentation will be generated based on the status codes returned by the controller actions. By default, Swashbuckle generates a `200 OK` response status code for controller actions not annotated with the `ProducesResponseType` attribute. Therefore, if the status codes are not properly annotated, the API documentation will not either, which can lead to confusion. This can be particularly problematic when the API is consumed by third-party clients, where the undocumented behavior can lead to unexpected results and bugs in the long run, without the API provider being aware of it.
This rule raises an issue on a controller action when:
* It can produce a response with a non-`200 OK` status code.
* It is not annotated with a `++[ProducesResponseType]++` attribute for this status code, either at action or controller level.
* It is not annotated with https://learn.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle#xml-comments[XML comments] for this status code.
* The application has enabled the https://learn.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle#add-and-configure-swagger-middleware[Swagger middleware].
* The controller is marked with the https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute[`++[ApiController]++`] attribute.
== How to fix it
There are two ways to fix this issue:
* Annotate the controller action with the `++[ProducesResponseType]++` attribute for the non-`200 OK` status codes.
* Annotate the controller action with XML comments for the non-`200 OK` status codes.
=== Code examples
==== Noncompliant code example
[source,csharp,diff-id=1,diff-type=noncompliant]
----
[ApiController]
[Route("api/[controller]")]
public class FoodController : ControllerBase
{
IFoodService _service;
[HttpGet("{id}")]
public IActionResult GetById(int id) // Noncompliant: Annotate this method with ProducesResponseType for "NotFound".
{
var result = _service.GetById(id);
return result is not null
? Ok(result)
: NotFound(); // 404 NotFound is not explicitly annotated
}
}
----
==== Compliant solution
[source,csharp,diff-id=1,diff-type=compliant]
----
[ApiController]
[Route("api/[controller]")]
public class FoodController : ControllerBase
{
IFoodService _service;
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetById(int id)
{
var result = _service.GetById(id);
return result is not null
? Ok(result)
: NotFound();
}
}
----
== Resources
=== Documentation
* Wikipedia - https://en.wikipedia.org/wiki/Web_API[Web API]
* Wikipedia - https://en.wikipedia.org/wiki/List_of_HTTP_status_codes[List of HTTP status codes]
* Mozilla - https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods[HTTP request methods]
* Mozilla - https://developer.mozilla.org/en-US/docs/Web/HTTP/Status[HTTP response status codes]
* Microsoft Learn - https://learn.microsoft.com/en-us/aspnet/core[ASP.NET Core]
* Microsoft Learn - https://learn.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle[Get started with Swashbuckle and ASP.NET Core]
* Microsoft Learn - https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute[ApiControllerAttribute Class]
* Microsoft Learn - https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesresponsetypeattribute[ProducesResponseTypeAttribute Class]
* SmartBear - https://swagger.io/[Swagger]
ifdef::env-github,rspecator-view[]
'''
== Implementation Specification
(visible only on this page)
=== Message
* Annotate this method with ProducesResponseType for "XXX".
(`XXX` is the method identifier causing this issue, e.g. `BadRequest` in `return BadRequest();`)
=== Highlighting
* Primary: The method name of the action. One issue per status code.
* Secondary: the return statement's method identifier (e.g. `BadRequest` in `return BadRequest();`).
'''
== Comments And Links
(visible only on this page)
endif::env-github,rspecator-view[]

View File

@ -0,0 +1,2 @@
{
}