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

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

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

Swagger/OpenAPI를 사용한 ASP.NET Core 웹 API 설명서
; https://docs.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://docs.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-5.0#openapi-specification-on-by-default

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

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

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




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



donaricano-btn



[최초 등록일: ]
[최종 수정일: 3/28/2021]

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

비밀번호

댓글 쓴 사람
 




1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12575정성태3/24/2021679개발 환경 구성: 557. Docker Desktop for Windows에서 DockerDesktopVM 기반의 Kubernetes 구성
12574정성태3/23/20211103.NET Framework: 1030. C# Socket의 Close/Shutdown 동작 (동기 모드)
12573정성태3/22/2021903개발 환경 구성: 556. WSL 인스턴스 초기 설정 명령어
12572정성태3/22/2021798.NET Framework: 1029. C# - GC 호출로 인한 메모리 압축(Compaction)을 확인하는 방법파일 다운로드1
12571정성태3/21/2021801오류 유형: 706. WSL 2 기반으로 "Enable Kubernetes" 활성화 시 초기화 실패 [1]
12570정성태3/19/2021798개발 환경 구성: 555. openssl - CA로부터 인증받은 새로운 인증서를 생성하는 방법
12569정성태3/18/2021986개발 환경 구성: 554. WSL 인스턴스 export/import 방법 및 단축 아이콘 설정 방법
12568정성태3/18/2021668오류 유형: 705. C# 빌드 - Couldn't process file ... due to its being in the Internet or Restricted zone or having the mark of the web on the file.
12567정성태3/17/2021707개발 환경 구성: 553. Docker Desktop for Windows를 위한 k8s 대시보드 활성화 [1]
12566정성태3/17/2021769개발 환경 구성: 552. Kubernetes - kube-apiserver와 REST API 통신하는 방법 (Docker Desktop for Windows 환경)
12565정성태3/17/2021570오류 유형: 704. curl.exe 실행 시 dll not found 오류
12564정성태3/16/2021665VS.NET IDE: 160. 새 프로젝트 창에 C++/CLI 프로젝트 템플릿이 없는 경우
12563정성태3/16/2021729개발 환경 구성: 551. C# - JIRA REST API 사용 정리 (3) jira-oauth-cli 도구를 이용한 키 관리
12562정성태3/15/2021824개발 환경 구성: 550. C# - JIRA REST API 사용 정리 (2) JIRA OAuth 토큰으로 API 사용하는 방법파일 다운로드1
12561정성태3/12/2021653VS.NET IDE: 159. Visual Studio에서 개행(\n, \r) 등의 제어 문자를 치환하는 방법 - 정규 표현식 사용
12560정성태3/11/20211026개발 환경 구성: 549. ssh-keygen으로 생성한 개인키/공개키 파일을 각각 PKCS8/PEM 형식으로 변환하는 방법
12559정성태3/11/2021728.NET Framework: 1028. 닷넷 5 환경의 Web API에 OpenAPI 적용을 위한 NSwag 또는 Swashbuckle 패키지 사용파일 다운로드1
12558정성태3/10/20211024Windows: 192. Power Automate Desktop (Preview) 소개 - Bitvise SSH Client 제어 [1]
12557정성태3/10/2021575Windows: 191. 탐색기의 보안 탭에 있는 "Object name" 경로에 LEFT-TO-RIGHT EMBEDDING 제어 문자가 포함되는 문제
12556정성태3/9/2021531오류 유형: 703. PowerShell ISE의 Debug / Toggle Breakpoint 메뉴가 비활성 상태인 경우
12555정성태3/8/2021762Windows: 190. C# - 레지스트리에 등록된 DigitalProductId로부터 라이선스 키(Product Key)를 알아내는 방법파일 다운로드2
12554정성태3/8/2021946.NET Framework: 1027. 닷넷 응용 프로그램을 위한 PDB 옵션 - full, pdbonly, portable, embedded
12553정성태3/5/2021827개발 환경 구성: 548. 기존 .NET Framework 프로젝트를 .NET Core 용으로 변환해 주는 upgrade-assistant, try-convert 도구 소개
12552정성태3/5/2021596개발 환경 구성: 547. github workflow/actions에서 Visual Studio Marketplace 패키지 등록하는 방법
12551정성태3/5/2021652오류 유형: 702. 비주얼 스튜디오 - The 'CascadePackage' package did not load correctly. (2)
12550정성태3/5/2021599오류 유형: 701. Live Share 1.0.3713.0 버전을 1.0.3884.0으로 업데이트 이후 ContactServiceModelPackage 오류 발생하는 문제
1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...