Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IDocumentProvider service and its implementation (2) #1677

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,5 @@ NSwagStudio*.nupkg
/samples/WithoutMiddleware/Sample.AspNetCore20/.vs/Sample.AspNetCore20/v15/Server/sqlite3
/samples/WithoutMiddleware/Sample.AspNetCore20/.vs/Sample.AspNetCore20/DesignTimeBuild
/samples/.vs/*
.vscode/
/src/.cr/*
20 changes: 20 additions & 0 deletions src/NSwag.AspNetCore/IDocumentProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//-----------------------------------------------------------------------
// <copyright file="IDocumentProvider.cs" company="NSwag">
// Copyright (c) Rico Suter. All rights reserved.
// </copyright>
// <license>https://github.com/NSwag/NSwag/blob/master/LICENSE.md</license>
// <author>Rico Suter, mail@rsuter.com</author>
//-----------------------------------------------------------------------

using System.IO;
using System.Threading.Tasks;

namespace Microsoft.Extensions.ApiDescription
{
// This service will be looked up by name from the service collection when using
// the Microsoft.Extensions.ApiDescription tool
internal interface IDocumentProvider
{
Task GenerateAsync(string documentName, TextWriter writer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,106 +6,108 @@
// <author>Rico Suter, mail@rsuter.com</author>
//-----------------------------------------------------------------------

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.Options;
using NSwag.SwaggerGeneration;
using NSwag.SwaggerGeneration.AspNetCore;

namespace NSwag.AspNetCore.Middlewares
{
/// <summary>Generates a Swagger specification on a given path.</summary>
public class AspNetCoreToSwaggerMiddleware
public class SwaggerMiddleware
{
private readonly RequestDelegate _nextDelegate;
private readonly string _path;
private readonly SwaggerSettings<AspNetCoreToSwaggerGeneratorSettings> _settings;
private readonly SwaggerJsonSchemaGenerator _schemaGenerator;
private readonly string _documentName;
private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionGroupCollectionProvider;
private readonly IOptions<MvcOptions> _mvcOptions;
private readonly IOptions<MvcJsonOptions> _mvcJsonOptions;
private readonly SwaggerDocumentProvider _documentProvider;
private readonly SwaggerMiddlewareSettings _settings;

private int _version;
private string _schemaJson;
private Exception _schemaException;
private string _swaggerJson;
private Exception _swaggerException;
private DateTimeOffset _schemaTimestamp;

/// <summary>Initializes a new instance of the <see cref="WebApiToSwaggerMiddleware"/> class.</summary>
/// <param name="nextDelegate">The next delegate.</param>
/// <param name="apiDescriptionGroupCollectionProvider">The <see cref="IApiDescriptionGroupCollectionProvider"/>.</param>
/// <param name="mvcOptions">The options.</param>
/// <param name="mvcJsonOptions">The json options.</param>
/// <param name="settings">The settings.</param>
/// <param name="schemaGenerator">The schema generator.</param>
public AspNetCoreToSwaggerMiddleware(RequestDelegate nextDelegate, IApiDescriptionGroupCollectionProvider apiDescriptionGroupCollectionProvider, IOptions<MvcOptions> mvcOptions, IOptions<MvcJsonOptions> mvcJsonOptions, SwaggerSettings<AspNetCoreToSwaggerGeneratorSettings> settings, SwaggerJsonSchemaGenerator schemaGenerator)
public SwaggerMiddleware(
RequestDelegate nextDelegate,
IServiceProvider serviceProvider,
string documentName,
Action<SwaggerMiddlewareSettings> configure)
{
_nextDelegate = nextDelegate;
_documentName = documentName;

_apiDescriptionGroupCollectionProvider = serviceProvider.GetService<IApiDescriptionGroupCollectionProvider>() ??
throw new InvalidOperationException("API Explorer not registered in DI.");
_documentProvider = serviceProvider.GetService<SwaggerDocumentProvider>() ??
throw new InvalidOperationException("The NSwag DI services are not registered: Call " + nameof(SwaggerExtensions.AddSwagger) + " in ConfigureServices().");

var settings = new SwaggerMiddlewareSettings();
configure?.Invoke(settings);
_settings = settings;
_path = settings.ActualSwaggerRoute;
_schemaGenerator = schemaGenerator;
_apiDescriptionGroupCollectionProvider = apiDescriptionGroupCollectionProvider;
_mvcOptions = mvcOptions;
_mvcJsonOptions = mvcJsonOptions;
}

/// <summary>Invokes the specified context.</summary>
/// <param name="context">The context.</param>
/// <returns>The task.</returns>
public async Task Invoke(HttpContext context)
{
if (context.Request.Path.HasValue && string.Equals(context.Request.Path.Value.Trim('/'), _path.Trim('/'), StringComparison.OrdinalIgnoreCase))
if (context.Request.Path.HasValue && string.Equals(context.Request.Path.Value.Trim('/'), _settings.Path.Trim('/'), StringComparison.OrdinalIgnoreCase))
{
var schemaJson = await GenerateSwaggerAsync(context);
context.Response.StatusCode = 200;
context.Response.Headers["Content-Type"] = "application/json; charset=utf-8";
await context.Response.WriteAsync(schemaJson);
}
else
{
await _nextDelegate(context);
}
}

/// <summary>Generates the Swagger specification.</summary>
/// <param name="context">The context.</param>
/// <returns>The Swagger specification.</returns>
protected virtual async Task<string> GenerateSwaggerAsync(HttpContext context)
{
if (_schemaException != null && _schemaTimestamp + _settings.ExceptionCacheTime > DateTimeOffset.UtcNow)
throw _schemaException;
if (_swaggerException != null && _schemaTimestamp + _settings.ExceptionCacheTime > DateTimeOffset.UtcNow)
{
throw _swaggerException;
}

var apiDescriptionGroups = _apiDescriptionGroupCollectionProvider.ApiDescriptionGroups;
if (apiDescriptionGroups.Version == Volatile.Read(ref _version) && _schemaJson != null)
return _schemaJson;
if (apiDescriptionGroups.Version == Volatile.Read(ref _version) && _swaggerJson != null)
{
return _swaggerJson;
}

try
{
var serializerSettings = _mvcJsonOptions.Value.SerializerSettings;
var settings = _settings.CreateGeneratorSettings(serializerSettings, _mvcOptions.Value);
var generator = new AspNetCoreToSwaggerGenerator(settings, _schemaGenerator);
var document = await generator.GenerateAsync(apiDescriptionGroups);
var document = await _documentProvider.GenerateAsync(_documentName);

document.Host = context.Request.Host.Value ?? "";
document.Schemes.Add(context.Request.Scheme == "http" ? SwaggerSchema.Http : SwaggerSchema.Https);
document.BasePath = context.Request.PathBase.Value?.Substring(0, context.Request.PathBase.Value.Length - (_settings.MiddlewareBasePath?.Length ?? 0)) ?? "";

_settings.PostProcess?.Invoke(document);
_schemaJson = document.ToJson();
_schemaException = null;
_settings.PostProcess?.Invoke(context.Request, document);

_swaggerJson = document.ToJson();
_swaggerException = null;
_version = apiDescriptionGroups.Version;
_schemaTimestamp = DateTimeOffset.UtcNow;
}
catch (Exception exception)
{
_schemaJson = null;
_schemaException = exception;
_swaggerJson = null;
_swaggerException = exception;
_schemaTimestamp = DateTimeOffset.UtcNow;
throw _schemaException;
throw _swaggerException;
}

return _schemaJson;
return _swaggerJson;
}
}
}
29 changes: 29 additions & 0 deletions src/NSwag.AspNetCore/Middlewares/SwaggerMiddlewareSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//-----------------------------------------------------------------------
// <copyright file="SwaggerMiddleware.cs" company="NSwag">
// Copyright (c) Rico Suter. All rights reserved.
// </copyright>
// <license>https://github.com/NSwag/NSwag/blob/master/LICENSE.md</license>
// <author>Rico Suter, mail@rsuter.com</author>
//-----------------------------------------------------------------------

using Microsoft.AspNetCore.Http;
using System;

namespace NSwag.AspNetCore.Middlewares
{
/// <summary>The Swagger middleware settings.</summary>
public class SwaggerMiddlewareSettings
{
/// <summary>Gets or sets the path to serve the OpenAPI/Swagger document.</summary>
public string Path { get; set; } = "swagger/v1/swagger.json";

/// <summary>Gets or sets for how long a <see cref="Exception"/> caught during schema generation is cached.</summary>
public TimeSpan ExceptionCacheTime { get; set; } = TimeSpan.FromSeconds(10);

/// <summary>Gets or sets the Swagger post process action.</summary>
public Action<HttpRequest, SwaggerDocument> PostProcess { get; set; }

/// <summary>Gets or sets the middleware base path (must start with '/').</summary>
public string MiddlewareBasePath { get; set; }
}
}
63 changes: 63 additions & 0 deletions src/NSwag.AspNetCore/SwaggerDocumentProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//-----------------------------------------------------------------------
// <copyright file="NSwagDocumentProvider.cs" company="NSwag">
// Copyright (c) Rico Suter. All rights reserved.
// </copyright>
// <license>https://github.com/NSwag/NSwag/blob/master/LICENSE.md</license>
// <author>Rico Suter, mail@rsuter.com</author>
//-----------------------------------------------------------------------

using Microsoft.Extensions.ApiDescription;
using System;
using System.IO;
using System.Threading.Tasks;

namespace NSwag.AspNetCore
{
internal class SwaggerDocumentProvider : IDocumentProvider
{
private readonly IServiceProvider _serviceProvider;
private readonly SwaggerDocumentRegistry _registry;

public SwaggerDocumentProvider(IServiceProvider serviceProvider, SwaggerDocumentRegistry registry)
{
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
_registry = registry ?? throw new ArgumentNullException(nameof(registry));
}

public async Task<SwaggerDocument> GenerateAsync(string documentName)
{
if (documentName == null)
{
throw new ArgumentNullException(nameof(documentName));
}

_registry.Documents.TryGetValue(documentName, out var settings);
if (settings == null)
{
throw new InvalidOperationException($"No registered OpenAPI/Swagger document found for document name '{documentName}'. " +
$"Add with the AddSwagger()/AddOpenApi() methods in ConfigureServices().");
}

return await settings.GenerateAsync(_serviceProvider);
}

// Called by the Microsoft.Extensions.ApiDescription tool
async Task IDocumentProvider.GenerateAsync(string documentName, TextWriter writer)
{
if (documentName == null)
{
throw new ArgumentNullException(nameof(documentName));
}

if (writer == null)
{
throw new ArgumentNullException(nameof(writer));
}

var document = await GenerateAsync(documentName);

var json = document.ToJson();
await writer.WriteAsync(json);
}
}
}
61 changes: 61 additions & 0 deletions src/NSwag.AspNetCore/SwaggerDocumentRegistry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//-----------------------------------------------------------------------
// <copyright file="DocumentRegistry.cs" company="NSwag">
// Copyright (c) Rico Suter. All rights reserved.
// </copyright>
// <license>https://github.com/NSwag/NSwag/blob/master/LICENSE.md</license>
// <author>Rico Suter, mail@rsuter.com</author>
//-----------------------------------------------------------------------

using NSwag.SwaggerGeneration;
using NSwag.SwaggerGeneration.AspNetCore;
using System;
using System.Collections.Generic;

namespace NSwag.AspNetCore
{
/// <summary>Registry with Swagger document generators.</summary>
public class SwaggerDocumentRegistry
{
private readonly Dictionary<string, ISwaggerGenerator> _documents;

/// <summary>Initializes a new instance of the <see cref="SwaggerDocumentRegistry"/> class.</summary>
public SwaggerDocumentRegistry()
{
_documents = new Dictionary<string, ISwaggerGenerator>(StringComparer.Ordinal);
}

/// <summary>Adds a document to the registry.</summary>
/// <param name="configure">The configure action.</param>
/// <returns>The registry.</returns>
public SwaggerDocumentRegistry AddDocument(Action<AspNetCoreToSwaggerGeneratorSettings> configure = null)
{
return AddDocument("v1", configure);
}

/// <summary>Adds a document to the registry.</summary>
/// <param name="documentName">The document name.</param>
/// <param name="configure">The configure action.</param>
/// <returns>The registry.</returns>
public SwaggerDocumentRegistry AddDocument(string documentName, Action<AspNetCoreToSwaggerGeneratorSettings> configure = null)
{
var settings = new AspNetCoreToSwaggerGeneratorSettings();
configure?.Invoke(settings);

var generator = new AspNetCoreToSwaggerGenerator(settings);
return AddDocument(documentName, generator);
}

/// <summary>Adds a document to the registry.</summary>
/// <param name="documentName">The document name.</param>
/// <param name="swaggerGenerator">The Swagger generator.</param>
/// <returns>The registry.</returns>
public SwaggerDocumentRegistry AddDocument(string documentName, ISwaggerGenerator swaggerGenerator)
{
_documents[documentName] = swaggerGenerator;
return this;
}

/// <summary>Gets a dictionary with all registered documents.</summary>
public IReadOnlyDictionary<string, ISwaggerGenerator> Documents => _documents;
}
}
Loading