Pull to refresh
103.59
SimbirSoft
Лидер в разработке современных ИТ-решений на заказ

Design API First. Кодогенерация Roslyn

Level of difficultyMedium
Reading time16 min
Views2K

Привет, 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:

  1. В качестве инструмента кодогенерации мы выбрали кодогенерацию с использованием компилятора Roslyn.

  2. В качестве прикладной инструментальной библиотеки кодогенерации был выбран NSwag.

  3. В качестве реализации была разработана служебная библиотека, которая использовала Source Generators и NSwag.

  4. В качестве реализации была разработана служебная библиотека, которая использовала Incremental Generators и NSwag.

  5. Библиотека кодогенерации на 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

НО! Возможны дополнительные временные издержки, связанные с доработкой кодогенерации — смотрите опыт команды МТС.

Заключение

По результатам проверки возможности реализации технического решения (фух!) мы пришли к следующим выводам:

  1. Для более общего понимания и принятия решения стоит рассмотреть использование Swagger вместо NSwag в качестве прикладного инструмента кодогенерации.

  2. Также следует рассмотреть альтернативу кодогенераторам Roslyn (например, в виде шаблонизатора T4).

К моменту написания этого материала мы еще не попробовали использовать связку Roslyn+Swagger, а вот с реализацией кодогенерации на основе шаблонизатора T4 поработали. Что из этого получилось — расскажем подробнее в следующей статье.

Спасибо за внимание!

Авторские материалы для разработчиков и архитекторов мы также публикуем в наших соцсетях – ВКонтакте и Telegram.

Tags:
Hubs:
Rating0
Comments4

Articles

Information

Website
www.simbirsoft.com
Registered
Founded
Employees
1,001–5,000 employees
Location
Россия