Привет, Habr! С вами Антон, руководитель Архитектурного комитета компании SimbirSoft. Мы продолжаем цикл статей, посвященных практическому внедрению подхода Design API First в разработку наших проектов. Прежде чем погрузиться в новую тему, напомню о предыдущих материалах:
общий взгляд на Design API First, особенности его применения;
разбор практического использования подхода на примере сервера аутентификации;
наш опыт подготовки и планирования интеграции Design API First в наш конвейер разработки ПО;
‘how-to’ для генерации моделей frontend по спецификации OpenAPI.
Настало время поделиться практическим опытом использования спецификаций OpenAPI для кодогенерации контрактов backend.
Дисклеймер:
Материал публикации в первую очередь передает практический опыт работы системных аналитиков и практикующих архитекторов при интеграции Design API First с непосредственным процессом разработки. Некоторые технические детали реализации будут описаны не полностью.

С чего мы начинали
В качестве приложения мы использовали сервис файлового хранилища. Backend написан на .Net Core 6. Реализация REST API на стороне backend была проведена разработчиками (Code First). Мы сделали так намеренно для проверки сценария перехода c Code First на Design First. Подробнее об этом описали в статье «Интеграция паттерна Design API First в конвейер разработки ПО: наш опыт».
Итак, что было сделано для поддержки подхода Design API First на стороне backend:
В качестве инструмента кодогенерации мы выбрали кодогенерацию с использованием компилятора Roslyn.
В качестве прикладной инструментальной библиотеки кодогенерации был выбран NSwag.
В качестве реализации была разработана служебная библиотека, которая использовала Source Generators и NSwag.
В качестве реализации была разработана служебная библиотека, которая использовала Incremental Generators и NSwag.
Библиотека кодогенерации на Incremental Generators была протестирована для REST API Backend.
По результатам проделанных работ мы провели оценку удобства использования кодогенераторов Roslyn для создания «заглушек» кода по спецификации OpenAPI. Но об этом расскажем в конце статьи. А сейчас приведем основные этапы проверки технологического решения.
Использование Roslyn
Мы решили проверить актуальные возможности платформы разработки .Net 6 и обнаружили, что компилятор Roslyn и его анализаторы предоставляют готовые механизмы для генерации исходных программных кодов. Для проверки этих возможностей, очевидно, было необходимо разработать непосредственно генератор для спецификации OpenAPI.
В рамках задачи генерации исходных кодов по спецификации OpenAPI мы также не стали ничего изобретать. В качестве инструмента выбрали NSwag — самое популярное решение среди наших backend-команд разработки.
Кодогенератор на основе Source Generators был разработан довольно быстро и оказался вполне работоспособным. Привожу листинг его реализации:
[Generator]
public class Generator : ISourceGenerator
{
/// <inheritdoc />
public void Execute(GeneratorExecutionContext context)
{
var openApiContext = OpenApiContext.CreateFromExecutionContext(context);
foreach (var openApiFileContext in openApiContext.Context)
{
var generatorStrategy = GeneratorStrategyFactory.GetStrategy(openApiFileContext);
var result = generatorStrategy.GenerateCode(openApiFileContext).Result;
if (result.IsSuccess)
{
context.AddSource(openApiFileContext.FileName, result.GeneratedCode);
}
else
{
context.ReportDiagnostic(Diagnostic.Create(result.Diagnostic.DiagnosticDescriptor, null, result.Diagnostic.Message));
}
}
}
/// <inheritdoc />
public void Initialize(GeneratorInitializationContext context)
{
//#if DEBUG
// if (!Debugger.IsAttached)
// {
// Debugger.Launch();
// }
//#endif
}
}
Код генератора довольно прост. Под «капотом» мы использовали стандартный вызов из библиотеки NSwag.Core: OpenApiDocument.FromFileAsync(filePath).
Настройки NSwag для генерации были следующими:
new CSharpControllerGeneratorSettings
{
ClassName = className,
CSharpGeneratorSettings =
{
Namespace = classesNamespace,
SchemaType = NJsonSchema.SchemaType.OpenApi3,
GenerateDefaultValues = true,
GenerateDataAnnotations = false
},
ControllerStyle = NSwag.CodeGeneration.CSharp.Models.CSharpControllerStyle.Abstract,
ControllerTarget = NSwag.CodeGeneration.CSharp.Models.CSharpControllerTarget.AspNetCore,
GenerateOptionalParameters = true,
GenerateModelValidationAttributes = true,
RouteNamingStrategy = NSwag.CodeGeneration.CSharp.Models.CSharpControllerRouteNamingStrategy.None,
UseActionResultType = true
};
Фактически мы использовали контекст GeneratorExecutionContext context нашего генератора при вызове ISourceGenerator.Execute для поиска спецификаций OpenAPI в заданном проекте. Затем мы передавали найденные спецификации для обработки в NSwag. На выходе получали исходники контроллеров и моделей предметной области согласно исходным спецификациям OpenAPI.
В целом, идея реализации на практике оказалась довольно жизнеспособной. Но были и проблемы... В частности, связанные с нюансами работы кодогенерации Roslyn.
Наверняка те, кто с ними уже знаком, понимают, о чем речь. Всем остальным вкратце расскажу (информация общедоступна и не является темой нашей статьи): у Source Generators на Roslyn есть существенные проблемы с производительностью, что хорошо проявляется на больших решениях и при частых изменениях источников кодогенерации (в нашем случае это спецификации OpenAPI). Работать, по факту, крайне неудобно. Например, при внесении изменений в спецификации часто приходится перезагружать проект или IDE целиком для запуска перегенерации исходных кодов.
Решения от Microsoft
Для решения указанных и довольно очевидных проблем мы пошли на поводу у обещаний «мелкомягких» и уверовали в предлагаемые Incremental Generators.
В этой версии реализация стала сложнее, проблемы с производительностью окончательно не устранились. Вдобавок выяснились некоторые нюансы использования кодогенерации Roslyn при подключении к другим проектам в решении.
Но обо всем по порядку.
Немного кода. Так сейчас выглядит наш IncrementalGenerator:
[Generator]
public class OpenApiGenerator : IIncrementalGenerator
{
static IncrementalValueProvider<Compilation> _compilationlValueProvider;
static IncrementalValueProvider<AnalyzerConfigOptionsProvider> _analyzerConfigOptionsProvider;
...
public void Initialize(IncrementalGeneratorInitializationContext context)
{
//#if DEBUG
// if (!Debugger.IsAttached)
// {
// Debugger.Launch();
// }
//#endif
_compilationlValueProvider = context.CompilationProvider;
_analyzerConfigOptionsProvider = context.AnalyzerConfigOptionsProvider;
context.AdditionalTextsProvider
.Where(static text => IsOpenApiSchemeExtension(text.Path))
.Combine(context.AnalyzerConfigOptionsProvider
.Select(static (x, _) => bool.Parse(x.GetGlobalOption("UseCache", prefix: Name) ?? bool.FalseString)))
.SelectAndReportExceptions(GetCacheValue, context, Id)
.Combine(context.CompilationProvider.Select(static (x, _) => x.AssemblyName))
.SelectAndReportExceptions(GetSourceCode, context, Id)
.AddSource(context);
}
...
}
В приведенной выше реализации помимо обработки спецификаций в вызове .SelectAndReportExceptions(GetSourceCode, context, Id) мы используем также вызов .Combine(context.CompilationProvider.Select(static (x, _) => x.AssemblyName)) для передачи в генератор соответствующего пространства имен, которое задает namespaces исходных кодов, созданных по спецификации OpenAPI.
Пример результатов работы кодогенерации в структуре проекта приведен на рисунке ниже:

Листинг спецификации OpenApi для работы с файлами:
openapi: 3.0.3
info:
title: File REST API для Samples
version: 0.3.0
description: REST API для File Storage Template в формате OpenAPI v3
servers:
- url: http://localhost:8080
description: Dev Server
paths:
/api/v3/process:
post:
summary: Загрузить файл в локальное хранилище
description: Загружает объект в хранилище
operationId: uploadFile
tags:
- Upload
requestBody:
content:
multipart/form-data:
schema:
$ref: "#/components/schemas/File.Request.UploadData"
encoding:
file:
style: form
responses:
'201': # файл успешно загружен
$ref: "#/components/responses/File.Response.SuccessUploadFile"
"500":
$ref: "#/components/responses/App.Response.Error5XX"
/api/v3/download/{bucket}/{docId}.{ext}:
get:
description: Скачать файл из локального хранилища
operationId: downloadFileFromBucket
tags:
- Download
parameters:
- $ref: "#/components/parameters/File.Download.docId"
- $ref: "#/components/parameters/File.Download.bucket"
- $ref: "#/components/parameters/File.Download.ext"
responses:
'200': # файл успешно скачен
$ref: "#/components/responses/File.Response.SuccessDownloadFile"
"500":
$ref: "#/components/responses/App.Response.Error5XX"
/api/v3/download/{docId}.{ext}:
get:
description: Скачать файл из локального хранилища
operationId: downloadFile
tags:
- Download
parameters:
- $ref: "#/components/parameters/File.Download.docId"
- $ref: "#/components/parameters/File.Download.ext"
- $ref: "#/components/parameters/File.Download.bucketQuery"
responses:
'200': # файл успешно скачен
$ref: "#/components/responses/File.Response.SuccessDownloadFile"
"500":
$ref: "#/components/responses/App.Response.Error5XX"
/api/v3/download/doc:
get:
tags:
- Download
summary: Скачать файл из локального хранилища
parameters:
- $ref: "#/components/parameters/File.Download.docIdQuery"
- $ref: "#/components/parameters/File.Download.bucketQuery"
- $ref: "#/components/parameters/File.Download.extQuery"
responses:
'200': # файл успешно скачен
$ref: "#/components/responses/File.Response.SuccessDownloadFile"
"500":
$ref: "#/components/responses/App.Response.Error5XX"
/api/v3/file/list:
get:
tags:
- FileManage
summary: Получить список файлов, размещенных в локальном хранилище
parameters:
- $ref: "#/components/parameters/File.Manage.page"
- $ref: "#/components/parameters/File.Manage.pageLength"
- $ref: "#/components/parameters/File.Manage.isMine"
responses:
'200':
$ref: "#/components/responses/File.Response.FilesList"
'500':
$ref: "#/components/responses/App.Response.Error5XX"
components:
parameters:
File.Download.docId:
name: docId
in: path
description: Идентификатор файла
required: true
schema:
type: string
default: ''
File.Download.docIdQuery:
name: docId
in: query
description: Идентификатор файла
required: true
schema:
type: string
default: ''
File.Download.bucket:
name: bucket
in: path
required: true
schema:
type: string
default: ''
File.Download.bucketQuery:
name: bucket
in: query
required: false
schema:
type: string
default: ''
File.Download.ext:
name: ext
in: path
required: true
schema:
type: string
default: ''
File.Download.extQuery:
name: ext
in: query
required: true
schema:
type: string
default: ''
File.Manage.page:
name: page
in: query
schema:
type: integer
format: int32
default: 0
File.Manage.pageLength:
name: pageLength
in: query
schema:
type: integer
format: int32
default: 10
File.Manage.isMine:
name: isMine
in: query
schema:
type: boolean
default: false
schemas:
File.Manage.FileItem:
required:
- createdTimestamp
- donwloadsCount
- fileType
- guid
- id
- name
- ownerId
- sizeKb
type: object
properties:
id:
type: integer
format: int32
ownerId:
type: string
guid:
type: string
fileType:
type: string
name:
type: string
createdTimestamp:
type: integer
format: int64
donwloadsCount:
type: integer
format: int32
sizeKb:
type: integer
format: int32
readOnly: true
additionalProperties: false
App.Response.Model.Error: # RFC 7807 (Problem Details for HTTP APIs)
type: object
required:
- title
- detail
- request
- time
- errorTraceId
properties:
title:
description: Краткое описание проблемы, понятное человеку
type: string
example: "Entity not found"
detail:
description: Описание конкретно возникшей ошибки, понятное человеку
type: string
example: "Entity [User] with id = [123456] not found. You MUST use PUT to add entity instead of GET"
request:
description: Метод и URL запроса
type: string
example: "PUT /users/123456"
time:
description: Время возникновения ошибки с точностью до миллисекунд
type: string
format: date-time
example: "2023-01-01T12:00:00.000+02:00"
errorTraceId:
description: Идентификатор конкретного возникновения ошибки
type: string
example: "5add1be1-90ab5d42-02fa8b1f-672503f2"
File.Request.UploadData:
type: object
properties:
file:
type: string
format: binary
securitySchemes:
Bearer:
type: http
description: Enter JWT Bearer token
scheme: bearer
bearerFormat: JWT
responses:
File.Response.SuccessUploadFile:
description: Файл загружен успешно. Возвращается идентификатор загруженного файла
content:
text/plain:
schema:
type: string
application/json:
schema:
type: string
text/json:
schema:
type: string
File.Response.SuccessDownloadFile:
description: Файл cкачен успешно. Возвращается файл в формате двоичных данных
content:
text/plain:
schema:
type: string
format: binary
application/json:
schema:
type: string
format: binary
text/json:
schema:
type: string
format: binary
File.Response.FilesList:
description: Success
content:
text/plain:
schema:
type: array
items:
$ref: '#/components/schemas/File.Manage.FileItem'
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/File.Manage.FileItem'
text/json:
schema:
type: array
items:
$ref: '#/components/schemas/File.Manage.FileItem'
App.Response.Error5XX:
description: Внутренняя ошибка сервера
content:
application/problem+json:
schema:
$ref: "#/components/schemas/App.Response.Model.Error"
security:
- Bearer: []
Кодогенерация для спецификации FileApi:
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.3.0))")]
public abstract class FileApiControllerBase : Microsoft.AspNetCore.Mvc.ControllerBase
{
/// <summary>
/// Загрузить файл в локальное хранилище
/// </summary>
/// <remarks>
/// Загружает объект в хранилище
/// </remarks>
/// <returns>Файл загружен успешно. Возвращается идентификатор загруженного файла</returns>
[Microsoft.AspNetCore.Mvc.HttpPost, Microsoft.AspNetCore.Mvc.Route("api/v3/process")]
public abstract System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.ActionResult<string>> UploadFile(Microsoft.AspNetCore.Http.IFormFile body = null);
/// <remarks>
/// Скачать файл из локального хранилища
/// </remarks>
/// <param name = "docId">Идентификатор файла</param>
/// <returns>Файл cкачен успешно. Возвращается файл в формате двоичных данных</returns>
[Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("api/v3/download/{bucket}/{docId}.{ext}")]
public abstract System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> DownloadFileFromBucket([Microsoft.AspNetCore.Mvc.ModelBinding.BindRequired] string docId, [Microsoft.AspNetCore.Mvc.ModelBinding.BindRequired] string bucket, [Microsoft.AspNetCore.Mvc.ModelBinding.BindRequired] string ext);
/// <remarks>
/// Скачать файл из локального хранилища
/// </remarks>
/// <param name = "docId">Идентификатор файла</param>
/// <returns>Файл скачен успешно. Возвращается файл в формате двоичных данных</returns>
[Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("api/v3/download/{docId}.{ext}")]
public abstract System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> DownloadFile([Microsoft.AspNetCore.Mvc.ModelBinding.BindRequired] string docId, [Microsoft.AspNetCore.Mvc.ModelBinding.BindRequired] string ext, [Microsoft.AspNetCore.Mvc.FromQuery] string bucket = "");
/// <summary>
/// Скачать файл из локального хранилища
/// </summary>
/// <param name = "docId">Идентификатор файла</param>
/// <returns>Файл скачен успешно. Возвращается файл в формате двоичных данных</returns>
[Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("api/v3/download/doc")]
public abstract System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> Doc([Microsoft.AspNetCore.Mvc.FromQuery][Microsoft.AspNetCore.Mvc.ModelBinding.BindRequired] string docId, [Microsoft.AspNetCore.Mvc.FromQuery][Microsoft.AspNetCore.Mvc.ModelBinding.BindRequired] string ext, [Microsoft.AspNetCore.Mvc.FromQuery] string bucket = "");
/// <summary>
/// Получить список файлов, размещенных в локальном хранилище
/// </summary>
/// <returns>Success</returns>
[Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("api/v3/file/list")]
public abstract System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.ActionResult<System.Collections.Generic.ICollection<FileItem>>> List([Microsoft.AspNetCore.Mvc.FromQuery] int? page = 0, [Microsoft.AspNetCore.Mvc.FromQuery] int? pageLength = 10, [Microsoft.AspNetCore.Mvc.FromQuery] bool? isMine = false);
}
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.3.0))")]
public partial class FileItem
{
[Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.Always)]
public int Id { get; set; }
[Newtonsoft.Json.JsonProperty("ownerId", Required = Newtonsoft.Json.Required.Always)]
public string OwnerId { get; set; }
[Newtonsoft.Json.JsonProperty("guid", Required = Newtonsoft.Json.Required.Always)]
public string Guid { get; set; }
[Newtonsoft.Json.JsonProperty("fileType", Required = Newtonsoft.Json.Required.Always)]
public string FileType { get; set; }
[Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Always)]
public string Name { get; set; }
[Newtonsoft.Json.JsonProperty("createdTimestamp", Required = Newtonsoft.Json.Required.Always)]
public long CreatedTimestamp { get; set; }
[Newtonsoft.Json.JsonProperty("donwloadsCount", Required = Newtonsoft.Json.Required.Always)]
public int DonwloadsCount { get; set; }
[Newtonsoft.Json.JsonProperty("sizeKb", Required = Newtonsoft.Json.Required.Always)]
public int SizeKb { get; set; }
}
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.3.0))")]
public partial class Error
{
/// <summary>
/// Краткое описание проблемы, понятное человеку
/// </summary>
[Newtonsoft.Json.JsonProperty("title", Required = Newtonsoft.Json.Required.Always)]
public string Title { get; set; }
/// <summary>
/// Описание конкретно возникшей ошибки, понятное человеку
/// </summary>
[Newtonsoft.Json.JsonProperty("detail", Required = Newtonsoft.Json.Required.Always)]
public string Detail { get; set; }
/// <summary>
/// Метод и URL запроса
/// </summary>
[Newtonsoft.Json.JsonProperty("request", Required = Newtonsoft.Json.Required.Always)]
public string Request { get; set; }
/// <summary>
/// Время возникновения ошибки с точностью до миллисекунд
/// </summary>
[Newtonsoft.Json.JsonProperty("time", Required = Newtonsoft.Json.Required.Always)]
public System.DateTimeOffset Time { get; set; }
/// <summary>
/// Идентификатор конкретного возникновения ошибки
/// </summary>
[Newtonsoft.Json.JsonProperty("errorTraceId", Required = Newtonsoft.Json.Required.Always)]
public string ErrorTraceId { get; set; }
private System.Collections.Generic.IDictionary<string, object> _additionalProperties;
[Newtonsoft.Json.JsonExtensionData]
public System.Collections.Generic.IDictionary<string, object> AdditionalProperties
{
get
{
return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary<string, object>());
}
set
{
_additionalProperties = value;
}
}
}
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.3.0))")]
public partial class UploadData
{
[Newtonsoft.Json.JsonProperty("file", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public byte[] File { get; set; }
private System.Collections.Generic.IDictionary<string, object> _additionalProperties;
[Newtonsoft.Json.JsonExtensionData]
public System.Collections.Generic.IDictionary<string, object> AdditionalProperties
{
get
{
return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary<string, object>());
}
set
{
_additionalProperties = value;
}
}
}
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.3.0))")]
public partial class FileResponse : System.IDisposable
{
private System.IDisposable _client;
private System.IDisposable _response;
public int StatusCode { get; private set; }
public System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> Headers { get; private set; }
public System.IO.Stream Stream { get; private set; }
public bool IsPartial
{
get
{
return StatusCode == 206;
}
}
public FileResponse(int statusCode, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, System.IO.Stream stream, System.IDisposable client, System.IDisposable response)
{
StatusCode = statusCode;
Headers = headers;
Stream = stream;
_client = client;
_response = response;
}
public void Dispose()
{
Stream.Dispose();
if (_response != null)
_response.Dispose();
if (_client != null)
_client.Dispose();
}
}
Что касается проблем с производительностью кодогенерации на основе IncrementalGenerators, в нашем случае проблемы полностью не исчезли, но работать стало возможно. Зависания IDE не наблюдалось, однако изменения в спецификации OpenAPI все так же не всегда отображались в генерируемых кодах без перезагрузки проекта IDE. На этом этапе также выяснились базовые ограничения кодогенераторов Roslyn. Они могут быть созданы только в проектах, у которых в качестве целевого фреймворка указан netstandard2.0. Изначально мы не придали значения этому ограничению, так как все работало и исходные коды на языке C# генерировались по заданным спецификациям OpenAPI.
Главные проблемы и неудобства (впрочем, не критичные) возникли при использовании результатов кодогенерации непосредственно в проектах (projects) конечных API. Чтобы вам было легче представить ситуацию и состояние решения к этому моменту, приведу простую схему backend в части использования моделей кодогенерации:

С какими проблемами мы столкнулись
Рассмотрим ряд сложностей и неудобств, с которыми мы столкнулись при использовании кодогенераторов Roslyn:
Проблема зависимостей
Сгенерированные модели из проекта OpenAPI spec Project отказывались «подхватываться» при их использовании в FileUploadAPI, FileDownloadAPI, FileManageAPI. Причем ошибка (error CS0246: The type or namespace name ‘...’ could not be found) возникала временно, в момент сборки. В остальное время ссылки на модели из OpenAPI specs Project доступны и ошибки в IDE не отображаются. Проблема связана с форматом их подключения. При подключении служебной библиотеки кодогенерации в виде nuget-пакета ошибка при сборке API не возникает. Мы были ограничены во времени для дальнейшего изучения и решения проблемы зависимостей. Если вы сталкивались с таким — расскажите в комментариях, какое решение вам удалось найти.
Проблема технического долга
Если возникает необходимость использовать результаты кодогенерации одной спецификации OpenAPI в нескольких проектах, появляется технический долг на поддержку наследования и создание заглушек для неиспользуемых методов. Ухудшается читаемость кода и увеличивается время на работу с кодовой базой. Это проявляется также в случае, когда изменения вносятся в спецификацию OpenAPI, т.к. генерируемые абстрактные классы контроллеров требуют поддержки во всех проектах, использующих изменяемую спецификацию.
Проблема доработок
Ряд атрибутов из OpenAPI-спецификации не добавляется в классы кодогенерации.
Как мы решали проблемы
Ни одна из возникших сложностей не была критичной. Ниже приведем возможные решения, к которым мы пришли в рамках текущего состояния:
1. Упаковка OpenAPI spec Project в nuget-пакет
НО! Только как временное решение для возможности проверки всего процесса. Поэтому необходимо время на понимание проблемы.
2. Разделение базовой спецификации на отдельные (посервисно)
НО! Это неудобно с точки зрения целостности доменной области. Есть необходимость нарезать спецификации на уровне реализации сервисов.
3. Настройка или доработка NSwag
НО! Возможны дополнительные временные издержки, связанные с доработкой кодогенерации — смотрите опыт команды МТС.
Заключение
По результатам проверки возможности реализации технического решения (фух!) мы пришли к следующим выводам:
Для более общего понимания и принятия решения стоит рассмотреть использование Swagger вместо NSwag в качестве прикладного инструмента кодогенерации.
Также следует рассмотреть альтернативу кодогенераторам Roslyn (например, в виде шаблонизатора T4).
К моменту написания этого материала мы еще не попробовали использовать связку Roslyn+Swagger, а вот с реализацией кодогенерации на основе шаблонизатора T4 поработали. Что из этого получилось — расскажем подробнее в следующей статье.
Спасибо за внимание!
Авторские материалы для разработчиков и архитекторов мы также публикуем в наших соцсетях – ВКонтакте и Telegram.