Skip to content

Commit

Permalink
Merge
Browse files Browse the repository at this point in the history
  • Loading branch information
RicoSuter committed Oct 23, 2018
2 parents 2da52e8 + a13c550 commit f388a2e
Show file tree
Hide file tree
Showing 77 changed files with 25,083 additions and 121 deletions.
34 changes: 34 additions & 0 deletions src/NSwag.AspNetCore/AspNetCoreToOpenApiMiddlewareSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//-----------------------------------------------------------------------
// <copyright file="AspNetCoreToOpenApiMiddlewareSettings.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;
using Microsoft.Extensions.DependencyInjection;
using NSwag.AspNetCore.Middlewares;

namespace NSwag.AspNetCore
{
/// <summary>
/// The settings for <see cref="OpenApiExtensions.UseOpenApi"/> and <see cref="AspNetCoreToOpenApiMiddleware"/>.
/// </summary>
/// <remarks>
/// Reviewers: Is it useful to have a post-processing step i.e. PostProcess property here that all documents share?
/// </remarks>
public class AspNetCoreToOpenApiMiddlewareSettings
{
/// <summary>
/// Gets or sets the Swagger URL route. Must start with '/' and should contain a '{documentName}' route
/// parameter.
/// </summary>
public string SwaggerRoute { get; set; } = "/swagger/{documentName}/swagger.json";

/// <summary>
/// Gets or sets for how long an <see cref="Exception"/> caught during schema generation is cached.
/// </summary>
public TimeSpan ExceptionCacheTime { get; set; } = TimeSpan.FromSeconds(10);
}
}
23 changes: 23 additions & 0 deletions src/NSwag.AspNetCore/DependencyInjection/IOpenApiBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//-----------------------------------------------------------------------
// <copyright file="IOpenApiBuilder.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.DependencyInjection;

namespace NSwag.AspNetCore.DependencyInjection
{
/// <summary>
/// An interface for configuring Open Api documents.
/// </summary>
public interface IOpenApiBuilder
{
/// <summary>
/// Gets the <see cref="IServiceCollection"/>.
/// </summary>
IServiceCollection Services { get; }
}
}
22 changes: 22 additions & 0 deletions src/NSwag.AspNetCore/DependencyInjection/OpenApiBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//-----------------------------------------------------------------------
// <copyright file="OpenApiBuilder.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.DependencyInjection;

namespace NSwag.AspNetCore.DependencyInjection
{
internal class OpenApiBuilder : IOpenApiBuilder
{
public OpenApiBuilder(IServiceCollection services)
{
Services = services;
}

public IServiceCollection Services { get; }
}
}
39 changes: 20 additions & 19 deletions src/NSwag.AspNetCore/DocumentRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,45 @@

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

namespace NSwag.AspNetCore
{
internal class DocumentRegistry
{
private readonly Dictionary<string, RegisteredDocument> _documents;
private readonly Dictionary<string, OpenApiDocumentSettings> _documentSettings;

public DocumentRegistry()
public DocumentRegistry(IEnumerable<OpenApiDocumentSettings> documentSettings)
{
_documents = new Dictionary<string, RegisteredDocument>(StringComparer.Ordinal);
}

public RegisteredDocument this[string documentName]
{
get
_documentSettings = new Dictionary<string, OpenApiDocumentSettings>(StringComparer.Ordinal);
foreach (var settings in documentSettings)
{
if (documentName == null)
if (_documentSettings.ContainsKey(settings.DocumentName))
{
throw new ArgumentNullException(nameof(documentName));
throw new InvalidOperationException($"Repeated document name '{settings.DocumentName}' in " +
"registry. Open API document names must be unique.");
}

_documents.TryGetValue(documentName, out var document);
return document;
_documentSettings[settings.DocumentName] = settings;
}

set
DocumentNames = new ReadOnlyCollection<string>(_documentSettings.Keys.ToList());
}

public IReadOnlyCollection<string> DocumentNames { get; }

public OpenApiDocumentSettings this[string documentName]
{
get
{
if (documentName == null)
{
throw new ArgumentNullException(nameof(documentName));
}

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

_documents[documentName] = value;
_documentSettings.TryGetValue(documentName, out var document);
return document;
}
}
}
Expand Down
92 changes: 92 additions & 0 deletions src/NSwag.AspNetCore/Middlewares/AspNetCoreToOpenApiMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//-----------------------------------------------------------------------
// <copyright file="AspNetCoreToOpenApiMiddleware.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;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.ObjectPool;

namespace NSwag.AspNetCore.Middlewares
{
internal class AspNetCoreToOpenApiMiddleware
{
private const string DocumentNameEntry = "documentName";
private readonly NSwagDocumentProvider _documentProvider;
private readonly TemplateMatcher _matcher;
private readonly RequestDelegate _nextDelegate;
private readonly AspNetCoreToOpenApiMiddlewareSettings _settings;

private string _failedDocumentName;
private Exception _documentException;
private DateTimeOffset _exceptionTimestamp;

public AspNetCoreToOpenApiMiddleware(
RequestDelegate nextDelegate,
NSwagDocumentProvider documentProvider,
ObjectPool<UriBuildingContext> pool,
AspNetCoreToOpenApiMiddlewareSettings settings)
{
_documentProvider = documentProvider;

var template = TemplateParser.Parse(settings.SwaggerRoute);
_matcher = new TemplateMatcher(template, defaults: null);
_nextDelegate = nextDelegate;
_settings = settings;
}

public async Task Invoke(HttpContext context)
{
var values = new RouteValueDictionary();
if (_matcher.TryMatch(context.Request.Path, values))
{
var documentName = "v1";
if (values.TryGetValue(DocumentNameEntry, out var document))
{
documentName = document as string;
if (documentName == null)
{
throw new InvalidOperationException($"Unexpected null '{DocumentNameEntry}' entry in " +
$"{nameof(RouteValueDictionary)}.");
}
}

var documentString = await GenerateOpenApiAsync(documentName);
context.Response.StatusCode = 200;
context.Response.Headers["Content-Type"] = "application/json; charset=utf-8";
await context.Response.WriteAsync(documentString);
}
else
await _nextDelegate(context);
}

protected virtual async Task<string> GenerateOpenApiAsync(string documentName)
{
var now = DateTimeOffset.UtcNow;
if (_documentException != null &&
_exceptionTimestamp + _settings.ExceptionCacheTime > now &&
string.Equals(_failedDocumentName, documentName, StringComparison.Ordinal))
throw _documentException;

try
{
return await _documentProvider.GenerateAsync(documentName);
}
catch (Exception exception)
{
_documentException = exception;
_exceptionTimestamp = now;
_failedDocumentName = documentName;

throw _documentException;
}
}
}
}
61 changes: 41 additions & 20 deletions src/NSwag.AspNetCore/NSwagDocumentProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
Expand All @@ -24,6 +25,10 @@ internal class NSwagDocumentProvider : IDocumentProvider
private readonly IOptions<MvcJsonOptions> _mvcJsonOptions;
private readonly DocumentRegistry _registry;

private int _version;
private string _lastDocument;
private string _lastDocumentName;

public NSwagDocumentProvider(
DocumentRegistry registry,
IApiDescriptionGroupCollectionProvider apiDescriptionGroupCollectionProvider,
Expand Down Expand Up @@ -56,35 +61,28 @@ public NSwagDocumentProvider(
_registry = registry;
}

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

var documentInfo = _registry[documentName];
if (documentInfo == null)
var apiDescriptionGroups = _apiDescriptionGroupCollectionProvider.ApiDescriptionGroups;
var newVersion = apiDescriptionGroups.Version;
if (_lastDocument != null &&
string.Equals(_lastDocumentName, documentName, StringComparison.Ordinal) &&
newVersion == Volatile.Read(ref _version))
{
throw new InvalidOperationException($"No registered document found for document name '{documentName}'.");
return _lastDocument;
}

documentInfo.Settings.ApplySettings(_mvcJsonOptions.Value.SerializerSettings, _mvcOptions.Value);
var document = await GenerateAsyncCore(documentName);
_lastDocument = document.ToJson();
_lastDocumentName = documentName;
Volatile.Write(ref _version, newVersion);

SwaggerDocument document;
if (documentInfo.Settings is AspNetCoreToSwaggerGeneratorSettings aspnetcore)
{
var generator = new AspNetCoreToSwaggerGenerator(aspnetcore, documentInfo.SchemaGenerator);
document = await generator.GenerateAsync(_apiDescriptionGroupCollectionProvider.ApiDescriptionGroups);
}
else
{
throw new InvalidOperationException($"Unsupported settings type '{documentInfo.GetType().FullName}");
}

documentInfo.PostProcess?.Invoke(document);

return document;
return _lastDocument;
}

// Called by the Microsoft.Extensions.ApiDescription tool
Expand All @@ -100,10 +98,33 @@ async Task IDocumentProvider.GenerateAsync(string documentName, TextWriter write
throw new ArgumentNullException(nameof(writer));
}

var document = await GenerateAsync(documentName);
var document = await GenerateAsyncCore(documentName);

var json = document.ToJson();
await writer.WriteAsync(json);
}

private async Task<SwaggerDocument> GenerateAsyncCore(string documentName)
{
var documentSettings = _registry[documentName];
if (documentSettings == null)
{
throw new InvalidOperationException(
$"No registered document found for document name '{documentName}'.");
}

documentSettings.GeneratorSettings.ApplySettings(
_mvcJsonOptions.Value.SerializerSettings,
_mvcOptions.Value);

var generator = new AspNetCoreToSwaggerGenerator(
documentSettings.GeneratorSettings,
documentSettings.SchemaGenerator);
var document = await generator.GenerateAsync(_apiDescriptionGroupCollectionProvider.ApiDescriptionGroups);

documentSettings.PostProcess?.Invoke(document);

return document;
}
}
}
Loading

0 comments on commit f388a2e

Please sign in to comment.