Как стать автором
Обновить

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

Уровень сложностиСредний
Время на прочтение16 мин
Количество просмотров2K
Рейтинг0
Комментарии4

Комментарии 4

Не обязательно заставлять генератор в режиме проектирования обрабатывать изменения и создавать код на лету (параллельно перезапуская IDE, т.к. сгенерированные файлы отказывались то меняться на лету, то вовсе быть видимыми/доступными в реалтайме).

Вариант: генерировать файлы при сборке проекта вполне рабочий и разумный - пробовали?

Собственно, в этом и проблема проявлялась у нас. При сборке целевого проекта API, изменения контракта в yaml файле не применяются на генерируемые файлы в подключаемой библиотеке с артефактами кодогенерации. Причем эта проблема плавающая (не всегда проявляется).

Использую Swagger/OpenAPI при разработке backend на Java. Использую подход API First. Что для себя усвоил. При описании paths

  1. использую теги, чтоб методы были сгенерированы в правильные интерфейсы

  2. всегда указываю operationId, чтоб был верно смапирован метод контроллера при имплементации

  3. всегда описываю request/response схему, чтоб были сгенерированы DTOшки

  4. при описании компонентов не использую точки и различные не стандартные знаки, иначе могут быть проблемы в сгенерированом коде. Например вы оперируете названием File.Response.SuccessDownloadFile , я бы использовал название FileDownloadSuccessResponse

в Java я получаю на выходе сгенерированные DTOшки и интерфейсы endpointов с дефаултной имплементацией return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

ну в принципе и всё. Т.е. для корректной работы реалезую имплементацию endpointов (методов) интерфейсов и DTOшки через мапперы далее использую модель данных в программе.

Далее в статье упоминаете о технологическом долге, что надо что-то там обновлять на нескольких сервисах. Тут думаю есть немного недопонимание. Swagger в данной ситуации является API контрактом (Interface Agreement), и у него есть версионость. Так вот, вы этим самым говорите, что такая-то версия вашего приложения общается вот таким-то языком и обозначаете версию вашего swaggera. Другие приложения, которые с вами общаются, должны поддерживать понятный вашему приложению язык общения, т.е. должны присылать запросы в соответствии API контрактом, но и ответ им будет понятен. Но если вы вносите правки в swagger, вы по сути делаете новую версию, меняете язык общения с вашим приложением, и другие должны у себя внести этим самые правки. т.е. это не является технологическим долгом, я этим вы наводите порядок и исключаете непонятные несогласованности в общении приложений между собой. Необходимо в идеале обеспечить обратную совместимость API. Чтоб при его обновлении не нужно было сразу везде всё менять. Тут уже вступает логика семантической версионности.

Спасибо, что делитесь своим опытом. По части версионности все верно. Сопровождение обновленных контрактов это не технический долг. В статье мы обозначили немного другую проблему, которая возникает при разделении одной OpenAPI спецификации между несколькими сервисами (или контроллерами).


Для понимания ситуации ниже приведем листинг одного из контроллеров. В нем используются только три метода из спецификации OpenAPI (Swagger), а оставшаяся часть этой спецификации используется в других сервисах.

Поэтому нам необходимо пометить неиспользуемую реализацию заглушек атрибутом [NonAction], чтобы закрыть к ним доступ для этого контроллера (сервиса).

/// <summary>
/// API для скачивания файлов
/// </summary>
[ApiVersion("3.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class DownloadController : FileApiControllerBase
{
    private static readonly string _defaultBucket = Guid.Empty.ToString("N");
    private readonly IStorageClient _storageClient;
    private readonly IGrpcFileClientWrapper _grpcClientWrapper;

    public DownloadController(IStorageClient storageClient, IGrpcFileClientWrapper grpcClientWrapper)
    {
        _storageClient = storageClient;
        _grpcClientWrapper = grpcClientWrapper;
    }

    /// <summary>
    /// Скачать файл из локального хранилища
    /// </summary>
    /// <param name="docId">Идентификатор файла</param>
    /// <param name="ext">Расширение файла</param>
    /// <param name="bucket">Контейнер файла</param>
    /// <returns>Файл для скачивания</returns>
    [MapToApiVersion("3.0")]
    [HttpGet("{docId}.{ext}")]
    [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(FileStreamResult))]
    [ProducesResponseType(StatusCodes.Status500InternalServerError, Type = typeof(Error))]
    public override async Task<IActionResult> DownloadFile(string docId, string ext, string bucket = "")
    {
         return  await Task.FromResult(RedirectToAction("DownloadFileFromBucket", new { docId = docId, ext = ext, bucket = bucket }));
    }

    /// <summary>
    /// Скачать файл из локального хранилища
    /// </summary>
    /// <param name="docId">Идентификатор файла</param>
    /// <param name="ext">Расширение файла</param>
    /// <param name="bucket">Контейнер файла</param>
    /// <returns>Файл для скачивания</returns>
    [MapToApiVersion("3.0")]
    [HttpGet("{bucket}/{docId}.{ext}")]
    [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(FileStreamResult))]
    [ProducesResponseType(StatusCodes.Status500InternalServerError, Type = typeof(Error))]
    public override  async Task<IActionResult> Doc(string docId, string ext, string bucket = "")
    {
        return await Task.FromResult(RedirectToAction("DownloadFileFromBucket", new { docId = docId, ext = ext, bucket = bucket }));
    }

    /// <summary>
    /// Скачать файл из локального хранилища
    /// </summary>
    /// <param name="docId">Идентификатор файла</param>
    /// <param name="ext">Расширение файла</param>
    /// <param name="bucket">Контейнер файла</param>
    /// <returns>Файл для скачивания</returns>
    [MapToApiVersion("3.0")]
    [HttpGet("doc")]
    [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(FileStreamResult))]
    [ProducesResponseType(StatusCodes.Status500InternalServerError, Type = typeof(Error))]

    //  [ResponseCache(Location = ResponseCacheLocation.Any, Duration = 16800, VaryByQueryKeys = new string[] { "bucket", "docId" })]
    public override async Task<IActionResult> DownloadFileFromBucket(string docId, string? bucket = "", string ext="")
    {
        if (string.IsNullOrEmpty(bucket))
            bucket = _defaultBucket;
        if (docId.Contains('-'))
            docId = docId.Replace("-", string.Empty);

        var prefix = new Uri(Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetName().CodeBase)).LocalPath;
        var path = Path.Combine(prefix, "doc", bucket, docId);
        Task task = null;
        Stream stream = new MemoryStream();

        if (System.IO.File.Exists(path))
        {
            stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
        }
        else
        {
            var tryLoad = false;
            try
            {
                var byteArray = await _storageClient.DownloadFileAsync(docId);
                stream.Write(byteArray, 0, byteArray.Length);
                stream.Seek(0, SeekOrigin.Begin);
                tryLoad = byteArray.Length > 0;
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
                Task.Run(() => {
                    try
                    {
                        var dir = Path.GetDirectoryName(path);
                        if (!Directory.Exists(dir))
                        {
                            Directory.CreateDirectory(dir);
                            System.IO.File.WriteAllBytes(path, byteArray);
                        }
                    }
                    catch (Exception ex)
                    {
                        //todo log
                    }
                });
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
            }
            catch (Exception ex)
            {
                //todo log
            }
            if (!tryLoad)
            {
                //check tempStorage
            }
            if (stream.Length == 0)
            {
                stream.Close();
                System.IO.File.Delete(path);
            }
        }

#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
        _grpcClientWrapper.RegisterDownloadFileAsync(docId, this.GetHeader("UserId"));
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
      
        var contentType =string.IsNullOrEmpty(ext)? "application/octet-stream" : ext.Contains('/')? ext: GetMimeFromExt(ext);
        //todo replace stubs to real content type
        return File(stream, contentType);
    }

    private static string GetMimeFromExt(string ext)
    {
        //todo switch with predefinded mimes
        return $"application/{ext}";
    }

    [NonAction]
    public override Task<ActionResult<string>> UploadFile(IFormFile body = null)
    {
        throw new NotImplementedException();
    }
    [NonAction]
    public override Task<ActionResult<ICollection<FileItem>>> List([FromQuery] int? page = 0, [FromQuery] int? pageLength = 10, [FromQuery] bool? isMine = false)
    {
        throw new NotImplementedException();
    }
}

То есть при выпуске новой версии, в случае разделения реализации (implementation) между разными контроллерами или сервисами API, нам требуется исключить неиспользуемые определения спецификации (paths), которые предоставляются кодогенерацией, для каждого конкретного случая частичного использования. Это и определяет наш технический долг, который не возникает при посервисной реализации спецификаций OpenAPI.

Однако посервисная реализация OpenAPI (как вариант решения) нам тоже не очень подходит. Гранулярность контрактов в итоге может оказаться слишком малой (множество мелких спецификаций), контексты предметной области или определенных бизнес-процессов будут размазаны по нескольким спецификациям, сопровождение и проектирование затруднено.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий