Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

(시리즈 글이 2개 있습니다.)
.NET Framework: 1028. 닷넷 5 환경의 Web API에 OpenAPI 적용을 위한 NSwag 또는 Swashbuckle 패키지 사용
; https://www.sysnet.pe.kr/2/0/12559

닷넷: 2193. C# - ASP.NET Web Application + OpenAPI(Swashbuckle) 스펙 제공
; https://www.sysnet.pe.kr/2/0/13511




닷넷 5 환경의 Web API에 OpenAPI 적용을 위한 NSwag 또는 Swashbuckle 패키지 사용

이번 글은 다음의 문서에 대한 간략 정리입니다. ^^

Swagger/OpenAPI를 사용한 ASP.NET Core 웹 API 설명서
; https://learn.microsoft.com/ko-kr/aspnet/core/tutorials/web-api-help-pages-using-swagger

좋군요, 한글화도 잘 되어 있고. ^^




닷넷 세계에서의 Web API는 대표적으로 Route-to-code 방식Controller 방식으로 구현할 수 있습니다. 물론, 현실적인 기준으로 봤을 때 일반적으로는 Controller 방식으로 구현하게 됩니다.

이렇게 Web API를 구현하는데 "OpenAPI" 스펙을 적용하는 것은 어떤 의미일까요?

만약 여러분이 기존 SOAP이나 WCF를 접했다면 자연스럽게 WSDL(Web Service Definition Language)을 사용해봤을 텐데요, OpenAPI 스펙을 REST API 세계에서의 WSDL이라고 생각하시면 이해가 더 빠를 것입니다. 그리고 보통 OpenAPI 스펙의 다른 이름으로 "Swagger Specification"이라고 혼용해서 불리기도 하지만,

OpenAPI Specification (formerly Swagger Specification) is an API description format for REST APIs.

엄밀하게 "Swagger" 단독으로는 OpenAPI 스펙용 도구들의 집합을 의미합니다.

Swagger is a set of open-source tools built around the OpenAPI Specification that can help you design, build, document and consume REST APIs.

이에 대해서는 SmartBear 사의 블로그에도 자세하게 소개하고 있습니다.

What Is the Difference Between Swagger and OpenAPI?
; https://swagger.io/blog/api-strategy/difference-between-swagger-and-openapi/

OpenAPI = Specification
Swagger = Tools for implementing the specification

잠시 이력을 살펴보면, 원래 SmartBear 사는 Swagger 명세와 그에 대한 도구들을 소유하고 있었는데, 자신들의 명세를 OpenAPI 이니셔티브에 기부하면서 기존에 "Swagger Specification"라고 알려졌던 것이 "OpenAPI Specification"으로 개명된 것입니다. 개명 시점의 버전이 OpenAPI 2.0이기 때문에 엄밀히 따지면 OpenAPI 3.0 스펙을 Swagger 스펙이라고 부르는 것은 맞지 않게 됩니다. 그리고, SmartBear 사의 기존 도구들은 여전히 Swagger라는 제품명을 유지하면서 오늘날의 이러한 혼란이 오게 된 것입니다.

어쨌든 기존의 WSDL 명세를 구현하기 위해 asmx나 WCF 프레임워크가 나온 것처럼, 언어 중립적인 OpenAPI 스펙을 제공하기 위해 닷넷 세계에서 제공하는 도구들이 바로 NSwag 또는 Swashbuckle 같은 패키지입니다.

사실, ASMX/WCF 때와 마찬가지로 사용법도 매우 쉽습니다. 여러분이 (아주 특별한 목적을 제외하고는) asmx/wcf를 만들 때 WSDL 생성을 직접 제어한 적은 없을 것입니다. (WSDL이 뭔지도 모르고 사용하던 분들도 많았을 것입니다.) 마찬가지로 OpenAPI 스펙 명세 구현도, 여러분은 그냥 기존 Web API를 만들기만 하고 NSwag/Swashbuckle 같은 패키지를 사용하면 그것들이 알아서 사용자가 구현한 Web API를 설명하는 OpenAPI 명세 및 UI를 제공하는 식으로 동작합니다.

또한 이렇게 API에 대한 명세가 있다면 (비주얼 스튜디오의 "웹/서비스 참조"가 그랬듯이) 클라이언트 측에서의 호출 코드 또한 자동 생성하는 것이 가능합니다.

실제로 간단하게 실습을 해볼까요?

ASP.NET Core Web API 유형의 프로젝트를 선택하면 다음과 같이 "Enable OpenAPI support" 옵션을 볼 수 있습니다.

openapi_support_net5_1.png

이것을 켜고 프로젝트를 생성하면 자동으로 Swashbuckle 패키지를 참조해 미들웨어 설정까지 모두 완료한 예제 프로젝트를 생성해 줍니다. (참고로 "Swashbuckle 및 ASP.NET Core 시작" 문서의 경우 "Enable OpenAPI support" 옵션을 켜지 않은 상태에서 Swashbuckle을 구성하는 방법을 설명합니다.)

using Microsoft.AspNetCore.Builder;
// ...[생략]...

namespace WebApplication1
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication1", Version = "v1" });
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebApplication1 v1"));
            }

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

또한, 기본 생성된 Web API 코드는 Swagger로 인한 어떠한 영향도 없이 일반적인 코드 구성을 보여주는데요,

using Microsoft.AspNetCore.Mvc;
// ...[생략]...

namespace WebApplication1.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]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

이를 실행하면, OpenAPI 스펙에 따라 Request/Response에 대응하는 API 목록을 보여주는 UI(/swagger/index.html)가 나옵니다.

openapi_support_net5_2.png

위의 화면에 왼쪽 상단을 보면 '/swagger/v1/swagger.json" 경로가 있는데요, 이것을 누르고 들어가면 OpenAPI 스펙에 따라 여러분이 작성한 Web API의 명세서를 자동으로 Swashbuckle 미들웨어에서 다음과 같이 생성해 줍니다.

{
  "openapi": "3.0.1",
  "info": {
    "title": "WebApplication1",
    "version": "v1"
  },
  "paths": {
    "/WeatherForecast": {
      "get": {
        "tags": [
          "WeatherForecast"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/WeatherForecast"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/WeatherForecast"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/WeatherForecast"
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "WeatherForecast": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "format": "date-time"
          },
          "temperatureC": {
            "type": "integer",
            "format": "int32"
          },
          "temperatureF": {
            "type": "integer",
            "format": "int32",
            "readOnly": true
          },
          "summary": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      }
    }
  }
}

그러니까, XML로 만들어진 WSDL 명세의 JSON 버전이라고 보면 됩니다. 그럼, 이 스펙에 따라 Web API를 호출하는 클라이언트 측 코드도 (ASXM/WCF 시절처럼) 자동 생성을 해볼까요? ^^

간단하게 콘솔 프로젝트를 하나 만들고, 프로젝트 노드의 하위에 있는 "Dependencies"를 우클릭해 "Add Connected Service"를 선택합니다.

openapi_support_net5_3.png

그럼 다음과 같은 화면이 뜨고, "Service References (OpenAPI, gRPC)"에 있는 "Add" 버튼을 눌러 "Add new OpenAPI service reference" 창을 띄운 후, 소스 코드 자동 생성을 하려는 대상의 OpenAPI 명세를 가리키는 파일 또는 URL을 선택하면 됩니다.

openapi_support_net5_4.png

이 글에서는 예제로 작성한 "WebApplication1" 프로젝트를 실행해 두면 OpenAPI 명세를 "http://localhost:30753/swagger/v1/swagger.json" 경로에서 구할 수 있어 위의 화면에 "URL" 조건으로 입력을 했고, (당연히 WebApplication1 웹 프로젝트를 실행해 둔 상태에서) "Finish" 버튼을 누르면 콘솔 프로젝트에 Web API를 호출할 수 있는 클라이언트 코드가 자동 생성됩니다.

해당 코드는 /obj/swaggerClient.cs 파일로 찾을 수 있는데 대충 다음과 같은 식의 C# 코드를 볼 수 있습니다.

//----------------------
// <auto-generated>
//     Generated using the NSwag toolchain v13.0.5.0 (NJsonSchema v10.0.22.0 (Newtonsoft.Json v11.0.0.0)) (http://NSwag.org)
// </auto-generated>
//----------------------

#pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended."
#pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword."
#pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?'
#pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ...
#pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..."

namespace ConsoleApp1
{
    [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.0.5.0 (NJsonSchema v10.0.22.0 (Newtonsoft.Json v11.0.0.0))")]
    public partial class swaggerClient 
    {
        private string _baseUrl = "";
        private System.Net.Http.HttpClient _httpClient;
        private System.Lazy<Newtonsoft.Json.JsonSerializerSettings> _settings;
    
        public swaggerClient(string baseUrl, System.Net.Http.HttpClient httpClient)
        {
            BaseUrl = baseUrl; 
            _httpClient = httpClient; 
            _settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(() => 
            {
                var settings = new Newtonsoft.Json.JsonSerializerSettings();
                UpdateJsonSerializerSettings(settings);
                return settings;
            });
        }
    
        public string BaseUrl 
        {
            get { return _baseUrl; }
            set { _baseUrl = value; }
        }
    
        protected Newtonsoft.Json.JsonSerializerSettings JsonSerializerSettings { get { return _settings.Value; } }
    
        partial void UpdateJsonSerializerSettings(Newtonsoft.Json.JsonSerializerSettings settings);
        partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url);
        partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder);
        partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response);
    
        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        public System.Threading.Tasks.Task<System.Collections.Generic.ICollection<WeatherForecast>> WeatherForecastAsync()
        {
            return WeatherForecastAsync(System.Threading.CancellationToken.None);
        }
    
        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        public async System.Threading.Tasks.Task<System.Collections.Generic.ICollection<WeatherForecast>> WeatherForecastAsync(System.Threading.CancellationToken cancellationToken)
        {
            var urlBuilder_ = new System.Text.StringBuilder();
            urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/WeatherForecast");
    
            var client_ = _httpClient;
            try
            {
                using (var request_ = new System.Net.Http.HttpRequestMessage())
                {
                    request_.Method = new System.Net.Http.HttpMethod("GET");
                    request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain"));
    
                    PrepareRequest(client_, request_, urlBuilder_);
                    var url_ = urlBuilder_.ToString();
                    request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
                    PrepareRequest(client_, request_, url_);
    
                    var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
                    try
                    {
                        var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
                        if (response_.Content != null && response_.Content.Headers != null)
                        {
                            foreach (var item_ in response_.Content.Headers)
                                headers_[item_.Key] = item_.Value;
                        }
    
                        ProcessResponse(client_, response_);
    
                        var status_ = ((int)response_.StatusCode).ToString();
                        if (status_ == "200") 
                        {
                            var objectResponse_ = await ReadObjectResponseAsync<System.Collections.Generic.ICollection<WeatherForecast>>(response_, headers_).ConfigureAwait(false);
                            return objectResponse_.Object;
                        }
                        else
                        if (status_ != "200" && status_ != "204")
                        {
                            var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); 
                            throw new ApiException("The HTTP status code of the response was not expected (" + (int)response_.StatusCode + ").", (int)response_.StatusCode, responseData_, headers_, null);
                        }
            
                        return default(System.Collections.Generic.ICollection<WeatherForecast>);
                    }
                    finally
                    {
                        if (response_ != null)
                            response_.Dispose();
                    }
                }
            }
            finally
            {
            }
        }
    
        protected struct ObjectResponseResult<T>
        {
            public ObjectResponseResult(T responseObject, string responseText)
            {
                this.Object = responseObject;
                this.Text = responseText;
            }
    
            public T Object { get; }
    
            public string Text { get; }
        }
    
        public bool ReadResponseAsString { get; set; }
        
        protected virtual async System.Threading.Tasks.Task<ObjectResponseResult<T>> ReadObjectResponseAsync<T>(System.Net.Http.HttpResponseMessage response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers)
        {
            if (response == null || response.Content == null)
            {
                return new ObjectResponseResult<T>(default(T), string.Empty);
            }
        
            if (ReadResponseAsString)
            {
                var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
                try
                {
                    var typedBody = Newtonsoft.Json.JsonConvert.DeserializeObject<T>(responseText, JsonSerializerSettings);
                    return new ObjectResponseResult<T>(typedBody, responseText);
                }
                catch (Newtonsoft.Json.JsonException exception)
                {
                    var message = "Could not deserialize the response body string as " + typeof(T).FullName + ".";
                    throw new ApiException(message, (int)response.StatusCode, responseText, headers, exception);
                }
            }
            else
            {
                try
                {
                    using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
                    using (var streamReader = new System.IO.StreamReader(responseStream))
                    using (var jsonTextReader = new Newtonsoft.Json.JsonTextReader(streamReader))
                    {
                        var serializer = Newtonsoft.Json.JsonSerializer.Create(JsonSerializerSettings);
                        var typedBody = serializer.Deserialize<T>(jsonTextReader);
                        return new ObjectResponseResult<T>(typedBody, string.Empty);
                    }
                }
                catch (Newtonsoft.Json.JsonException exception)
                {
                    var message = "Could not deserialize the response body stream as " + typeof(T).FullName + ".";
                    throw new ApiException(message, (int)response.StatusCode, string.Empty, headers, exception);
                }
            }
        }
    
        private string ConvertToString(object value, System.Globalization.CultureInfo cultureInfo)
        {
            if (value is System.Enum)
            {
                string name = System.Enum.GetName(value.GetType(), value);
                if (name != null)
                {
                    var field = System.Reflection.IntrospectionExtensions.GetTypeInfo(value.GetType()).GetDeclaredField(name);
                    if (field != null)
                    {
                        var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute(field, typeof(System.Runtime.Serialization.EnumMemberAttribute)) 
                            as System.Runtime.Serialization.EnumMemberAttribute;
                        if (attribute != null)
                        {
                            return attribute.Value != null ? attribute.Value : name;
                        }
                    }
                }
            }
            else if (value is bool) {
                return System.Convert.ToString(value, cultureInfo).ToLowerInvariant();
            }
            else if (value is byte[])
            {
                return System.Convert.ToBase64String((byte[]) value);
            }
            else if (value != null && value.GetType().IsArray)
            {
                var array = System.Linq.Enumerable.OfType<object>((System.Array) value);
                return string.Join(",", System.Linq.Enumerable.Select(array, o => ConvertToString(o, cultureInfo)));
            }
        
            return System.Convert.ToString(value, cultureInfo);
        }
    }

    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.0.22.0 (Newtonsoft.Json v11.0.0.0)")]
    public partial class WeatherForecast 
    {
        [Newtonsoft.Json.JsonProperty("date", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public System.DateTimeOffset Date { get; set; }
    
        [Newtonsoft.Json.JsonProperty("temperatureC", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public int TemperatureC { get; set; }
    
        [Newtonsoft.Json.JsonProperty("temperatureF", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public int TemperatureF { get; set; }
    
        [Newtonsoft.Json.JsonProperty("summary", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string Summary { get; set; }
    
    
    }

    [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.0.5.0 (NJsonSchema v10.0.22.0 (Newtonsoft.Json v11.0.0.0))")]
    public partial class ApiException : System.Exception
    {
        public int StatusCode { get; private set; }

        public string Response { get; private set; }

        public System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> Headers { get; private set; }

        public ApiException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, System.Exception innerException) 
            : base(message + "\n\nStatus: " + statusCode + "\nResponse: \n" + response.Substring(0, response.Length >= 512 ? 512 : response.Length), innerException)
        {
            StatusCode = statusCode;
            Response = response; 
            Headers = headers;
        }

        public override string ToString()
        {
            return string.Format("HTTP Response: \n\n{0}\n\n{1}", Response, base.ToString());
        }
    }

    [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.0.5.0 (NJsonSchema v10.0.22.0 (Newtonsoft.Json v11.0.0.0))")]
    public partial class ApiException<TResult> : ApiException
    {
        public TResult Result { get; private set; }

        public ApiException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, TResult result, System.Exception innerException) 
            : base(message, statusCode, response, headers, innerException)
        {
            Result = result;
        }
    }

}

#pragma warning restore 1591
#pragma warning restore 1573
#pragma warning restore  472
#pragma warning restore  114
#pragma warning restore  108

기존의 귀찮은 REST API 호출 작업을, 이제는 strong-type 형식으로 다음과 같이 SDK에서 제공하는 라이브러리 사용하듯이 REST API를 사용하면 됩니다.

using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static async Task Main(string[] args)
        {
            using HttpClient httpClient = new();
            swaggerClient client = new swaggerClient("http://localhost:30753", httpClient);

            var list = await client.WeatherForecastAsync();

            foreach (var info in list)
            {
                Console.WriteLine(info.Summary);
            }
        }
    }
}

/* 출력 결과
Chilly
Cool
Sweltering
Scorching
Mild
*/




위의 예제에서는 .NET 5의 Web API 프로젝트가 기본 지원하는 Swashbuckle 패키지를 사용한 예인데요, 만약 NSwag을 사용하려면 어떻게 해야 할까요?

이를 위해서는 (콘솔 프로젝트로 바닥부터 Web API 호스팅 프로젝트를 만들거나) ASP.NET Core Web API 프로젝트 생성 당시 "Enable OpenAPI support" 옵션을 해제하고 만드는 것으로 시작할 수 있습니다. (물론, "Enable OpenAPI support" 옵션으로 생성한 프로젝트의 경우라면 Swashbuckle 패키지 참조를 제거하고 Startup.cs 파일에 구성된 Swashbuckle 관련 미들웨어 코드를 제거하는 식으로 처리해도 무방합니다.)

그다음, NSwag 패키지를 nuget으로부터 참조 추가하고,

Install-Package NSwag.AspNetCore

Startup.cs 파일에 미들웨어 관련 설정을 다음과 같이 구성합니다.

using Microsoft.AspNetCore.Builder;
// ...[생략]...

namespace WebApplication2
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddSwaggerDocument();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseAuthorization();

            app.UseOpenApi();
            app.UseSwaggerUi3();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

끝입니다. 이제 예제 코드를 실행하면 (Swashbuckle를 사용했을 때와는 다르게) Swagger 관련 UI 페이지가 먼저 뜨지 않고 그냥 /weatherforecast 예제를 구동한다는 차이가 있어, Swagger UI와 명세를 보려면 명시적으로 다음의 URL로 방문해야 합니다.

Swagger UI - http://localhost:<port>/swagger
Web API 명세서 - http://localhost:<port>/swagger/v1/swagger.json

당연히 OpenAPI 스펙은 동일하기 때문에 기존에 Swashbuckle 웹을 대상으로 생성해 두었던 swaggerClient를 그대로 사용할 수 있습니다.




이렇게 대표적으로 2가지의 선택 사항이 있긴 하지만, 현실적으로 Visual Studio의 "Enable OpenAPI support" 옵션에서 Swashbuckle 패키지를 기본으로 제공하기 때문에,

What's new in ASP.NET Core 5.0
 - OpenAPI Specification on by default
; https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-5.0#openapi-specification-on-by-default

향후 NSwag은 그다지 사용하지 않을 가능성이 높아졌습니다.

이쯤에서 정리해 보면, Web API 프로젝트에 Swashbuckle 패키지를 사용하지 않을 이유가 없습니다. 일단 적용만 해두면, 클라이언트 측은 각기 언어에 맞게 자동 소스 코드 생성을 할 수 있고, 만약 그런 도구가 없다 해도 기존 방식 그대로 Web API 호출을 할 수 있기 때문입니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 1/4/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2023-05-12 03시40분
정성태
2023-05-31 01시18분
[.NET] API 스펙을 공유해보자. - 문제의 서막
; https://blog.naver.com/vactorman/223114164633

[.NET] API 스펙을 공유해보자. - Service Reference
; https://blog.naver.com/vactorman/223114969468

[.NET] API 스펙을 공유해보자. - NSwag
; https://blog.naver.com/vactorman/223114970347

[.NET] API 스펙을 공유해보자. - submodule
; https://blog.naver.com/vactorman/223114971288

-----------------------------------------------

OpenAPI enhancements in ASP.NET Core
; https://devblogs.microsoft.com/dotnet/announcing-dotnet-9/
정성태

1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13634정성태5/24/20244929Phone: 18. C# MAUI - 안드로이드 플랫폼에서의 Activity 제어 [1]
13633정성태5/22/20244919스크립트: 64. 파이썬 - ASGI를 만족하는 최소한의 구현 코드
13632정성태5/20/20245020Phone: 17. C# MAUI - Android 내에 Web 서비스 호스팅
13631정성태5/19/20244977Phone: 16. C# MAUI - /Download 등의 공용 디렉터리에 접근하는 방법 [1]
13630정성태5/19/20245428닷넷: 2263. C# - Thread가 Task보다 더 빠르다는 어떤 예제(?)
13629정성태5/18/20245210개발 환경 구성: 710. Android - adb.exe를 이용한 파일 전송
13628정성태5/17/20244878개발 환경 구성: 709. Windows - WHPX(Windows Hypervisor Platform)를 이용한 Android Emulator 가속
13627정성태5/17/20244955오류 유형: 904. 파이썬 - UnicodeEncodeError: 'ascii' codec can't encode character '...' in position ...: ordinal not in range(128)
13626정성태5/15/20245074Phone: 15. C# MAUI - MediaElement Source 경로 지정 방법파일 다운로드1
13625정성태5/14/20245241닷넷: 2262. C# - Exception Filter 조건(when)을 갖는 catch 절의 IL 구조
13624정성태5/12/20245175Phone: 14. C# - MAUI에서 MediaElement 사용파일 다운로드1
13623정성태5/11/20245090닷넷: 2261. C# - 구글 OAuth의 JWT (JSON Web Tokens) 해석파일 다운로드1
13622정성태5/10/20245310닷넷: 2260. C# - Google 로그인 연동 (ASP.NET 예제)파일 다운로드1
13621정성태5/10/20244939오류 유형: 903. IISExpress - Failed to register URL "..." for site "..." application "/". Error description: Cannot create a file when that file already exists. (0x800700b7)
13620정성태5/9/20244978VS.NET IDE: 190. Visual Studio가 node.exe를 경유해 Edge.exe를 띄우는 경우
13619정성태5/7/20245115닷넷: 2259. C# - decimal 저장소의 비트 구조파일 다운로드1
13618정성태5/6/20245229닷넷: 2258. C# - double (배정도 실수) 저장소의 비트 구조파일 다운로드1
13617정성태5/5/20245300닷넷: 2257. C# - float (단정도 실수) 저장소의 비트 구조파일 다운로드1
13616정성태5/3/20245311닷넷: 2256. ASP.NET Core 웹 사이트의 HTTP/HTTPS + Dual mode Socket (IPv4/IPv6) 지원 방법파일 다운로드1
13615정성태5/3/20245337닷넷: 2255. C# 배열을 Numpy ndarray 배열과 상호 변환
13614정성태5/2/20245322닷넷: 2254. C# - COM 인터페이스의 상속 시 중복으로 메서드를 선언
13613정성태5/1/20245819닷넷: 2253. C# - Video Capture 장치(Camera) 열거 및 지원 포맷 조회파일 다운로드1
13612정성태4/30/20245464오류 유형: 902. Visual Studio - error MSB3021: Unable to copy file
13611정성태4/29/20245230닷넷: 2252. C# - GUID 타입 전용의 UnmanagedType.LPStruct - 두 번째 이야기파일 다운로드1
13610정성태4/28/20245329닷넷: 2251. C# - 제네릭 인자를 가진 타입을 생성하는 방법 - 두 번째 이야기
13609정성태4/27/20245461닷넷: 2250. PInvoke 호출 시 참조 타입(class)을 마샬링하는 [IN], [OUT] 특성파일 다운로드1
1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...