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 …)를 이어서 작성하겠습니다.