Files
jellyfin/Jellyfin.Server/Filters/CachingOpenApiProvider.cs
Tim Eisele 23b48a0d0f
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI / OpenAPI - HEAD (push) Has been cancelled
OpenAPI / OpenAPI - BASE (push) Has been cancelled
OpenAPI / OpenAPI - Difference (push) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Upgrade Swashbuckle and fix OpenAPI spec (#15886)
2026-01-01 18:46:51 -07:00

94 lines
3.5 KiB
C#

using System;
using AsyncKeyedLock;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Jellyfin.Server.Filters;
/// <summary>
/// OpenApi provider with caching.
/// </summary>
internal sealed class CachingOpenApiProvider : ISwaggerProvider
{
private const string CacheKey = "openapi.json";
private static readonly MemoryCacheEntryOptions _cacheOptions = new() { SlidingExpiration = TimeSpan.FromMinutes(5) };
private static readonly AsyncNonKeyedLocker _lock = new(1);
private static readonly TimeSpan _lockTimeout = TimeSpan.FromSeconds(1);
private readonly IMemoryCache _memoryCache;
private readonly SwaggerGenerator _swaggerGenerator;
private readonly SwaggerGeneratorOptions _swaggerGeneratorOptions;
private readonly ILogger<CachingOpenApiProvider> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="CachingOpenApiProvider"/> class.
/// </summary>
/// <param name="optionsAccessor">The options accessor.</param>
/// <param name="apiDescriptionsProvider">The api descriptions provider.</param>
/// <param name="schemaGenerator">The schema generator.</param>
/// <param name="memoryCache">The memory cache.</param>
/// <param name="logger">The logger.</param>
public CachingOpenApiProvider(
IOptions<SwaggerGeneratorOptions> optionsAccessor,
IApiDescriptionGroupCollectionProvider apiDescriptionsProvider,
ISchemaGenerator schemaGenerator,
IMemoryCache memoryCache,
ILogger<CachingOpenApiProvider> logger)
{
_swaggerGeneratorOptions = optionsAccessor.Value;
_swaggerGenerator = new SwaggerGenerator(_swaggerGeneratorOptions, apiDescriptionsProvider, schemaGenerator);
_memoryCache = memoryCache;
_logger = logger;
}
/// <inheritdoc />
public OpenApiDocument GetSwagger(string documentName, string? host = null, string? basePath = null)
{
if (_memoryCache.TryGetValue(CacheKey, out OpenApiDocument? openApiDocument) && openApiDocument is not null)
{
return AdjustDocument(openApiDocument, host, basePath);
}
using var acquired = _lock.LockOrNull(_lockTimeout);
if (_memoryCache.TryGetValue(CacheKey, out openApiDocument) && openApiDocument is not null)
{
return AdjustDocument(openApiDocument, host, basePath);
}
if (acquired is null)
{
throw new InvalidOperationException("OpenApi document is generating");
}
try
{
openApiDocument = _swaggerGenerator.GetSwagger(documentName);
}
catch (Exception ex)
{
_logger.LogError(ex, "OpenAPI generation error");
throw;
}
_memoryCache.Set(CacheKey, openApiDocument, _cacheOptions);
return AdjustDocument(openApiDocument, host, basePath);
}
private OpenApiDocument AdjustDocument(OpenApiDocument document, string? host, string? basePath)
{
document.Servers = _swaggerGeneratorOptions.Servers.Count != 0
? _swaggerGeneratorOptions.Servers
: string.IsNullOrEmpty(host) && string.IsNullOrEmpty(basePath)
? []
: [new OpenApiServer { Url = $"{host}{basePath}" }];
return document;
}
}