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
});
});
Related
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>();
});
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.
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;
Long story short - I have an Entity Framework model which accepts Enum type property:
public class FileUploads {
public AllowedFileTypes FileType { get; set; }
}
Enum is:
public enum AllowedFileTypes {
jpg,
png,
gif,
jpeg,
bmp,
}
Then, in a Web API controller I set a validation attribute for the IFormFile like this:
[HttpPost]
public async Task<IActionResult> Upload(
[Required]
[FileExtensions(Extensions = "jpg,png,gif,jpeg,bmp")] // allowed filetypes
IFormFile file)
{
return Ok()
}
The method is used to upload files. Now, the problem is that I am basically setting FileExtensions attribute allowed formats manually. This means that whenever a new file format is added to enum in the future - I will need to go and update each FileExtensions attribute manually. This could be easily forgotten, or any other developer could not be aware of this fact..
So, I was thinking whether or not or How is it possible to pass Enum type parameter to the FileExtensions attribute?
My attempt was the following:
[FileExtensions(Extensions = string.Join(",", Enum.GetValues(typeof(FileExtensions))))]
Unfortunately, Extensions parameter must be a const type string, therefore an error is thrown by VS. I can of course write my own custom validation attribute such as this:
FileExtensions fileExtension;
bool fileExtensionParseResult = Enum.TryParse<FileExtensions>(Path.GetExtension(file.FileName), true, out fileExtension);
Any other ideas?
So, when I deal with white lists, I generally utilize a configuration file instead of hard coding this into the application. Also, I would utilize the Content-Type header to determine the content type of the request. They should send something like image/jpeg when uploading a jpg.
If this doesn't give you enough information to get started, please comment, and I will work up a quick example.
Edited:
Here is an example from my own project. In appsettings.json, add the below:
"AllowedFileUploadTypes": {
"image/jpeg": "jpg",
"video/quicktime": "mov"
}
I generally create a wrapper class for accessing settings, and below is an example of mine my .NET Core version:
using System.Linq;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
public class AppConfigurationManager
{
private IConfiguration _configuration;
public AppConfigurationManager(IConfiguration configuration)
{
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}
public IDictionary<string, string> AllowedFileUploadTypes =>
_configuration.GetSection(nameof(AllowedFileUploadTypes)).GetChildren()
.Select(item => new KeyValuePair<string, string>(item.Key, item.Value))
.ToDictionary(x => x.Key, x => x.Value);
}
Of course you have to register this in Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
//stuff...
services.AddSingleton(Configuration);
services.AddSingleton<AppConfigurationManager>();
//other stuff...
}
}
Then you can use the AppConfigurationManager.AllowedFileUploadTypes to evaluate the IFormFile.ContentType property to validate the content type of the file is valid. You can attempt to get the value from the dictionary and then validate against that property. Based on the documentation, I am assuming that the ContentType property will be populated by the Content-Type header. I generally upload files using chunks, so I have not used IFormFile.
Edited: Wanting a way to apply to the action.
Using an ActionFilterAttribute, you could do something like this:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
public class ValidateFileExtensionsAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var fileKeyValue = context.ActionArguments.FirstOrDefault(x => typeof(IFormFile).IsAssignableFrom(x.Value.GetType()));
if (fileKeyValue.Value != null)
{
AppConfigurationManager sessionService = context.HttpContext.RequestServices.GetService(typeof(AppConfigurationManager)) as AppConfigurationManager;
IFormFile fileArg = fileKeyValue.Value as IFormFile;
if (!sessionService.AllowedFileUploadTypes.Keys.Any(x => x == fileArg.ContentType))
{
context.Result = new ObjectResult(new { Error = $"The content-type '{fileArg.ContentType}' is not valid." }) { StatusCode = 400 };
//or you could set the modelstate
//context.ModelState.AddModelError(fileKeyValue.Key, $"The content-type '{fileArg.ContentType}' is not valid.");
return;
}
}
await next();
}
}
Then you could apply that to the action like this:
[HttpPost]
[ValidateFileExtensions]
public async Task<IActionResult> Upload([Required]IFormFile file)
{
return Ok();
}
You could modify the ActionFilter to set the ModelState or you could just return the value.
My team has currently implemented a REST API (JSON) using the Web API 2 platform for .NET. We have some working URLs, such as:
/api/schools
/api/schools/5000
/api/occupations
/api/occupations/22
And here is some of our data controller code:
public class OccupationsController : ApiController
{
// /api/Occupations/1991
public IHttpActionResult GetOccupation(int id)
{
var occupation = GetAllOccupations().FirstOrDefault((p) => p.OccupationID == id);
if (occupation == null)
{
return NotFound();
}
return Ok(occupation);
}
// /api/occupations
public IEnumerable<Occupation> GetAllOccupations()
{
var ctx = new TCOSDBEntities();
return ctx.Occupations.OrderBy(occupation => occupation.Title);
}
}
We are now introducing data filtering (based on user checkbox selection) and I am curious how to approach this in our existing API, or if I should abandon REST for filtering down and try a different approach all together?
Here is our checkbox filtering mechanism:
Checkbox UI
How can I introduce search parameters in my REST services and DataController methods? Like, how do I get a range filter on a field (like Cost?)? Can I have multiple fields filtering, like Cost, Tuition, etc?
From the comments above:
You should look into the oData implementation for Web API, there are a couple of MS NuGet packages you have to install. After that its mostly configuring what you want to expose, any restrictions you want to limit the callers to (like max page size), and the rest is done by the client by manipulating the URL to filter, page, sort, etc.
Here an example:
Url sample
This retrieves the top 24 schools in the list sorted by name where the number of students is between 10 and 100 inclusive
/odata/Schools/?$count=true&$top=24&$skip=0&$filter=(numberOfStudents ge 10 and numberOfStudents le 100)&$orderby=name desc
SchoolController.cs
using System.Web.Http;
using System.Web.OData;
using System.Web.OData.Routing;
[ODataRoutePrefix("Schools")]
public sealed class SchoolODataController : ODataController
{
private DbContext _context; // your DbContext implementation, assume some DbSet<School> with the property name Schools
public SchoolODataController(DbContext context)
{
_context = context;
}
[EnableQuery(MaxNodeCount = 200, MaxTop = 100, PageSize = 64 )]
[ODataRoute]
[HttpGet]
public IHttpActionResult Get()
{
return Ok(_context.Schools);
}
}
WebApiConfig.cs
using System.Web.Http;
using System.Web.OData.Builder;
using System.Web.OData.Extensions;
public static class WebApiConfig {
public static void Register(HttpConfiguration config) {
// other code
config.MapODataServiceRoute("odata", "odata", GetModel());
}
public static IEdmModel GetModel()
{
var builder = new ODataConventionModelBuilder();
builder.EnableLowerCamelCase();
var setOrders = builder.EntitySet<SchoolModel>("Schools").EntityType.HasKey(x => new { x.SchoolId });
return builder.GetEdmModel();
}
}
NuGet packages
Install-Package Microsoft.AspNet.OData
Install-Package Microsoft.OData.Core
Install-Package Microsoft.OData.Edm