If I have an action defined as (note - all names are hypothetical)
[HttpGet("api/[controller]/[action]")]
public string LogMeIn(...)
Swagger will generate URL as .../api/Auth/LogMeIn
I've added option
services.AddRouting(opt => opt.LowercaseUrls = true)
This got me to this point in the swagger .../api/auth/logmein
But I need to be at camelCase --> .../api/auth/logMeIn
I've tried to look through swagger options, app options, but no luck. What can I do here? BTW, searched the internet. Mostly they talk about parameters/models. This is swagger UI URL. Thanks
i don't know if something inbuilt is available or not but you can write a simple document filter like this.
public class UrlRenameDocumentFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
OpenApiPaths keyValuePairs = new OpenApiPaths();
foreach(var path in swaggerDoc.Paths)
{
var value = path.Value;
// here you have to put logic to convert name to camelCase
string newkey = ConvertToCamelCase(path.Key);
keyValuePairs.Add(newkey, value);
}
swaggerDoc.Paths = keyValuePairs;
}
}
in program.cs
builder.Services.AddSwaggerGen(o =>
{
o.DocumentFilter<UrlRenameDocumentFilter>();
});
Related
I currently have this web app in which I need to use a custom authentication/authorization middleware. We're registering the routes using minimal APIs like so:
// snip
app.UseEndpoints(x => {
x.MapGet("/api/something", () => { ... returns something ... });
})
// snip
Our custom middleware requires us to configure a list of all the paths that need to be authorized to certain types of users and those that allow for anonymous access. But what I would really like to do is define extension methods that would work similarly to how AllowAnonymous() and RequiresAuthorization(...) work:
// snip
app.UseEndpoints(x => {
x.MapGet("/api/something", () => { ... returns something ... }).RequiresAuthorization("Admins");
})
// snip
This way I can avoid having to remember to update the configuration values in the middleware, while also having the auth information in the same place as the route definition. So far I can't seem to figure out what to set up in the extension method to be able to then inspect the context of each request inside the custom middleware to know if the route should have auth or not. I'm guessing I have to set some sort of metadata on the route, but I'm not sure what or where to set it up. I tried checking the source code for asp net core, but it's so meta and indirect that you really never know what anything actually does in that code. Any guidance or pointers would be very much appreciated :D
You can just add any metadata and analyze it in your custom middleware:
public class SomeCustomMeta
{
public string Meta { get; set; }
}
app.MapGet("/test", () => Results.Ok("Hello World!"))
.WithMetadata(new SomeCustomMeta{ Meta = "Test"});
And in the middleware:
public async Task InvokeAsync(HttpContext context)
{
if(context.GetEndpoint()?.Metadata.GetMetadata<SomeCustomMeta>() is { } meta)
{
Console.WriteLine($"SomeCustomMeta: {meta.Meta}");
}
await _next(context);
}
The WithMetadata... call can be moved to nice extension method:
public static class BuilderExts
{
public static TBuilder RequireCustomAuth<TBuilder>(this TBuilder builder, string meta) where TBuilder : IEndpointConventionBuilder
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
return builder.WithMetadata(new SomeCustomMeta { Meta = meta });
}
}
What I have done is created a small API in a class library. This API would be used by other sites. Think of it as a standard endpoint that all of our websites will contain.
[Route("api/[controller]")]
[ApiController]
public class CustomController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
}
The above is in a class library. Now what i would like to do is be able to add this to the projects in a simple manner.
app.UseCustomAPI("/api/crap");
I am not exactly sure how i should handle routing to the api controllers in the library. I created a CustomAPIMiddleware which is able to catch that i called "/api/crap" however i am not sure how i should forward the request over to CustomController in the library
public async Task Invoke(HttpContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
PathString matched;
PathString remaining;
if (context.Request.Path.StartsWithSegments(_options.PathMatch, out matched, out remaining))
{
PathString path = context.Request.Path;
PathString pathBase = context.Request.PathBase;
context.Request.PathBase = pathBase.Add(matched);
context.Request.Path = remaining;
try
{
await this._options.Branch(context);
}
finally
{
context.Request.PathBase = pathBase;
context.Request.Path = path;
}
path = new PathString();
pathBase = new PathString();
}
else
await this._next(context);
}
After having done that i am starting to think i may have approached this in the wrong manner and should actually be trying to add it directly to the routing tables somehow. That being said i would like it if they could customize the endpoint that the custom controller reads from.
Update
The following does work. Loading and registering API Controllers From Class Library in ASP.NET core
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddApplicationPart(Assembly.Load(new AssemblyName("WebAPI")));
However i am really looking for a middlewere type solution so that users can simply add it and i can configure the default settings or they can change some of the settings. The above example would not allow for altering the settings.
app.UseCustomAPI("/api/crap");
Update from comment without Assembly
If i dont add the .AddApplicationPart(Assembly.Load(new AssemblyName("WebAPI")));
This localhost page can’t be found No webpage was found for the web address:
https://localhost:44368/api/Custom
To customise the routing for a controller at runtime, you can use an Application Model Convention. This can be achieved with a custom implementation of IControllerModelConvention:
public class CustomControllerConvention : IControllerModelConvention
{
private readonly string newEndpoint;
public CustomControllerConvention(string newEndpoint)
{
this.newEndpoint = newEndpoint;
}
public void Apply(ControllerModel controllerModel)
{
if (controllerModel.ControllerType.AsType() != typeof(CustomController))
return;
foreach (var selectorModel in controllerModel.Selectors)
selectorModel.AttributeRouteModel.Template = newEndpoint;
}
}
This example just replaces the existing template (api/[controller]) with whatever is provided in the CustomControllerConvention constructor. The next step is to register this new convention, which can be done via the call to AddMvc. Here's an example of how that works:
services.AddMvc(o =>
{
o.Conventions.Add(new CustomControllerConvention("api/whatever"));
});
That's all that's needed to make things work here, but as you're offering this up from another assembly, I'd suggest an extension method based approach. Here's an example of that:
public static class MvcBuilderExtensions
{
public static IMvcBuilder SetCustomControllerRoute(
this IMvcBuilder mvcBuilder, string newEndpoint)
{
return mvcBuilder.AddMvcOptions(o =>
{
o.Conventions.Add(new CustomControllerConvention(newEndpoint));
});
}
}
Here's how that would be called:
services.AddMvc()
.SetCustomControllerRoute("api/whatever");
This whole approach means that without a call to SetCustomControllerRoute, api/Custom will still be used as a default.
So I decided to use this method since it's the simplest to add custom headers for my api calls, example:
[HttpPost]
[Route("something")]
public async Task<somethingObject> DoSomething([Microsoft.AspNetCore.Mvc.FromHeader(Name = "deviceGuid")] string deviceGuid)
{
var somethingObject= await doSomethingWithDevice(deviceGuid)
return somethingObject;
}
The expected outcome from this is a field in Swagger where I can input the deviceGuid and Swagger should consider it as a header.
Issue at hand is that Swagger is considering it a query and not a header:
Any clue how I can solve this?
I dont think SwashBuckle (im guessing this is the swagger implementation you're using), knows about FromHeader.
What you could do, is this;
Make an OperationFilter that changes the ParameterType to Header - something like the following; - note I found this in this gist.
public class FromHeaderAttributeOperationFilter : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
foreach (var httpParameterDescriptor in apiDescription.ActionDescriptor.GetParameters().Where(e => e.GetCustomAttributes<FromHeaderAttribute>().Any()))
{
var parameter = operation.parameters.Single(p => p.name == httpParameterDescriptor.ParameterName);
parameter.name = httpParameterDescriptor.GetCustomAttributes<FromHeaderAttribute>().Single().HeaderName;
parameter.#in = "header";
}
}
}
How can I add individual headers on different controllers.
E.g.:
Controller Name: Controller1,
Custom header: Header1
Controller Name: Controller2,
Custom header: Header2
The headers should be displayed for all the apis under the specific controller
This can be solved by adding an OperationFilter to your swagger configuration.
First you have to provide a class that implements IOperationFilter. The Applymethod receives an Operation parameter which contains the controller name in the tagfield. When the Swagger UI is rendered, the Applymethod will be called for each method in the API. You could even provide individual parameters for each API method, as Operation also contains the operationId.
public class AddRequiredHeaderParameter : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
if (operation.parameters == null)
operation.parameters = new List<Parameter>();
if (operation.tags[0]?.CompareTo("Example") == 0)
{
operation.parameters.Add(new Parameter
{
name = "X-ExampleParam",
#in = "header",
#default = "42", // optional default value, can be omitted
type = "string",
description = "My special parameter for the example API",
required = true
});
}
else if (operation.tags[0]?.CompareTo("Whatever") == 0)
{
// add other header parameters here
}
}
}
In the debugger, with a controller named ExampleController, it looks like this:
The result in the Swagger UI is a special parameter that is only applied to the API of my Example controller:
Tell Swagger to use your OperationFilter by adding one line in the Register method of the SwaggerConfig class:
public class SwaggerConfig
{
public static void Register(HttpConfiguration config)
{
var thisAssembly = typeof(SwaggerConfig).Assembly;
//GlobalConfiguration.Configuration
config
.EnableSwagger(c =>
{
... // omitted some lines here
c.OperationFilter<AddRequiredHeaderParameter>(); // Add this line
... // omitted some lines here
})
}
The idea to this solution is based on ShaTin's answer: How to send custom headers with requests in Swagger UI?
This is not an answer but stackoverflow wont' let me just make a comment on the solution from jps. Just wanted to add this is what I needed for my using clauses in jps's answer to get this to work in regular .net:
using Swashbuckle.Application;
using Swashbuckle.Swagger;
using System.Collections.Generic;
using System.Web.Http.Description;
I am currently using swagger in my project and i have more than 100 controllers there. I guess due to the large number of controller, swagger UI documentation page takes more than 5 min to load its controller. Is it possible to select specific controllers at the UI page and load options for them only?
Or else there are other methods to load UI page faster?
Help me!
you can use ApiExplorerSettings on either controller to ignore a controller completely or on a method.
[ApiExplorerSettings(IgnoreApi = true)]
public class MyController
{
[ApiExplorerSettings(IgnoreApi = true)]
public string MyMethod
{
...
}
}
Using swashbuckle's document filter you can remove some elements of the generated specification after the fact, and they would then not be included on the integrated swagger-ui. Create a class such as the below:
using System;
using System.Web.Http.Description;
using Swashbuckle.Swagger;
internal class SwaggerFilterOutControllers : IDocumentFilter
{
void IDocumentFilter.Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer)
{
foreach (ApiDescription apiDescription in apiExplorer.ApiDescriptions)
{
Console.WriteLine(apiDescription.Route.RouteTemplate);
if ((apiDescription.RelativePathSansQueryString().StartsWith("api/System/"))
|| (apiDescription.RelativePath.StartsWith("api/Internal/"))
|| (apiDescription.Route.RouteTemplate.StartsWith("api/OtherStuff/"))
)
{
swaggerDoc.paths.Remove("/" + apiDescription.Route.RouteTemplate.TrimEnd('/'));
}
}
}
}
and then edit your SwaggerConfig.cs file to include the filter:
GlobalConfiguration.Configuration
.EnableSwagger(c =>
c.DocumentFilter<SwaggerFilterOutControllers>();
Note that while the controllers have been removed from the specification, other items such as the result models will still be included in the specification and might still be slowing down the page load.
It could also be slow simply due to enumerating all of the controllers/models etc in the first place, in which case this might not help.
Edit: I noticed it would regenerate the whole definition every time the UI page was viewed (which could be crippling in your scenario). Fortunately it's super easy to cache this (which should be fine as it shouldn't change at runtime for the majority of people).
Add this to your config:
c.CustomProvider((defaultProvider) => new CachingSwaggerProvider(defaultProvider));
and use this class shamelessly copied from https://github.com/domaindrivendev/Swashbuckle/blob/master/Swashbuckle.Dummy.Core/App_Start/CachingSwaggerProvider.cs
using Swashbuckle.Swagger;
using System.Collections.Concurrent;
namespace <your namespace>
{
public class CachingSwaggerProvider : ISwaggerProvider
{
private static ConcurrentDictionary<string, SwaggerDocument> _cache =
new ConcurrentDictionary<string, SwaggerDocument>();
private readonly ISwaggerProvider _swaggerProvider;
public CachingSwaggerProvider(ISwaggerProvider swaggerProvider)
{
_swaggerProvider = swaggerProvider;
}
public SwaggerDocument GetSwagger(string rootUrl, string apiVersion)
{
string cacheKey = string.Format("{0}_{1}", rootUrl, apiVersion);
return _cache.GetOrAdd(cacheKey, (key) => _swaggerProvider.GetSwagger(rootUrl, apiVersion));
}
}
}
In response to the previous answer, this is the updated code for ASP.NET Core. I also added the feature to remove models.
using System;
using System.Linq;
using System.Web.Http;
using Swashbuckle.AspNetCore.SwaggerGen;
using Swashbuckle.AspNetCore.Swagger;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
internal class SwaggerFilterOutControllers : IDocumentFilter
{
void IDocumentFilter.Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
{
foreach (var item in swaggerDoc.Paths.ToList())
{
if (!(item.Key.ToLower().Contains("/api/endpoint1") ||
item.Key.ToLower().Contains("/api/endpoint2")))
{
swaggerDoc.Paths.Remove(item.Key);
}
}
swaggerDoc.Definitions.Remove("Model1");
swaggerDoc.Definitions.Remove("Model2");
}
}
swaggerDoc.paths.Remove("/" + apiDescription.Route.RouteTemplate.TrimEnd('/'));did not remove anything for me. So,
internal class SwaggerFilterOutControllers : IDocumentFilter
{
void IDocumentFilter.Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer)
{
foreach (var item in swaggerDoc.Paths.ToList())
{
if (!(item.Key.ToLower().Contains("/api/v1/xxxx") ||
item.Key.ToLower().Contains("/api/v1/yyyy")))
{
swaggerDoc.Paths.Remove(item.Key);
}
}
}
}
You can try this. You APIExplorerSetting to specify APIs to be included in a particular group.
Start by defining multiple Swagger docs in Startup.cs:
services.AddSwaggerGen(c => {
c.SwaggerDoc("v1", new OpenApiInfo {
Title = "My API - V1",
Version = "v1"
});
c.SwaggerDoc("v2", new OpenApiInfo {
Title = "My API - V2",
Version = "v2"
});
});
Then decorate the individual controller with the above groups:
[ApiExplorerSettings(GroupName = "v2")]
Reference: https://github.com/domaindrivendev/Swashbuckle.AspNetCore#generate-multiple-swagger-documents
You can use DocInclusionPredicate to Customize the Action Selection Process:
When selecting actions for a given Swagger document, the generator invokes a DocInclusionPredicate against every ApiDescription that's surfaced by the framework. The default implementation inspects ApiDescription.GroupName and returns true if the value is either null OR equal to the requested document name. However, you can also provide a custom inclusion predicate.
services.AddSwaggerGen(c =>
{
c.DocInclusionPredicate((string title, ApiDescription apiDesc) =>
{
// filter the ApiDescription
});
});