aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server/Filters/CachingOpenApiProvider.cs
blob: 833b6844449ab46563bd4cf82647f4bdfe406052 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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;
    }
}