Skip to content

Commit

Permalink
1) Add feature: remove Paths and Definitions from OpenApi documentati…
Browse files Browse the repository at this point in the history
…on for specific controller action without accepted roles;

2) Add code refactoring.
  • Loading branch information
Chebotov Nikolay committed Feb 28, 2020
1 parent 6a4fa35 commit 7d89e80
Show file tree
Hide file tree
Showing 16 changed files with 451 additions and 58 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

These are the changes to each version that has been released on the [nuget](https://www.nuget.org/packages/Unchase.Swashbuckle.AspNetCore.Extensions/).

## v2.2.0 `(2020-02-28)`

- [x] Add feature: remove Paths and Definitions from OpenApi documentation for specific controller action without accepted roles

## v2.1.6 `(2020-02-21)`

- [x] Fix bug: replace `<br>` tag to `{Envirinment.NewLine}{Envirinment.NewLine}` for enum descriptions
Expand Down
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public void ConfigureServices(IServiceCollection services)
}
```

2. **Hide Paths and Defenitions from OpenApi documentation** without accepted roles in OpenApi document:
2. **Hide Paths and Definitions from OpenApi documentation** without accepted roles:
- In the _ConfigureServices_ method of _Startup.cs_, inside your `AddSwaggerGen` call, enable `HidePathsAndDefinitionsByRolesDocumentFilter` document filter:

```csharp
Expand All @@ -127,6 +127,28 @@ public void ConfigureServices(IServiceCollection services)
}
```

- Since [v2.2.0](https://github.com/unchase/Unchase.Swashbuckle.AspNetCore.Extensions/releases/tag/v2.2.0) you can hide Paths and Definitions from OpenApi documentation for specific controller action without accepted roles like this:

```csharp
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
//...
// enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger(c =>
{
c.PreSerializeFilters.Add((openApiDoc, httpRequest) =>
{
// remove Paths and Components from OpenApi documentation for specific controller action without accepted roles
openApiDoc.RemovePathsAndComponentsWithoutAcceptedRolesFor<SomeController>(controller => nameof(controller.SomeAction), new List<string> {"AcceptedRole"});
});
});

//...
}
```

3. **Append action count into the SwaggetTag's descriptions in OpenApi document**:
- In the _ConfigureServices_ method of _Startup.cs_, inside your `AddSwaggerGen` call, enable `AppendActionCountToTagSummaryDocumentFilter` document filter:

Expand Down
2 changes: 1 addition & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: 2.1.{build}
version: 2.2.{build}
pull_requests:
do_not_increment_build_number: true
skip_tags: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ internal static string AddEnumValuesDescription(this OpenApiSchema schema, bool
if (!schema.Extensions.ContainsKey("x-enumDescriptions"))
continue;

var xenumDescriptions = (OpenApiArray)schema.Extensions["x-enumDescriptions"];
if (xenumDescriptions?.Count == schema.Enum.Count)
var xEnumDescriptions = (OpenApiArray)schema.Extensions["x-enumDescriptions"];
if (xEnumDescriptions?.Count == schema.Enum.Count)
{
var description = ((OpenApiString)((OpenApiArray)schema.Extensions["x-enumDescriptions"])[i]).Value;
sb.Append($" ({description})");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Models;
using Unchase.Swashbuckle.AspNetCore.Extensions.Factories;
using Unchase.Swashbuckle.AspNetCore.Extensions.Filters;

namespace Unchase.Swashbuckle.AspNetCore.Extensions.Extensions
{
/// <summary>
/// Extension methods for <see cref=" OpenApiDocument"/>.
/// </summary>
public static class OpenApiDocumentExtensions
{
/// <summary>
/// Remove Paths and Components from OpenApi documentation without accepted roles.
/// </summary>
/// <param name="openApiDoc"><see cref="OpenApiDocument"/>.</param>
/// <param name="actionNameSelector">Action name selector.</param>
/// <param name="acceptedRoles">Collection of accepted roles.</param>
/// <returns>
/// Returns <see cref="OpenApiDocument"/>.
/// </returns>
public static OpenApiDocument RemovePathsAndComponentsWithoutAcceptedRolesFor<TController>(this OpenApiDocument openApiDoc, Func<TController, string> actionNameSelector,
IReadOnlyList<string> acceptedRoles) where TController : class, new()
{
var actionDescriptor = ApiDescriptionFactory.Create(actionNameSelector, typeof(TController).GetCustomAttribute<RouteAttribute>().Template)?.ActionDescriptor;
if (actionDescriptor != null)
{
var paths = new Dictionary<MethodInfo, string>
{
{ ((Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor)actionDescriptor).MethodInfo, actionDescriptor.AttributeRouteInfo.Template }
};

HidePathsAndDefinitionsByRolesDocumentFilter.RemovePathsAndComponents(openApiDoc, paths, openApiDoc.Components.Schemas, acceptedRoles);
}

return openApiDoc;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

namespace Unchase.Swashbuckle.AspNetCore.Extensions.Extensions
{
/// <summary>
/// Extension methods for <see cref="SwaggerGenOptions"/>.
/// </summary>
public static class SwaggerGenOptionsExtensions
{
#region Extensions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Routing;

namespace Unchase.Swashbuckle.AspNetCore.Extensions.Factories
{
/// <summary>
/// <see cref="ApiDescription"/> factory.
/// </summary>
internal static class ApiDescriptionFactory
{
/// <summary>
/// Create <see cref="ApiDescription"/>.
/// </summary>
/// <param name="controllerType">Type of Controller.</param>
/// <param name="actionName">Controller action name.</param>
/// <param name="groupName">Group name.</param>
/// <param name="httpMethod">Http method.</param>
/// <param name="relativePath">Relative path.</param>
/// <param name="parameterDescriptions">Collection of <see cref="ApiParameterDescription"/>.</param>
/// <param name="supportedRequestFormats">Collection of <see cref="ApiRequestFormat"/>.</param>
/// <param name="supportedResponseTypes">Collection of <see cref="ApiResponseType"/>.</param>
/// <returns>
/// Returns <see cref="ApiDescription"/>.
/// </returns>
internal static ApiDescription Create(
Type controllerType,
string actionName,
string groupName = null,
string httpMethod = null,
string relativePath = null,
IEnumerable<ApiParameterDescription> parameterDescriptions = null,
IEnumerable<ApiRequestFormat> supportedRequestFormats = null,
IEnumerable<ApiResponseType> supportedResponseTypes = null)
{
var methodInfo = controllerType.GetMethod(actionName);

if (methodInfo == null)
return null;

var actionDescriptor = CreateActionDescriptor(methodInfo);

var routAttr = controllerType.GetCustomAttributes().OfType<RouteAttribute>().LastOrDefault();

if (string.IsNullOrWhiteSpace(actionDescriptor.AttributeRouteInfo.Template))
return null;
//throw new InvalidOperationException($"HttpMethod attribute of \"{methodInfo.Name}\" action in \"{controllerType.Name}\" controller must have a template specified.");

if (routAttr != null && !string.IsNullOrWhiteSpace(routAttr.Template))
{
var template = routAttr.Template;
actionDescriptor.AttributeRouteInfo.Template = template + "/" + actionDescriptor.AttributeRouteInfo.Template;
}

foreach (var routeValue in actionDescriptor.RouteValues)
{
actionDescriptor.AttributeRouteInfo.Template =
actionDescriptor.AttributeRouteInfo.Template.Replace($"[{routeValue.Key}]", routeValue.Value);
}

httpMethod = httpMethod ?? methodInfo?.GetCustomAttributes().OfType<HttpMethodAttribute>().FirstOrDefault()?.HttpMethods?.FirstOrDefault();

var apiDescription = new ApiDescription
{
ActionDescriptor = actionDescriptor,
GroupName = groupName,
HttpMethod = httpMethod,
RelativePath = relativePath,
};

if (parameterDescriptions != null)
{
foreach (var parameter in parameterDescriptions)
{
// If the provided action has a matching parameter - use it to assign ParameterDescriptor & ModelMetadata
var controllerParameterDescriptor = actionDescriptor.Parameters
.OfType<ControllerParameterDescriptor>()
.FirstOrDefault(parameterDescriptor => parameterDescriptor.Name == parameter.Name);

if (controllerParameterDescriptor != null)
{
parameter.ParameterDescriptor = controllerParameterDescriptor;
parameter.ModelMetadata = ModelMetadataFactory.CreateForParameter(controllerParameterDescriptor.ParameterInfo);
}

apiDescription.ParameterDescriptions.Add(parameter);
}
}

if (supportedRequestFormats != null)
{
foreach (var requestFormat in supportedRequestFormats)
{
apiDescription.SupportedRequestFormats.Add(requestFormat);
}
}

if (supportedResponseTypes != null)
{
foreach (var responseType in supportedResponseTypes)
{
// If the provided action has a return value AND the response status is 2XX - use it to assign ModelMetadata
if (methodInfo?.ReturnType != null && responseType.StatusCode / 100 == 2)
{
responseType.ModelMetadata = ModelMetadataFactory.CreateForType(methodInfo.ReturnType);
}

apiDescription.SupportedResponseTypes.Add(responseType);
}
}

return apiDescription;
}

/// <summary>
/// Create <see cref="ApiDescription"/>.
/// </summary>
/// <typeparam name="TController">Type of Controller.</typeparam>
/// <param name="actionNameSelector">Action name selector.</param>
/// <param name="groupName">Group name.</param>
/// <param name="httpMethod">Http method.</param>
/// <param name="relativePath">Relative path.</param>
/// <param name="parameterDescriptions">Collection of <see cref="ApiParameterDescription"/>.</param>
/// <param name="supportedRequestFormats">Collection of <see cref="ApiRequestFormat"/>.</param>
/// <param name="supportedResponseTypes">Collection of <see cref="ApiResponseType"/>.</param>
/// <returns>
/// Returns <see cref="ApiDescription"/>.
/// </returns>
internal static ApiDescription Create<TController>(
Func<TController, string> actionNameSelector,
string groupName = null,
string httpMethod = null,
string relativePath = null,
IEnumerable<ApiParameterDescription> parameterDescriptions = null,
IEnumerable<ApiRequestFormat> supportedRequestFormats = null,
IEnumerable<ApiResponseType> supportedResponseTypes = null)
where TController : new()
{
return Create(
typeof(TController),
actionNameSelector(new TController()),
groupName,
httpMethod,
relativePath,
parameterDescriptions,
supportedRequestFormats,
supportedResponseTypes
);
}

private static ActionDescriptor CreateActionDescriptor(MethodInfo methodInfo)
{
var httpMethodAttribute = methodInfo.GetCustomAttribute<HttpMethodAttribute>();
var attributeRouteInfo = (httpMethodAttribute != null)
? new AttributeRouteInfo { Template = httpMethodAttribute.Template, Name = httpMethodAttribute.Name }
: null;

var parameterDescriptors = methodInfo.GetParameters()
.Select(CreateParameterDescriptor)
.ToList();

var routeValues = new Dictionary<string, string>
{
["controller"] = methodInfo.DeclaringType?.Name.Replace("Controller", string.Empty)
};

return new ControllerActionDescriptor
{
AttributeRouteInfo = attributeRouteInfo,
ControllerTypeInfo = methodInfo.DeclaringType.GetTypeInfo(),
MethodInfo = methodInfo,
Parameters = parameterDescriptors,
RouteValues = routeValues
};
}

private static ParameterDescriptor CreateParameterDescriptor(ParameterInfo parameterInfo)
{
return new ControllerParameterDescriptor
{
Name = parameterInfo.Name,
ParameterInfo = parameterInfo,
ParameterType = parameterInfo.ParameterType,
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace Unchase.Swashbuckle.AspNetCore.Extensions.Factories
{
public static class ModelMetadataFactory
{
public static ModelMetadata CreateForType(Type type)
{
return new EmptyModelMetadataProvider().GetMetadataForType(type);
}

public static ModelMetadata CreateForProperty(Type containingType, string propertyName)
{
return new EmptyModelMetadataProvider().GetMetadataForProperty(containingType, propertyName);
}

public static ModelMetadata CreateForParameter(ParameterInfo parameter)
{
return new EmptyModelMetadataProvider().GetMetadataForParameter(parameter);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ internal class ChangeResponseByHttpStatusCodeDocumentFilter<T> : IDocumentFilter
{
#region Fileds

private int _httpStatusCode;
private readonly int _httpStatusCode;

private string _responseDescription;
private readonly string _responseDescription;

private ResponseExampleOptions _responseExampleOption;
private readonly ResponseExampleOptions _responseExampleOption;

private T _responseExample;
private readonly T _responseExample;

#endregion

Expand Down
Loading

0 comments on commit 7d89e80

Please sign in to comment.