How to define links for OAS3 using Swashbuckle? - asp.net-core

I have noticed that in the Swagger UI v3 and in OAS3 we now have support for something called "links"
But I cant really figure out if its possible to use this feature with Swashbuckle, and if it is.. then how? Been searching the net and haven't found anything regarding this..
Anyone been able to use links with Swashbuckle?

You can use an OperationFilter. Create a class that implements IOperationFilter
public class MyLinkFilter : IOperationFilter
{
into it select the response for which you want to add Links.
public void Apply(OpenApiOperation operation, OperationFilterContext context)
var responses = operation.Responses;
var response = responses.FirstOrDefault(r => r.Key == "200").Value;
then update the Links property
response.Links = new Dictionary<string, OpenApiLink>
{
{
"YourKey"
,new OpenApiLink {
OperationId = "YourOperationId",
Description = ".............",
Parameters = new Dictionary<string, RuntimeExpressionAnyWrapper>
{
{
"yourParam", new RuntimeExpressionAnyWrapper
{
Any = new OpenApiString("$request.path.number")
}
}
}
}
}
};
Register your OperationFilter into startup.cs
services.AddSwaggerGen(options =>
{
options.OperationFilter<MyLinkFilter>();
});
OpenAPI, Response
OpenAPI, Link
Finally, you'll have to implement a mechanism to apply the links to the good Action in your controller.

Related

Custom Result in Net 6 Minimal API

In ASP.NET Core 5 I had a custom Action Result as follows:
public class ErrorResult : ActionResult {
private readonly IList<Error> _errors;
public ErrorResult(IList<Error> errors) {
_errors = errors;
}
public override async Task ExecuteResultAsync(ActionContext context) {
// Code that creates Response
await result.ExecuteResultAsync(context);
}
}
Then on a Controller action I would have:
return new ErrorResult(errors);
How to do something similar in NET 6 Minimal APIs?
I have been looking at it and I think I should implement IResult.
But I am not sure if that is the solution or how to do it.
I have recently been playing around with minimal APIs and and working on global exception handling. Here is what I have come up with so far.
Create a class implementation of IResult
Create a constructor which will take an argument of the details you want going into your IResult response. APIErrorDetails is a custom implementation of mine similar to what you'd see in ProblemDetails in MVC. Method implementation is open to whatever your requirements are.
public class ExceptionAllResult : IResult
{
private readonly ApiErrorDetails _details;
public ExceptionAllResult(ApiErrorDetails details)
{
_details = details;
}
public async Task ExecuteAsync(HttpContext httpContext)
{
var jsonDetails = JsonSerializer.Serialize(_details);
httpContext.Response.ContentType = MediaTypeNames.Application.Json;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(jsonDetails);
httpContext.Response.StatusCode = _details.StatusCode;
await httpContext.Response.WriteAsync(jsonDetails);
}
}
Return result in your exception handling middleware in your Program.cs file.
app.UseExceptionHandler(
x =>
{
x.Run(
async context =>
{
// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-6.0
var exceptionFeature = context.Features.Get<IExceptionHandlerPathFeature>();
// Whatever you want for null handling
if (exceptionFeature is null) throw new Exception();
// My result service for creating my API details from the HTTP context and exception. This returns the Result class seen in the code snippet above
var result = resultService.GetErrorResponse(exceptionFeature.Error, context);
await result.ExecuteAsync(context); // returns the custom result
});
}
);
If you still want to use MVC (Model-View-Controller), you still can use Custom ActionResult.
If you just want to use Minimal APIs to do the response, then you have to implement IResult, Task<IResult> or ValueTask<IResult>.
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
The following example uses the built-in result types to customize the response:
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound())
.Produces<Todo>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
You can find more IResult implementation samples here: https://github.com/dotnet/aspnetcore/tree/main/src/Http/Http.Results/src
Link: Minimal APIs overview | Microsoft Docs

RequireAuthorization and Swashbuckle IOperationFilter

I am looking for a way to determine if endpoint requires authorization (.Net Core 3.1) using IOperationFilter.
If Authorization is setup via filter or explicitly as attribute, it can be found in OperationFilterContext context.ApiDescription.ActionDescriptor.FilterDescriptors.Select(filterInfo => filterInfo.Filter).Any(filter => filter is AuthorizeFilter) and context.ApiDescription.CustomAttributes().OfType<AuthorizeAttribute>().
But if authorization is set as
endpoints.MapControllers().RequireAuthorization();, which should add AuthorizationAttribute to all endpoints, it is not appeared neither in filters nor in attributes. Any thoughts on how to catch if auth is applied to endpoints in this case?
I was able to beat this today like so (swashbuckle 5.63):
Make a new class like this
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Authorization;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace YourNameSpace
{
public class SwaggerGlobalAuthFilter : IOperationFilter
{
public void Apply( OpenApiOperation operation, OperationFilterContext context )
{
context.ApiDescription.TryGetMethodInfo( out MethodInfo methodInfo );
if ( methodInfo == null )
{
return;
}
var hasAllowAnonymousAttribute = false;
if ( methodInfo.MemberType == MemberTypes.Method )
{
// NOTE: Check the controller or the method itself has AllowAnonymousAttribute attribute
hasAllowAnonymousAttribute =
methodInfo.DeclaringType.GetCustomAttributes( true ).OfType<AllowAnonymousAttribute>().Any() ||
methodInfo.GetCustomAttributes( true ).OfType<AllowAnonymousAttribute>().Any();
}
if ( hasAllowAnonymousAttribute )
{
return;
}
// NOTE: This adds the "Padlock" icon to the endpoint in swagger,
// we can also pass through the names of the policies in the List<string>()
// which will indicate which permission you require.
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "oauth2" // note this 'Id' matches the name 'oauth2' defined in the swagger extensions config section below
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
}
};
}
}
}
In swagger config extensions
options.AddSecurityDefinition( "oauth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
Implicit = new OpenApiOAuthFlow
{
//_swaggerSettings is a custom settings object of our own
AuthorizationUrl = new Uri( _swaggerSettings.AuthorizationUrl ),
Scopes = _swaggerSettings.Scopes
}
}
} );
options.OperationFilter<SwaggerGlobalAuthFilter>();
Put together from docs, other SO and decompiled code of built-in SecurityRequirementsOperationFilter
AFAIK, it is defining a global auth setup for all your routed endpoints except those that explicitly have AllowAnonymousAttribute on controller or endpoint. since, as your original question hints at, using the extension RequireAuthorization() when setting up routing implicitly puts that attribute on all endpoints and the built-in SecurityRequirementsOperationFilter which detect the Authorize attribute fails to pick it up. Since your routing setup effectively is putting Authorize on every controller/route it seems setting up a default global filter like this that excludes AllowAnonymous would be in line with what you are configuring in the pipeline.
I suspect there may be a more 'built-in' way of doing this, but I could not find it.
Apparently, this is an open issue on the NSwag repo as well (for people like me that drive by with the same issue, but with NSwag instead of Swashbuckle):
https://github.com/RicoSuter/NSwag/issues/2817
Where there's also another example of solving the issue (not only securityrequirement, but also its scopes).
I know it's been a long time since this question was asked.
But I was facing a similar issue, and following the advice from an issue in GitHub here, managed to resolve it using this implementation of IOperationFilter (and now works like a charm):
public class AuthorizeCheckOperationFilter : IOperationFilter
{
private readonly EndpointDataSource _endpointDataSource;
public AuthorizeCheckOperationFilter(EndpointDataSource endpointDataSource)
{
_endpointDataSource = endpointDataSource;
}
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var descriptor = _endpointDataSource.Endpoints.FirstOrDefault(x =>
x.Metadata.GetMetadata<ControllerActionDescriptor>() == context.ApiDescription.ActionDescriptor);
var hasAuthorize = descriptor.Metadata.GetMetadata<AuthorizeAttribute>()!=null;
var allowAnon = descriptor.Metadata.GetMetadata<AllowAnonymousAttribute>() != null;
if (!hasAuthorize || allowAnon) return;
operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
operation.Security = new List<OpenApiSecurityRequirement>
{
new()
{
[
new OpenApiSecurityScheme {Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "oauth2"}
}
] = new[] {"api1"}
}
};
}
}
The issue stated this:
ControllerActionDescriptor.EndpointMetadata only reflects the metadata
discovered on the controller action. Any metadata configured via the
endpoint APIs do not show up here. It was primarily the reason we
documented it as being infrastructure-only since it's a bit confusing
to use.
There's a couple of options you could use
a) You could decorate your controllers using [Authorize]. That should allow the metadata to show up in the property.
b) You could look up the metadata by reading from EndpointDataSource.

How do I use IViewLocationExtender with Razor Pages to render device specific pages

Currently we are building a web application, desktop first, that needs device specific Razor Pages for specific pages. Those pages are really different from their Desktop version and it makes no sense to use responsiveness here.
We have tried to implement our own IViewLocationExpander and also tried to use the MvcDeviceDetector library (which is basically doing the same). Detection of the device type is no problem but for some reason the device specific page is not picked up and it is constantly falling back to the default Index.cshtml.
(edit: We're thinking about implementing something based on IPageConvention, IPageApplicationModelProvider or something ... ;-))
Index.mobile.cshtml
Index.cshtml
We have added the following code using the example of MvcDeviceDetector:
public static IMvcBuilder AddDeviceDetection(this IMvcBuilder builder)
{
builder.Services.AddDeviceSwitcher<UrlSwitcher>(
o => { },
d => {
d.Format = DeviceLocationExpanderFormat.Suffix;
d.MobileCode = "mobile";
d.TabletCode = "tablet";
}
);
return builder;
}
and are adding some route mapping
routes.MapDeviceSwitcher();
We expected to see Index.mobile.cshtml to be picked up when selecting a Phone Emulation in Chrome but that didnt happen.
edit Note:
we're using a combination of Razor Views/MVC (older sections) and Razor Pages (newer sections).
also not every page will have a mobile implementation. That's what would have a IViewLocationExpander solution so great.
edit 2
I think the solution would be the same as how you'd implement Culture specific Razor Pages (which is also unknown to us ;-)). Basic MVC supports Index.en-US.cshtml
Final Solution Below
If this is a Razor Pages application (as opposed to an MVC application) I don't think that the IViewLocationExpander interface is much use to you. As far as I know, it only works for partials, not routeable pages (i.e. those with an #page directive).
What you can do instead is to use Middleware to determine whether the request comes from a mobile device, and then change the file to be executed to one that ends with .mobile. Here's a very rough and ready implementation:
public class MobileDetectionMiddleware
{
private readonly RequestDelegate _next;
public async Task Invoke(HttpContext context)
{
if(context.Request.IsFromAMobileDevice())
{
context.Request.Path = $"{context.Request.Path}.mobile";
}
await _next.Invoke(context);
}
}
It's up to you how you want to implement the IsFromAMobileDevice method to determine the nature of the user agent. There's nothing stopping you using a third party library that can do the check reliably for you. Also, you will probably only want to change the path under certain conditions - such as where there is a device specific version of the requested page.
Register this in your Configure method early:
app.UseMiddleware<MobileDetectionMiddleware>();
I've finally found the way to do it convention based. I have implemented a IViewLocationExpander in order to tackle the device handling for basic Razor Views (including Layouts) and I've implemented IPageRouteModelConvention + IActionConstraint to handle devices for Razor Pages.
Note: this solution only seems to be working on ASP.NET Core 2.2 and up though. For some reason 2.1.x and below is clearing the constraints (tested with a breakpoint in a destructor) after they've been added (can probably be fixed).
Now I can have /Index.mobile.cshtml /Index.desktop.cshtml etc. in both MVC and Razor Pages.
Note: This solution can also be used to implement a language/culture specific Razor Pages (eg. /Index.en-US.cshtml /Index.nl-NL.cshtml)
public class PageDeviceConvention : IPageRouteModelConvention
{
private readonly IDeviceResolver _deviceResolver;
public PageDeviceConvention(IDeviceResolver deviceResolver)
{
_deviceResolver = deviceResolver;
}
public void Apply(PageRouteModel model)
{
var path = model.ViewEnginePath; // contains /Index.mobile
var lastSeparator = path.LastIndexOf('/');
var lastDot = path.LastIndexOf('.', path.Length - 1, path.Length - lastSeparator);
if (lastDot != -1)
{
var name = path.Substring(lastDot + 1);
if (Enum.TryParse<DeviceType>(name, true, out var deviceType))
{
var constraint = new DeviceConstraint(deviceType, _deviceResolver);
for (var i = model.Selectors.Count - 1; i >= 0; --i)
{
var selector = model.Selectors[i];
selector.ActionConstraints.Add(constraint);
var template = selector.AttributeRouteModel.Template;
var tplLastSeparator = template.LastIndexOf('/');
var tplLastDot = template.LastIndexOf('.', template.Length - 1, template.Length - Math.Max(tplLastSeparator, 0));
template = template.Substring(0, tplLastDot); // eg Index.mobile -> Index
selector.AttributeRouteModel.Template = template;
var fileName = template.Substring(tplLastSeparator + 1);
if ("Index".Equals(fileName, StringComparison.OrdinalIgnoreCase))
{
selector.AttributeRouteModel.SuppressLinkGeneration = true;
template = selector.AttributeRouteModel.Template.Substring(0, Math.Max(tplLastSeparator, 0));
model.Selectors.Add(new SelectorModel(selector) { AttributeRouteModel = { Template = template } });
}
}
}
}
}
protected class DeviceConstraint : IActionConstraint
{
private readonly DeviceType _deviceType;
private readonly IDeviceResolver _deviceResolver;
public DeviceConstraint(DeviceType deviceType, IDeviceResolver deviceResolver)
{
_deviceType = deviceType;
_deviceResolver = deviceResolver;
}
public int Order => 0;
public bool Accept(ActionConstraintContext context)
{
return _deviceResolver.GetDeviceType() == _deviceType;
}
}
}
public class DeviceViewLocationExpander : IViewLocationExpander
{
private readonly IDeviceResolver _deviceResolver;
private const string ValueKey = "DeviceType";
public DeviceViewLocationExpander(IDeviceResolver deviceResolver)
{
_deviceResolver = deviceResolver;
}
public void PopulateValues(ViewLocationExpanderContext context)
{
var deviceType = _deviceResolver.GetDeviceType();
if (deviceType != DeviceType.Other)
context.Values[ValueKey] = deviceType.ToString();
}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
var deviceType = context.Values[ValueKey];
if (!string.IsNullOrEmpty(deviceType))
{
return ExpandHierarchy();
}
return viewLocations;
IEnumerable<string> ExpandHierarchy()
{
var replacement = $"{{0}}.{deviceType}";
foreach (var location in viewLocations)
{
if (location.Contains("{0}"))
yield return location.Replace("{0}", replacement);
yield return location;
}
}
}
}
public interface IDeviceResolver
{
DeviceType GetDeviceType();
}
public class DefaultDeviceResolver : IDeviceResolver
{
public DeviceType GetDeviceType() => DeviceType.Mobile;
}
public enum DeviceType
{
Other,
Mobile,
Tablet,
Normal
}
Startup
services.AddMvc(o => { })
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddRazorOptions(o =>
{
o.ViewLocationExpanders.Add(new DeviceViewLocationExpander(new DefaultDeviceResolver()));
})
.AddRazorPagesOptions(o =>
{
o.Conventions.Add(new PageDeviceConvention(new DefaultDeviceResolver()));
});

Naming conventions for view pages and setting controller action for view

I am unsure on how I should be naming my View pages, they are all CamelCase.cshtml, that when viewed in the browser look like "http://www.website.com/Home/CamelCase".
When I am building outside of .NET my pages are named like "this-is-not-camel-case.html". How would I go about doing this in my MVC4 project?
If I did go with this then how would I tell the view to look at the relevant controller?
Views/Home/camel-case.cshtml
Fake edit: Sorry if this has been asked before, I can't find anything via search or Google. Thanks.
There are a few ways you can do this:
Name all of your views in the style you would like them to show up in the url
This is pretty simple, you just add the ActionName attribute to all of your actions and specify them in the style you would like your url to look like, then rename your CamelCase.cshtml files to camel-case.cshtml files.
Use attribute routing
Along the same lines as above, there is a plugin on nuget to enable attribute routing which lets you specify the full url for each action as an attribute on the action. It has convention attributes to help you out with controller names and such as well. I generally prefer this approach because I like to be very explicit with the routes in my application.
A more framework-y approach
It's probably possible to do something convention based by extending the MVC framework, but it would be a decent amount of work. In order to select the correct action on a controller, you'd need to map the action name on its way in to MVC to its CamelCase equivalent before the framework uses it to locate the action on the controller. The easiest place to do this is in the Route, which is the last thing to happen before the MVC framework takes over the request. You'll also need to convert the other way on the way out so the urls generated look like you want them to.
Since you don't really want to alter the existing method to register routes, it's probably best write a function in application init that loops over all routes after they have been registered and wraps them with your new functionality.
Here is an example route and modifications to application start that achieve what you are trying to do. I'd still go with the route attribute approach however.
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
WrapRoutesWithNamingConvention(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
AuthConfig.RegisterAuth();
}
private void WrapRoutesWithNamingConvention(RouteCollection routes)
{
var wrappedRoutes = routes.Select(m => new ConventionRoute(m)).ToList();
routes.Clear();
wrappedRoutes.ForEach(routes.Add);
}
private class ConventionRoute : Route
{
private readonly RouteBase baseRoute;
public ConventionRoute(RouteBase baseRoute)
: base(null, null)
{
this.baseRoute = baseRoute;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var baseRouteData = baseRoute.GetRouteData(httpContext);
if (baseRouteData == null) return null;
var actionName = baseRouteData.Values["action"] as string;
var convertedActionName = ConvertHyphensToPascalCase(actionName);
baseRouteData.Values["action"] = convertedActionName;
return baseRouteData;
}
private string ConvertHyphensToPascalCase(string hyphens)
{
var capitalParts = hyphens.Split('-').Select(m => m.Substring(0, 1).ToUpper() + m.Substring(1));
var pascalCase = String.Join("", capitalParts);
return pascalCase;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
var valuesClone = new RouteValueDictionary(values);
var pascalAction = valuesClone["action"] as string;
var hyphens = ConvertPascalCaseToHyphens(pascalAction);
valuesClone["action"] = hyphens;
var baseRouteVirtualPath = baseRoute.GetVirtualPath(requestContext, valuesClone);
return baseRouteVirtualPath;
}
private string ConvertPascalCaseToHyphens(string pascal)
{
var pascalParts = new List<string>();
var currentPart = new StringBuilder();
foreach (var character in pascal)
{
if (char.IsUpper(character) && currentPart.Length > 0)
{
pascalParts.Add(currentPart.ToString());
currentPart.Clear();
}
currentPart.Append(character);
}
if (currentPart.Length > 0)
{
pascalParts.Add(currentPart.ToString());
}
var lowers = pascalParts.Select(m => m.ToLower());
var hyphens = String.Join("-", lowers);
return hyphens;
}
}
}

Custom OpenIdClient for Customer URL in MVC 4

I'm working with the default template for MVC 4 and trying to add my own openID provider for example http://steamcommunity.com/dev to the list of openID logins and an openID box where the user can type in their openID information.
To add Google I just un-comment
OAuthWebSecurity.RegisterGoogleClient();
as for other custom solutions you can do something like
OAuthWebSecurity.RegisterClient(new SteamClient(),"Steam",null);
The trouble I have is creating SteamClient (or a generic one) http://blogs.msdn.com/b/webdev/archive/2012/08/23/plugging-custom-oauth-openid-providers.aspx doesn't show anywhere to change the URL.
I think the reason I could not find the answer is that most people thought it was common sense. I prefer my sense to be uncommon.
public class OidCustomClient : OpenIdClient
{
public OidCustomClient() : base("Oid", "http://localhost:5004/") { }
}
Based on #Jeff's answer I created a class to handle Stack Exchange OpenID.
Register:
OAuthWebSecurity.RegisterClient(new StackExchangeOpenID());
Class:
public class StackExchangeOpenID : OpenIdClient
{
public StackExchangeOpenID()
: base("stackexchange", "https://openid.stackexchange.com")
{
}
protected override Dictionary<string, string> GetExtraData(IAuthenticationResponse response)
{
FetchResponse fetchResponse = response.GetExtension<FetchResponse>();
if (fetchResponse != null)
{
var extraData = new Dictionary<string, string>();
extraData.Add("email", fetchResponse.GetAttributeValue(WellKnownAttributes.Contact.Email));
extraData.Add("name", fetchResponse.GetAttributeValue(WellKnownAttributes.Name.FullName));
return extraData;
}
return null;
}
protected override void OnBeforeSendingAuthenticationRequest(IAuthenticationRequest request)
{
var fetchRequest = new FetchRequest();
fetchRequest.Attributes.AddRequired(WellKnownAttributes.Contact.Email);
fetchRequest.Attributes.AddRequired(WellKnownAttributes.Name.FullName);
request.AddExtension(fetchRequest);
}
}
Retrieving extra data:
var result = OAuthWebSecurity.VerifyAuthentication();
result.ExtraData["email"];
result.ExtraData["name"];