이번 포스팅을 통해서는 ASP.NET Core Web API를 사용해 Web Service(HTTP 또는 REST services)를 구축하는 것에 관한 내용을 살펴보려고 합니다. 구축된 Web Service는 다른 website나 데스크탑 응용프로그램, 모바일 App을 포함하여 거의 대부분 유형의 HTTP client를 통해 사용될 수 있습니다.
1. ASP.NET Core Web API를 사용한 Web Service 구축하기
● web service
본래 HTTP는 웹상에서 HTML과 함께 이미지나 기타 리소스를 사용자에게 응답할 수 있도록 설계되었지만 웹 서비스를 구축하는 하나의 좋은 대안이 되기도 합니다.
Web Service의 주요 핵심이 되는 Representational State Transfer(REST) architectur는 Roy Fielding박사논문에서 언급되었으며 아래 이유로 HTTP 표준이 Web Service를 구축하기에 좋은 대안이 될 수 있다고 언급하였습니다.
- Https://locahost:3118/products/14와 같은 URI를 사용해 서버의 리소스를 식별할 수 있음.
- GET, POST, PUT, DELETE와 같은 Method를 통해 이들 리소스에 대한 일반적인 작업을 수행할 수 있음.
- 요청과 응답에서 컨텐츠를 주고받을 수 있는 XML이나 JSON과 같은 Type을 협상할 수 있음. (컨텐츠 협상은 클라이언트가 'Accept: application/xml,*/*;q=0.8'와 같이 특정 header를 요청할때 발생하며 ASP.NET Core Web API는 기본 적으로 'Content-Type: application/json; charset=utf-8'처럼 응답헤더를 만들어 JSON포멧을 응답합니다.)
Web services는 HTTP 표준을 사용하며 이러한 이유로 HTTP 또는 RESTful services라 불리기도 하는데 때로는 Simple Object Access Protocol (SOAP)를 의미하기도 합니다. 이것은 WS-* standards라 하여 근본적으로 다른 시스템상의 클라이언트와 서비스간 통신을 위한 것이며 IBM이 Microsoft와 같은 다른 회사와 함께 공동으로 정의한 표준입니다.
● Windows Communication Foundation
Windows Communication Foundation (WCF)는 .NET Framework 3.0및 이후부터 포함한 remote procedure call (RPC)기술로서 특정 시스템에서의 코드를 네트워크를 통해 다른 시스템에서 실행하도록 하는 개념입니다. WCF는 개발자가 이러한 시스템 구현을 SOAP를 포함하여 손쉽게 구현 하도록 하는 것인데 이후에는 Web/HTTP/REST 서비스를 지원하도록 되었지만 Web/HTTP/REST만을 필요로 하는 경우에는 다소 부담스러운 하나의 거대한 플렛폼이었습니다.
만약 기존에 구현된 WCF services를 최신 .NET으로 전환하고자 한다면 2021년 2월에 General Availability(GA)버전으로 릴리스된 open-source프로젝트를 참고하시기 바랍니다.
CoreWCF 0.1.0 GA Release | CoreWCF
Microsoft는 WCF의 새로운 대안으로 gRPC사용을 권장하고 있습니다. gRPC는 Google에 의해 만들어진 최신의 cross-platform open-source RPC framework입니다.
● Web API에서의 요청(Request)과 응답(Response)
HTTP는 요청표준및 표준코드를 정의하여 응답 유형을 식별하며 Web API services를 구현하는데 사용될 수 있습니다. 예를 들어 GET과 같은 가장 일반적인 요청에서는 서버의 리소스를 식별하기 위한 중복되지 않는 유일한 Path(경로)와 함께 header를 사용하여 다음과 같은 특정 Type을 추가로 지정합니다.
GET /path/to/resource
Accept: application/json
이때 응답에서는 성공 혹은 몇가지 유형의 실패를 응답할 수 있습니다.
상태코드 | 이유 |
200 OK | 요청한 경로(Path)에 따라 리소스를 성공적으로 반환한 경우입니다. 리소스를 요청한 Type으로 직렬화 하여 Body를 통해 응답하는데 이때 응답 Header는 Content-Type, Content-Length, Content-Encoding을 지정합니다. |
301 Moved Permanently | 요청한 경로(Path)의 리소스가 다른 곳으로 이동되었음을 의미합니다. 웹 서비스는 해당 응답코드와 함께 이동된 Path를 응답할 수 있으며 응답 Header에도 Location이름으로 새로운 Path를 포함할 수 있습니다. |
302 Found | 301과 같지만 변경된 Path를 포함하지는 않습니다. |
304 Not Modified | If-Modified-Since를 Header에 포함하여 요청하는 경우에 대한 응답이며 응답본문은 캐시된 리소스 복사본을 사용해야 하므로 비어있을 수 있습니다. |
400 Bad Request | 요청이 잘못된 경우 입니다. Https://locahost:3118/products/14와 같은 URL의 경우 14처럼 식별가능한 ID가 빠졌거나 필요한 리소스지정이 되지 않은 경우 입니다. |
401 Unauthorized | 요청한 리소스는 존재하지만 인증된 사용자가 아닌 경우 대한 응답입니다. 이런 경우 Authorization Header의 설정을 통해 문제를 해결할 수 있습니다. |
403 Forbidden | 요청한 리소스는 존재하지만 인증된 사용자가 아니거나 리소스에 접근할 수 있는 충분한 권한이 없는 경우 대한 응답입니다. 이런 경우 Authorization Header의 설정을 통해 문제를 해결할 수 있습니다. |
404 Not Found | 요청에 따른 리소스를 찾지 못한 경우에 대한 응답입니다. 리소스를 발견하지 못한것을 표시하기 위해 410 Gone을 반환하기도 합니다. |
406 Not Acceptable | 요청 header의 Accept에 응답으로 요청한 Media Type을 서버에서 제공할 수 없는 경우에 대한 응답입니다. 예를 들어 응답포멧으로 JSON을 요청했으나 서버는 오로지 XML로만 응답이 가능한 경우입니다. |
451 Unavailable for Legal Reasons | 미국에서 호스팅되는 웹 사이트의 경우 유럽으로부터 오는 요청에 대해 일반 데이터 보호 규정을 준수하지 않도록 이 요청을 반환할 수 있습니다. 451이라는 숫자는 '화씨 451'에서 유래하였습니다. |
500 Server Error | 요청이 잘못되었거나 서버 자체의 문제로 인하여 요청을 처리할 수 없는 경우의 응답입니다. |
503 Service Unavailable | 웹서비스가 포화상태이거나 요청을 처리할 수 없는 경우의 응답입니다. |
GET이외에는 리소스를 생성하거나 변경, 삭제하기 위한 POST, PUT, PATCH, DELETE와 같은 요청이 있을 수 있습니다.
새로운 리소스를 생성하는 경우 POST요청을 만들 수 있으며 리소스에 대한 상세를 body에 포함하여 아래와 같은 형태로 요청할 수 있습니다.
POST /product/add
Content-Length: 23
Content-Type: application/json
기존에 존재하는 리소스변경하거나 추가하기 위해서는 PUT요청이 사용될 수 있으며 기존에 리소스가 존재하지 않는 경우 새로운 리소스를 추가하게 됩니다.
PUT /product
Content-Length: 23
Content-Type: application/json
새로운 리소스 추가가 아닌 온전히 존재하는 리소스를 변경하고자 하는 목적이라면 PATCH요청을 수행합니다.
PATCH /product/modify
Content-Length: 23
Content-Type: application/json
리소스를 삭제하는 경우라면 DELETE요청을 수행합니다.
DELETE /product/remove
이전에 소개한 GET요청에 대한 응답과 더불어 리소스를 추가, 삭제, 수정하는 경우 아래와 같은 응답코드가 발생할 수 있습니다.
상태코드 | 이유 |
201 Created | 리소스가 성공적으로 생성되었으며 응답 header의 Location에 새로운 리소스를 확인할 수 있는 path를 포함할 수 있습니다. body또한 생성된 리소스자체를 포함하기도 합니다. |
202 Accepted | 요청한 리소스를 즉각적으로 생성할 수 없고 나중에 처리될 수 있도록 queue되었음을 나타냅니다. 응답 body는 몇몇 형태의 상태확인을 위한 지점이나 리소스가 사용가능하기 까지의 예상을 포함할 수 있습니다. |
204 No Content | 일반적으로 DELETE요청의 응답으로 사용되며 리소스를 삭제하므로 어떠한 content도 포함하지 않음을 나타냅니다. 또는 POST, PUT, PATCH와 같은 요청에서도 사용자에게 제대로 처리 되었는지를 알려줄 필요가 없는 경우 사용될 수 있습니다. |
405 Method Not Allowed | POST요청을 받는 곳에 GET요청을 하는등 지원하지 않는 Method를 통해 요청을 시도하는 경우의 응답니다. |
415 Unsupported Media Type | JSON만을 응답하는 곳에 XML응답을 요청하는등, 지원하지 않는 형식을 응답하도록 요청하는 경우에 대한 응답입니다. |
● ASP.NET Core Web API 프로젝트 만들기
아래의 방법으로 간단한 데모 프로젝트를 생성해 보겠습니다. 이렇게 만든 웹서비스는 HTTP요청과 HTTP응답을 받을 수 있는 모든 플렛폼의 클라이언트에서 사용할 수 있습니다.
Visual Studio 2022를 실행해 File -> New -> Project를 선택하고 템플릿에서 'ASP.NET Core Web API'를 선택합니다.
적당한 프로젝트명을 입력합니다.
설정은 기본값 그대로 두겠습니다.
생성된 프로젝트의 WeatherForecastController.cs를 보면 우선 Controller가 ControllerBase상속받고 있음을 알 수 있습니다.
using Microsoft.AspNetCore.Mvc;
namespace MyWebAPI.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
}
ControllerBase는 MVC에서 사용된 Controller Class보다 훨씬 간소화된 것으로 HTML을 응답하는 View와 같은 메서드를 가지지 않습니다.
또한 WeatherForecastController는 [Route]라는 attribute를 가지고 있는데 이 것은 /weatherforecast URL을 통해 클라이언트가 HTTP요청을 만들 수 있도록 합니다. 예를 들어 https://localhost:5441/weatherforecast로 보낸 HTTP요청은 WeatherForecastController 클래스에서 처리될 것입니다. 어떤 경우에는 컨트롤러이름과 함께 API라는것을 분명히 명시하기 위해 /api/weatherforecast와 같은 형태를 사용하기도 합니다.
[ApiController] attribute는 ASP.NET Core 2.1에서 처음 소개된 것으로 controller가 기본적으로 REST별 동작을 수행할 수 있도록 하며 [HttpGet]는 Controller클래스 안에서 GET요청에 대응할 method를 지정하는데 데모 프로젝트에서는 Get()메서드가 이에 해당합니다.
현재 상태에서 Summaries중 특정 index의 값을 반혼하기 위한 두번째 Get() 메서드를 다음과 같이 추가합니다.
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
[HttpGet("{idx:int}")]
public string Get(int idx)
{
return Summaries[idx];
}
[HttpGet]은 해당 메서드가 Get요청에 응답한다는 것이며 내부에 {idx:int}는 idx라는 int형의 매개변수가 필요하다는 것을 의미합니다.
● 프로젝트 설정 살펴보기
위 에제에서는 Get() 메서드를 추가한 뒤 추가한 URL을 확인하기 위해 프로젝트를 실행하고 임으로 /WeatherForecast로 이동해 특정 int값을 부여한 것입니다.
지금 Visual Studio의 Properties폴더에 있는 LaunchSettings.json파일을 열어보면 다음과 같은 설정을 확인해 볼 수 있는데
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:50094",
"sslPort": 44375
}
},
"profiles": {
"MyWebAPI": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7228;http://localhost:5228",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
해당 설정 중 profiles에 있는 launchBrowser와 lanunchUrI는 프로젝트를 실행하면 자동으로 웹브라우저를 열고 /swagger라는 URL로 이동함을 나타냅니다. 따라서 별다른 조치를 하지 않아도 swagger를 통해 API명세를 확인할 수 있었습니다.
applicationUrl은 시작시 기본적으로 탐색되는 URL과 port번호이며 필요하다면 해당 포트번호를 임의로 수정할 수 있습니다.
launchUrI를 다음과 같이 변경하고 프로젝트를 실행하면
"launchUrl": "WeatherForecast/3",
이전과 달리 곧장 새로 추가된 Get() 메서드의 동작을 확인할 수 있습니다.
● Database 사용하기
Database생성및 설정은 이전 글에서
[.NET/ASP.NET Core] - [ASP.NET Core] Razor Page로 웹프로젝트 만들기
'DB 사용하기'부분을 참고해 주시기 바랍니다. 참고로 해당 WEB API 프로젝트에서는 AddNorthwindContext()를 Program.cs의 AddControllers()가 호출되기 전에 추가하도록 합니다.
builder.Services.AddNorthwindContext();
builder.Services.AddControllers();
MVC Controller와 달리 WEB API Controller는 HTML을 표시하기 위한 View를 반환하지 않습니다. 대신 HTTP를 요청하는 클라이언트와의 교섭을 통해 XML이나 JSON과 같은 형식의 데이터를 반환하므로 클라이언트는 이러한 형식의 데이터를 받아 적절한 방법으로 처리해야 합니다.
대부분의 경우 React나 Vue와 같은 프레임워크를 통해 Front-End가 만들어지는 경우가 많아지며 꼭 그렇지 않다 하더라도 가볍지만 데이터 자체를 충실히 표현할 수 있는 JSON형식이 많이 사용되는 추세입니다.
Database작업이 완료되었으면 간단히 CRUD(Create, Retrieve, Update, Delete)를 구현하는 페이지를 만들어 볼 것입니다. CRUD대상은 Northwind DB의 Region 테이블입니다.
● API Controller 구현
MVC Controller에서는 URL을 통해 클래스와 메서드 2가지를 알 수 있었습니다. 예를 들어 /home/index 라면 Home Controller에 index 메서드를 의미합니다.
반면 API Controller는 기본적으로 Action Method는 없고 Controller만이 존재합니다. 방금전 사용했던 /WeatherForecast 처럼 Controller Class만을 표현하고 있습니다. 따라서 Controller에 존재하는 각 Action Method의 실행을 결정하기 위해서는 Action Method에 POST나 GET과 같은 HTTP 메서드를 [HttpGet]이나 [HttpHead]등의 attribute를 사용해 decorate함으로서 지정해야 합니다. 또한 가급적 Action Method에는 클라이언트가 응답을 예상할 수 있도록 모든 응답코드에 대응하느 [ProducesResponseType] attribute를 사용하는 것이 좋습니다. 이는 Method에 대한 정보를 공개적으로 노출시켜 클라이언트가 웹 서비스와 어떻게 상호작용할 수 있을지를 나타내기 위한 것입니다.
Action Method는 string과 같은 단순한 .NET Type부터 class나 struct와 같은 복합형식의 객체까지 거의 대부분의 데이터를 반환할 수 있습니다. 이들 데이터는 HTTP요청시 accept header로 설정된 형식에 따라 데이터를 serialize하여 반환합니다. 대게의 경우 JSON형식을 따릅니다.
응답을 보다 세부적으로 제어하기 위한 NET 형식의 ActionResult 래퍼를 반환하는 헬퍼 메서드가 있습니다. 입력 또는 기타 변수에 따라 다른 반환 유형을 반환할 수 있는 경우 작업 메서드의 반환 유형을 IActionResult로 선언하거나 상태 코드가 다른 단일 유형만 반환하는 경우 작업 메서드의 반환 유형을 ActionResult<T>로 선언할 수 있습니다.
예를 들어 아래와 같은 메서드가 있다면
[HttpGet("{idx:int}")]
[ProducesResponseType(200, Type = typeof(object))]
[ProducesResponseType(404)]
public IActionResult Get(int idx)
이 메서드는 int형식의 idx값을 필요로 하며 성공적으로 처리되는 경우(200) object형식의 데이터를 같이 반환하고 그렇지 않으면 404가 반환될 수 있음을 알려주고 있습니다.
ControllerBase Class는 응답에 필요한 아래와 같은 메서드를 가지고 있으므로 상황에 맞게 해당 메서드를 사용할 수 있습니다.
메서드 | 설명 |
Ok | 상태코드 200과 함께 클라이언트가 필요로 하는 데이터를 반환합니다. |
CreatedAtRoute | 상태코드 201과 함께 새로운 resource가 존재하는 경로를 반환합니다. |
Accepted | 상태코드 202를 반환합니다. 요청이 처리되었다는 의미를 갖고 있지만 실제로는 아직 완료되지 않을 수 있습니다.(비동기 처리의 경우) |
NoContentResult | 상태코드 204를 반환하며 어떠한 데이터도 반환하지 않습니다. 삭제(DELETE)나 변경(PATCH)과 같은 요청에 응답할 수 있습니다. |
NotFound | 상태코드 404를 반환히먀 자동적으로 ProblemDetails를 포함합니다. |
BadRequest | 상태코드 400을 반환하며 필요하다면 오류에 대한 상세설명을 포함할 수 있습니다. |
Visual Studio의 Solution Explorer에서 Controller폴더를 선택한뒤 마우스 오른쪽 버튼으로 Add -> Controller를 선택하여
Region이름의 새로운 Controller를 추가합니다.
Database에 관한 설정은 이미 위에서 소개한 이전 글에서 설명하였으므로 생략하도록 하겠습니다. 새롭게 추가된 컨트롤러는 생성자주입을 통해 DB연결 객체를 가져옵니다.
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using MyWebAPI.Data;
namespace MyWebAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class RegionController : ControllerBase
{
private readonly NorthwindContext DB;
public RegionController(NorthwindContext context)
{
DB = context;
}
}
}
그리고 다음과 같이 현재 Region에 있는 모든 데이터를 가져오는 API 메서드를 추가합니다.
[HttpGet]
[ProducesResponseType(200, Type = typeof(IEnumerable<Region>))]
public async Task<IEnumerable<Region>> GetAll()
{
var result = await Task.FromResult(DB.Region);
return result;
}
추가된 GetAll() 메서드는 HttpGet요청을 받게 되며 정상적으로 요청이 처리되면 200상태코드와 함께 IEnumerable<Region>형식의 결과를 반혼합니다. 그리고 async와 await키워드를 통해 메서드를 비동기로 구현하였습니다.
프로젝트를 실행하고
추가된 메서드의 정상동작을 확인하기 위해 Swagger를 통해 WebBrowser에서 테스트를 진행합니다.
혹은 PostMan과 같은 HTTP 요청을 생성해 주는 프로그램을 사용할 수도 있습니다.
참고 : 실제 Region에는 RegionID, RegionDescription이라는 2개의 컬럼만 존재하지만 API결과상으로는 territories라는 Array Type도 같이 표현되는데 이는 Territories테이블의 Join을 반영한 모델의 결과를 그대로 반영했기 때문입니다.
public partial class Region
{
public Region()
{
Territories = new HashSet<Territories>();
}
public int RegionId { get; set; }
public string RegionDescription { get; set; }
public virtual ICollection<Territories> Territories { get; set; }
}
따라서 정확한 Column만을 표현하려면 일부 가공이 필요합니다.
using Microsoft.AspNetCore.Mvc;
using MyWebAPI.Data;
namespace MyWebAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class RegionController : ControllerBase
{
private readonly NorthwindContext DB;
public RegionController(NorthwindContext context)
{
DB = context;
}
[HttpGet]
[ProducesResponseType(200, Type = typeof(IEnumerable<Region>))]
public async Task<IActionResult> GetAll()
{
var result = await Task.FromResult(DB.Region.Select(x => new { x.RegionId, x.RegionDescription }));
return Ok(result);
}
}
}
HTTP요청이 발생하면 service에 의해 요청이 수신되고 Controller Class의 인스턴스를 생성하여 적절한 Method를 호출하게 될 것입니다. 그리고 클라이언트가 필요로 하는 형식의 응답을 반환하게 됩니다.
GetAll() 메서드를 수정하여 이번에는 지정한 ID에 따라 해당 ID를 가진 Region만을 가져올 수 있도록 하였습니다.
[HttpGet]
[ProducesResponseType(200, Type = typeof(IEnumerable<Region>))]
public async Task<IActionResult> GetAll(int? id)
{
var result = await (id == null ? Task.FromResult(DB.Region.Select(x => new { x.RegionId, x.RegionDescription })) : Task.FromResult(DB.Region.Select(x => new { x.RegionId, x.RegionDescription }).Where(x => x.RegionId == id)));
return Ok(result);
}
따라서 '/api/Region?id=2'와 같은 요청이 온다면 해당 id와 알치하는 Region만을 반환할 것입니다. 물론 같은 구현을 아래와 같이 다른 방법으로 구현할 수도 있습니다.
[HttpGet("{id:int}", Name = nameof(GetRegion))]
[ProducesResponseType(200, Type = typeof(Region))]
[ProducesResponseType(404)]
public async Task<IActionResult> GetRegion(int id)
{
var result = await Task.FromResult(DB.Region.Select(x => new { x.RegionId, x.RegionDescription }).Where(x => x.RegionId == id));
if (result == null)
return NotFound();
else
return Ok(result);
}
새롭게 추가한 메서드는 int형식의 id값을 통해 위에서와 동일한 형식으로 해당 id의 Region을 가져오도록 하는 것입니다.
다만 HttpGet에서 id Route를 지정했으므로 요청을 'api/region/2'와 같은 방법으로 수행할 수 있습니다.
아래 메서드는 POST방식으로 데이터를 받아 새로운 Region을 추가합니다.
[HttpPost]
[ProducesResponseType(201, Type = typeof(Region))]
[ProducesResponseType(400)]
public async Task<IActionResult> Create(Region region)
{
if (region == null)
return BadRequest();
await DB.Region.AddAsync(region);
int result = DB.SaveChanges();
if (result == 0)
{
return BadRequest("요청이 잘못되었습니다.");
}
else
{
var reg = DB.Region.Where(x => x.RegionId == region.RegionId).SingleOrDefault();
return CreatedAtRoute(routeName: nameof(GetRegion), routeValues: new { id = region.RegionId }, value: reg);
}
}
POST를 통해 새로운 Region의 데이터를 받아 추가하고 실패하는 경우 400을, 성공하는 경우 201을 반환합니다. 201의 경우에는 새롭게 추가된 Region을 확인할 수 있는 Route를 함께 제공하고 있습니다.
API는 기본이 JSON이지만 만약 데이터가 XML형태로도 제공될 수 있는 경우라면
public async Task<IActionResult> Create([FromBody] Region region)
와 같은 형식으로 매개변수를 지정해 줄 수 있습니다.
Route를 제공할때 이름은 GetRegion() 메서드의 이름을 사용했는데 이 이름과 일치시키기 위해 GetRegion()메서드 작성시 이름을 Name 속성에 동일하게 지정하였습니다.
Region 업데이트는 HttpPatch를 사용하였습니다.
[HttpPatch]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public async Task<IActionResult> Update(Region region)
{
DB.Region.Update(region);
int result = await DB.SaveChangesAsync();
if (result == 0)
return NotFound();
return new NoContentResult();
}
참고로 PUT은 전체업데이트를 PATCH는 일부분 업데이트를 의미합니다.
마지막으로 삭제의 경우에는 DELETE메서드를 구현합니다.
[HttpDelete("{id:int}")]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public async Task<IActionResult> Delete(int id)
{
var region = DB.Region.Where(x => x.RegionId == id).SingleOrDefault();
if (region == null)
return NotFound();
DB.Region.Remove(region);
int result = await DB.SaveChangesAsync();
if (result == 0)
return BadRequest("삭제 실패!");
else
return new NoContentResult();
}
● 오류상황 보고하기
ASP.NET Core는 오류상황에 대한 세부 정보를 특정할 수 있는 웹표준 기반의 기능을 구현할 수 있습니다. Web API Controller에 보면 [ApiController] 속성이 decorate되어 있는걸 볼 수 있는데 여기에서 IActionResult를 반환하는 Action 메서드는 오류가 발생하는 경우 400과 같은 상태코드와 함께 ProblemDetails 클래스의 인스턴스를 body에 추가하여 응답하게 됩니다.
따라서 이러한 오류의 상세정보 접근이 필요한 경우 ProblemDetails의 인스턴스를 임의로 생성하여 제공하면 됩니다.
아래 Delete()메서드는 ProblemDetails의 인스턴스를 어떤 방법으로 생성하는지를 보여주기 위해 id값이 0인 경우를 예외로 구현하였습니다.
[HttpDelete("{id:int}")]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public async Task<IActionResult> Delete(int id)
{
if (id == 0)
{
ProblemDetails pb = new()
{
Status = StatusCodes.Status400BadRequest,
Type = "https://localhost:7228/error:",
Title = "Error!",
Detail = $"{id}값은 처리할 수 없습니다.",
Instance = HttpContext.Request.Path
};
return BadRequest(pb);
}
var region = DB.Region.Where(x => x.RegionId == id).SingleOrDefault();
if (region == null)
return NotFound();
DB.Region.Remove(region);
int result = await DB.SaveChangesAsync();
if (result == 0)
return BadRequest("삭제 실패!");
else
return new NoContentResult();
}
● XML 응답하기
ASP.NET Web API의 기본 응답형식인 JSON대신 XML형식의 응답이 필요하다면 Program.cs에서 다음과 같이 XmlSerializer를 추가해 주면 됩니다. 그러면 JSON과 더불어 클라이언트가 요청하는 경우 XML형식도 응답이 가능할 수 있습니다.
builder.Services.AddControllers().AddXmlSerializerFormatters().AddXmlDataContractSerializerFormatters();
AddXmlSerializerFormatters() 메서드는 XML형식의 응답을 위한 것이며 AddXmlDataContractSerializerFormatters() 메서드는 DataContext 결과를 XML으로 형식화 하기 위한 것입니다.
이 상태에서 특정 메서드의 XML반환을 위해서는 별도의 format을 지정하여 처리합니다.
[HttpGet("{id:int}.{format}", Name = nameof(GetRegion)), FormatFilter]
[ProducesResponseType(200, Type = typeof(Region))]
[ProducesResponseType(404)]
public async Task<List<Region>> GetRegion(int id)
{
var result = await Task.FromResult(DB.Region.Where(x => x.RegionId == id));
var r = result.ToList();
return r;
}
● HTTP Logging
HTTP Logging는 HTTP요청과 HTTP응답에 대한 다양한 정보를 기록할 수 있는 선택적 미들웨어입니다. 다만 web service를 감시하고 디버깅하기 위한 좋은 방법일 수 있지만 성능에는 부정적인 영향을 줄 수 있으므로 주의해야 합니다.
HTTP Logging을 위해서는 우선 Program.cs에서 다음 네임스페이스를 선언해야 합니다.
using Microsoft.AspNetCore.HttpLogging;
그리고 service 설정 부분에서 다음과 같이 HTTP Logging을 위한 설정을 추가합니다.
builder.Services.AddHttpLogging(options =>
{
options.LoggingFields = HttpLoggingFields.All;
options.RequestBodyLogLimit = 2048;
options.ResponseBodyLogLimit = 2048;
});
상기 설정은 HTTP사양 전체를 Logging대상으로 하며 요청과 응답모두 2048바이트 크기만큼을 Logging하도록 합니다. 기본값은 32k입니다. 마지막으로 app객체를 통해 UseHttpLogging()메서드를 호출하고
app.UseHttpLogging();
appsettings.json에서 Logging사용을 위한 설정을 추가합니다.
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
}
},
2. API 사용하기
● HttpClientFactory
위에서 생성한 API를 호출하여 사용하는데는 여러가지 방법이 있을 수 있지만 HttpClientFactory를 사용하면 비교적 쉬운 방법으로 API를 호출하여 원하는 데이터를 다룰 수 있습니다.
Visual Studio 템플릿을 통해서 Razor Page를 사용하는 간단한 웹프로젝트를 생성한뒤 Program.cs에 다음과 같이 HttpClientFactory를 사용하기 위한 구문을 추가합니다.
using System.Net.Http.Headers;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddHttpClient(name: "myAPI",
configureClient: options =>
{
options.BaseAddress = new Uri("https://localhost:7228/");
options.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json", 1.0));
});
이는 이전에 만든 API으 URL주소를 통해 json방식의 요청과 응답을 사용하도로 하는 것입니다.
그리고 index.cshtml.cs페이지에서 HttpClientFactory 필드를 선언하고
private readonly IHttpClientFactory clientFactory;
생성자를 통해 위에서 선언한 HttpClientFactory객체를 설정한뒤 OnGet()메서드에서 API를 통해 필요한 데이터를 가져올 수 있도록 구현합니다.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace APITest.Pages
{
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
private readonly IHttpClientFactory clientFactory;
public IEnumerable<Region>? regions { get; set; }
public IndexModel(ILogger<IndexModel> logger, IHttpClientFactory httpClientFactory)
{
_logger = logger;
clientFactory = httpClientFactory;
}
public void OnGet()
{
HttpClient client = clientFactory.CreateClient(name: "myAPI");
HttpRequestMessage request = new(method: HttpMethod.Get, requestUri: "api/region/2");
HttpResponseMessage response = client.Send(request);
regions = response.Content.ReadFromJsonAsync<IEnumerable<Region>>().Result;
}
}
public class Region
{
public int regionId { get; set; }
public string? regionDescription { get; set; }
}
}
마지막으로 가져온 데이터를 Index.cshtml에 표시하도록 HTML를 추가합니다.
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>
@if (Model.regions is not null)
{
<table>
@foreach(var item in Model.regions) {
<tr>
<td>@item.regionId</td>
<td>@item.regionDescription</td>
</tr>
}
</table>
}
● CORS
CORS는 클라이언트와 서버가 서로 다른 도메인일 경우 웹리소스를 보호하기 위한 HTTP Header 기반의 규격이며 '교차 출처 리소스 공유'라고 합니다.
따라서 CORS는 허용한다는 것은 서버가 다른 도메인으로부터의 요청을 받아 리소스를 공유하는 것을 허용한다는 의미가 됩니다.
ASP.NET Core Web API에서 CORS를 허용하기 위해서는 우선 서비스설정에서 AddCors()메서드를 추가하고
builder.Services.AddCors();
HTTP Pipeline설정에서 UseEndpoints를 호출하기 이전에 아래 구문을 추가해 줍니다.
app.UseCors(configurePolicy: options =>
{
options.WithMethods("GET", "POST", "PUT", "DELETE");
options.WithOrigins(
"https://localhost:4880"
);
});
이 설정은 CORS를 사용하고 'https://localhost:4880'도메인으로 부터 들어오는 GET, POST, PUT, DELETE요청을 허용할것임을 의미합니다.
3. API를 위한 추가설정
● Health Check API
ASP.NET Core 2.2부터는 웹서비스가 정상적으로 동작중인지, 필요한 데이터를 정상적으로 제공할 수 있는지의 여부등을 확인할 수 있는 기능을 비교적 간단한 방법으로 구현할 수 있습니다.
예를 들어 정상적으로 DB에 접속하여 요청한 데이터를 가져올 수 있는지이 여부를 확인하려면 우선 웹프로젝트에 Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore Nuget Package를 추가하고
서비스 설정에서 AddHealthChecks()메서드를 DB Context와 함께 추가합니다.
builder.Services.AddHealthChecks().AddDbContextCheck<NorthwindContext>();
그리고 상태를 확인할 URL을 아래와 같이 지정해주면
app.UseHealthChecks(path: "/status");
app.MapControllers();
해당 URL을 호출하여 DB연결상태를 확인할 수 있게 됩니다.
Healthy : 건강해요!
● Oen API analyzer
Web API를 만들때 API Controller Class에 있는 Action Method에는 [ProducesResponseType]등의 Attribute등을 추가하하였습니다. 하지만 이런 작업은 실제 API가 동작하는 데는 아무런 영향을 까치지 않습니다. 이러한 설정은 Swagger와 같은 API명세에 해당 Action Method가 어떤 응답을 하는지를 표현하기 위해 추가된 것입니다.
Oen API analyzer는 [ApiController]로 명시된 Controller Class에서 사용된 Method를 분석해 저절한 attribute가 decorate되어 있지 않으면 '경고'를 표시할 수 있도록 함으로서 API명세가 좀더 정확하게 작성될 수 있도록 유도해 줍니다.
Oen API analyzer를 사용하기 위해서는 단지 Project의 csproj파일에서 IncludeOpenAPIAnalyzers를 true로 설정해 주기만 하면 딥니다.
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IncludeOpenAPIAnalyzers>true</IncludeOpenAPIAnalyzers>
</PropertyGroup>
● HTTP 응답 헤더 추가
API 호출 후 클라이언트에게 응답할때 필요하다면 임의의 헤더를 추가하여 응답할 수 있습니다.
이를 위해 프로젝트에서 임의의 클래스를 만들어 다음과 같이 응답할 헤더와 값을 지정합니다.
namespace MyWebAPI
{
public class MyHeader
{
private readonly RequestDelegate next;
public MyHeader(RequestDelegate _next)
{
next = _next;
}
public Task Invoke(HttpContext context)
{
context.Response.Headers.Add("tmpHeader", new Microsoft.Extensions.Primitives.StringValues("123"));
return next(context);
}
}
}
Invoke() 메서드에 필요한 헤더를 추가할 수 있으며 예제에서 헤더명은 tmpHeader로 값은 123으로 하였습니다.
그리고 이렇게 만든 클래스는 UseMiddleware를 통해 UseEndpoints를 호출하기 전 추가해 주기만 하면 됩니다.
app.UseMiddleware<MyHeader>();
이제 API를 호출하면 지정한 헤더를 붙여 응답하는 것을 확인할 수 있습니다.
● MapGet
MapGet() 메서드는 API Controller와 Action Method를 일일이 구현하지 않고도 간단한 방법으로 임의의 API를 구현할 수 있도록 지원합니다.
app.MapGet("/api/test", () =>
{
return "test";
});
MapGet()메서드를 Run이 호출되기 이전에 만들어 놓게 디면 지정한 URL로 요청이 왔을때 return에 지정된 응답을 수행하게 됩니다.
'.NET > ASP.NET' 카테고리의 다른 글
[ASP.NET Core] ASP.NET Core 개요 (0) | 2022.07.27 |
---|---|
[ASP.NET Core] Blazor 웹 프로젝트 시작하기 (0) | 2022.04.01 |
[ASP.NET Core] MVC패턴 웹프로젝트 만들기 (0) | 2022.03.04 |
[ASP.NET Core] HttpContext.User (0) | 2022.02.10 |
[ASP.NET Core] Razor Page로 웹프로젝트 만들기 (10) | 2022.01.24 |