http 요청을 처리할 때 클라이언트와 서버는 처리에 대한 동작을 지정할 수 있습니다.
대표적으로 자원에 대한 조회, 생성, 수정, 삭제를 위한 GET, POST, PUT, PATCH, DELETE 등이 있고, controller에서 이를 처리하기 위한 방법을 알아보겠습니다.
프레임워크에서는 http 요청을 처리하기 위해 여러가지 attribute를 제공하고 있습니다.
HTTP Method 처리를 위한 Attribute
- [HttpGet]
- [HttpPost]
- [HttpPut]
- [HttpPatch]
- [HttpDelete]
- 등등…
HTTP 요청 매개변수 바인딩
- [FromQuery]
- [FromRoute]
- [FromBody]
- [FromForm]
- [FromHeader]
controller에서는 위 attribute를 사용하여 요청을 처리할 수 있습니다.
[ApiController]
[Route("api/[controller]")]
public class FooController : Controller
{
public ActionResult Test(string str)
{
return Ok($"response - {str}");
}
}
하지만 attribute가 없어도 http 요청에 대한 처리를 할 수 있습니다. 실행을 하게 되면 swagger docs에서 에러가 나지만 http 요청을 진행하면 정상적으로 처리되는 모습을 볼 수 있습니다.
GET http://localhost:5000/api/Foo?str=test
// response
response - test
POST http://localhost:5000/api/Foo?str=test
// response
response - test
... 모든 HTTP method에 대해 처리
기본적으로 public 메서드가 존재한다면 모든 요청에 대해 라우팅이 되지만 public 메서드가 여러 개라면 AmbiguousMatchException 예외가 발생하면서 요청을 처리할 수 없게 됩니다.
[ApiController]
[Route("api/[controller]")]
public class FooController : Controller
{
public ActionResult Test(string str)
{
return Ok($"response - {str}");
}
public ActionResult Test2(string str)
{
return Ok($"response2 - {str}");
}
}
GET http://localhost:5000/api/Foo?str=test
// response - 500 error
Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints. Matches:
WebAPITutorial.Controllers.FooController.Test (WebAPITutorial)
WebAPITutorial.Controllers.FooController.Test2 (WebAPITutorial)
at Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.ReportAmbiguity(Span`1 candidateState)
at Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.ProcessFinalCandidates(HttpContext httpContext, Span`1 candidateState)
at Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.Select(HttpContext httpContext, Span`1 candidateState)
at Microsoft.AspNetCore.Routing.Matching.DfaMatcher.MatchAsync(HttpContext httpContext)
at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.Invoke(HttpContext httpContext)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
HEADERS
=======
Accept: application/xml
Connection: keep-alive
Host: localhost:5000
User-Agent: PostmanRuntime/7.29.2
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
Content-Length: 25
Postman-Token: 748a997b-84fe-47a7-a6cd-adf5596a61b9
위와 같은 상황을 방지하고, 적절한 http 요청을 처리하기 위해 HTTP Method attribute를 메서드 별로 정의해야 합니다.
HttpGet - 자원 조회
[ApiController]
[Route("api/[controller]")]
public class FooController : Controller
{
private readonly static List<Student> _students =
[
new ("Json", 15),
new ("Mike", 21),
];
[HttpGet]
public ActionResult Get()
{
return Ok(_students);
}
}
public record Student(string Name, int Age);
GET http://localhost:5000/api/Foo
// response
[
{
"name": "Json",
"age": 15
},
{
"name": "Mike",
"age": 21
}
]
POST http://localhost:5000/api/Foo
// response
405 - Method Not Allowed
[HttpGet]을 통해 해당 endpoint로 GET 요청이 들어왔을 때 처리를 진행할 수 있습니다. 정의되지 않은 HTTP method로 요청이 온다면 controller는 405 - Method Not Allowed를 반환하게 됩니다.
action method에는 따로 endpoint를 지정할 수 있습니다. [HttpGet]을 이용하거나, [Route]를 이용할 수 있습니다.
[HttpGet("test1")]
public ActionResult Get()
{
return Ok(_students);
}
GET http://localhost:5000/api/Foo/test1
[HttpGet("test1")]
[HttpGet("test2")]
public ActionResult Get()
{
return Ok(_students);
}
GET http://localhost:5000/api/Foo/test1
GET http://localhost:5000/api/Foo/test2
[HttpGet]
[Route("test1")]
public ActionResult Get()
{
return Ok(_students);
}
GET http://localhost:5000/api/Foo/test1
[HttpGet]
[Route("test1")]
[Route("test2")]
public ActionResult Get()
{
return Ok(_students);
}
GET http://localhost:5000/api/Foo/test1
GET http://localhost:5000/api/Foo/test2
HttpGet과 Route을 이용하여 endpoint를 지정하면 swagger docs에서 에러가 발생하지만 정상적으로 요청은 가능합니다.
Path Parameter, Query string
보통 GET 요청을 진행할 때 Path parameter와 Query string을 포함하여 요청을 진행하는 경우가 있습니다.
Path parameter
action method에서 Path parameter를 받기 위해서는 [Http{Verb}] attribute의 생성자에 template를 이용하여 지정할 수 있습니다. (Route도 가능합니다.)
[HttpGet("{idx}")]
public ActionResult Get(int idx)
{
return Ok(_students[idx]);
}
GET http://localhost:5000/api/Foo/1
// response
{
"name": "Mike",
"age": 21
}
GET http://localhost:5000/api/Foo/a
// response
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"id": [
"The value 'a' is not valid."
]
},
"traceId": "00-2f35d9d35192de9991b673a2f88ea276-11f7f0af47f8ed7c-00"
}
template에 {매개변수 명}를 이용하여 path parameter를 지정할 수 있고, int가 아닌 값이 들어온다면 모델 바인딩 검사로 인해 400 에러가 반환 됩니다.
만약 template에 타입을 명시한다면 404 not found를 반환할 수 있습니다.
[HttpGet("{idx:int}")]
public ActionResult Get(int idx)
{
return Ok(_students[idx]);
}
GET http://localhost:5000/api/Foo/1
// response
{
"name": "Mike",
"age": 21
}
GET http://localhost:5000/api/Foo/a
// response
404 Not Found
Query string
http 요청에서 http://localhost:5000/api/Foo?name=Mike와 같이 ? 뒤에 인자를 받기 위해서는 action method에 매개변수로 추가만 하면 됩니다.
[HttpGet]
public ActionResult Get(string name)
{
return Ok(_students.FirstOrDefault(s => s.Name == name));
}
GET https://localhost:5000/api/Foo?name=Mike
// response
{
"name": "Mike",
"age": 21
}
GET https://localhost:5000/api/Foo?name2=Mike
// response
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"name": [
"The name field is required."
]
},
"traceId": "00-8c40f209924f043b4b7c60b3b9d1bfee-6c403580a33fbbc2-00"
}
Query string에 담긴 key가 매개변수 명칭에 매핑이 됩니다. 매개변수에 정의한 key가 Query string에 없는 경우 required로 설정되어 있기 때문에 모델 바인딩 에러가 발생합니다.
이 경우는 매개변수를 nullable로 처리하거나, 기본 값을 지정하여 optional로 설정할 수 있습니다.
[HttpGet]
public ActionResult Get(string? name)
{
return Ok(_students.FirstOrDefault(s => s.Name == name));
}
// response
204 - No Content
[HttpGet]
public ActionResult Get(string name = "Json")
{
return Ok(_students.FirstOrDefault(s => s.Name == name));
}
GET https://localhost:5000/api/Foo?name2=Mike
// response
{
"name": "Json",
"age": 15
}
action method의 매개변수는 [ApiController]를 통해 매개변수 바인딩이 자동으로 이루어지게 됩니다.
기본적으로 아무런 바인딩을 명시하지 않은 경우, int, double과 같은 타입은 [FromQuery]가 기본으로 설정됩니다.
path parameter 또한 template에서 정의한 명칭으로 [FromRoute]로 자동적으로 바인딩이 되게 되는데, 바인딩 같은 경우는 명시를 해주는 것이 유지보수성에 좋다고 생각합니다.
[HttpGet("{id:int}")]
public ActionResult Get([FromRoute] int id, [FromQuery] string filter)
{
...
}
내용이 많아 다음 HTTP method (POST, DELETE …)를 이어서 작성하겠습니다.