How to deal with swagger against route overrides? - asp.net-core

I'm trying to embed Swagger in my Asp Core (.Net 6) project where there are some cases of route overriding. However, the issue I'm facing can be reproduced even on the following case.
Consider a minimal Asp Core (.Net 6) app. For instance, just like the one used by Swashbuckle as test: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/tree/master/test/WebSites/MinimalApp
Now, consider two controllers in the same app:
[ApiController]
[Route("/api")]
public class MyFallbackController : ControllerBase
{
[HttpGet("values", Order = 1)]
public ActionResult<object> GetValues()
{
return new[] { 1, 2, 3 };
}
}
[ApiController]
[Route("/api")]
public class MyOverrideController : ControllerBase
{
[HttpGet("values", Order = 0)]
public ActionResult<object> GetValues()
{
return new[] { 4, 5, 6 };
}
}
Notice that the routes are exactly the same, but only the first (Order = 0) will be considered.
If I run the app and navigate to:
https://localhost:7263/api/values
the response gives the expected result: [4, 5, 6]
However, when I try to access the Swagger section, it does not work because (apparently) it figures as a collision the controller pair:
An unhandled exception occurred while processing the request.
SwaggerGeneratorException: Conflicting method/path combination "GET
api/values" for actions -
WebApplication2.MyFallbackController.GetValues
(WebApplication2),WebApplication2.MyOverrideController.GetValues
(WebApplication2). Actions require a unique method/path combination
for Swagger/OpenAPI 3.0. Use ConflictingActionsResolver as a
workaround
Is there any way to get rid of that problem?

Found it.
The trick is in the SwaggerGen configuration, as the exception message suggests, by the way.
using Microsoft.AspNetCore.Mvc.ApiExplorer;
services.AddSwaggerGen(c =>
{
c.ResolveConflictingActions(apiDescriptions =>
{
int best_order = int.MaxValue;
ApiDescription? best_descr = null;
foreach (var curr_descr in apiDescriptions)
{
int curr_order = curr_descr.ActionDescriptor.AttributeRouteInfo?.Order ?? 0;
if (curr_order < best_order)
{
best_descr = curr_descr;
}
}
return best_descr;
});
});
Basically, the above function selects only the ApiDescription with the lowest order among the duplicates.
That is my naive yet effective solution. For instance, I don't know if the input collection is given already sorted by order. In that case, the code could be even simpler.

Related

.NET Core 2.1 Swashbuckle - group controllers by area

My situation is rather simple. I have a very large .NET Core 2.1 MVC/WebApi divided into several Areas, representing different modules of my system. I use Swagger (SwashBuckle) and it works very well. My routing are like {area}/{controller}/{action}.
In Swagger UI, every action is grouped into the controllers (standard behaviour). My list of controllers and operations is becoming very very large and hard to grasp. Because of that, i would love if Swagger could divide my controllers into the different areas! Making it possible to collapse area x and every controller within area x.
I really miss this feature or a way to implement it myself! Any ideas are appreciated!
UPDATE
I've tried annotating actions with tags.
This gives me:
- Area 1
- MethodFromControllerA()
- MethodFromControllerB()
- Area 2
- MethodFromControllerC()
- MethodFromControllerD()
What i want:
- Area 1
- ControllerA
- MethodFromControllerA()
- ControllerB
- MethodFromControllerB()
- Area 2
- ControllerC
- MethodFromControllerC()
- ControllerD
- MethodFromControllerD()
Update 2
Another option would be to have several specifications for each of my areas. Like different Swagger UIs for every area. Possible?
You first need to install annotations and enable them in your startup:
services.AddSwaggerGen(c =>
{
c.EnableAnnotations();
});
Then you need to add your "area" as tag to every action.
[SwaggerOperation(
Tags = new[] { "Area51" }
)]
When you open your swagger ui, it should be automatically grouped by tag now (per default the controller name is the chosen tag).
Further nested grouping of endpoints is currently not possible out of the box with the existing swagger ui generator.
If your still looking to do this by area name, this is the complete code for doing it in Swashbuckle.AspNetCore:
c.OperationFilter<TagByAreaNameOperationFilter>();
public class TagByAreaNameOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (context.ApiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
{
var areaName = controllerActionDescriptor.ControllerTypeInfo.GetCustomAttributes(typeof(AreaAttribute), true)
.Cast<AreaAttribute>().FirstOrDefault();
if (areaName != null)
{
operation.Tags = new List<OpenApiTag> { new OpenApiTag { Name = areaName.RouteValue } };
}
else
{
operation.Tags = new List<OpenApiTag> { new OpenApiTag { Name = controllerActionDescriptor.ControllerName } };
}
}
}
}
I've created a simple custom attribute to annotate the controllers that I want to group and used the TagActionsBy method when registering the Swashbuckle.
builder.Services
.AddSwaggerGen(c =>
{
c.TagActionsBy(api =>
{
if (api.ActionDescriptor is ControllerActionDescriptor actionDescriptor)
{
var group = actionDescriptor.ControllerTypeInfo.GetCustomAttributes(typeof(GroupTagAttribute), true)
.Cast<GroupTagAttribute>().FirstOrDefault();
return group != null
? new List<string> {group.Name}
: new List<string> {actionDescriptor.ControllerName};
}
throw new NullReferenceException("Couldn't find the group name");
});
// the rest of the configuration
});
The GroupTagAttribute
public class GroupTagAttribute : Attribute
{
public string Name { get; }
public ApiGroupAttribute(string name)
{
Name = name;
}
}

Implement Pagination in ASP.NET Core 2.1 Web API

I searched, but did't really found articles on how to implement pagination logic in an ASP.NET WebAPI Core 2.1 application...
I have the following
[Route("api/[controller]")]
[ApiController]
[EnableCors("AllowMyOrigin")]
public class EntriesController : ControllerBase
{
private readonly EntriesContext _context;
public EntriesController(EntriesContext context) {
_context = context;
if (_context.Entries.Count() == 0) {
_context.Entries.Add(new Entry { From = "default", To = "default" });
_context.SaveChanges();
}
}
[HttpGet]
public ActionResult<List<Entry>> GetAll() {
return _context.Entries.ToList();
}
[HttpGet("{id}", Name = "GetEntry")]
public ActionResult<Entry> GetById(long id) {
var item = _context.Entries.Find(id);
if (item == null) { return NotFound(); }
return item;
}
Now, I want my entries to be paginated using new params page and pageSize. Say
/api/entries?pageSize=3&page=2
Should I use the GetAll() method by adding some http params to it, or rather create a new method? There are no much sense to use page without pageSize, how do I manage this?
There are libraries, such as X.PagedList you can use. Frankly, pagination is pretty straight-forward, so you may not even need that. All you need to know is the page number, page size, and total number of results. The page number obvious comes from the request, and the page size can as well, if you want it customizable, or you can hard-code it.
public ActionResult<List<Entry>> GetAll(int page = 1, int size = 10)
Then, you can use Skip and Take to get the data for a particular page:
var query = _context.Entries;
var entries = await query.Skip((page - 1) * size).Take(size).ToListAsync();
var count = await query.CountAsync();
Then, all you need to know is the total number of pages, which can be calculated simply via:
var totalPages = (int)Math.Ceil(count / (float)size);
From that, you can calculate anything else you need, i.e.:
var firstPage = 1; // obviously
var lastPage = totalPages;
var prevPage = page > firstPage ? page - 1 : firstPage;
var nextPage = page < lastPage ? page + 1 : lastPage;
First of all, you can default you pageSize value to something:
[HttpGet]
public ActionResult<List<Entry>> GetAll(int? page = null, int? pageSize = 10)
{
if (!page.HasValue) {
return _context.Entries.ToList();
}
// do you pagination here
}
But you also may look at OData, it seems to be your case. It will allow you to query your data using http params, e.g.: /api/Entires?$skip=5&$top=5
I've just created a PagingTagHelper for ASP.NET Core Razor pages to render paging control easily with just the basic parameters, the simplest setup looks like below:
<paging total-records="Model.TotalRecords" page-no="Model.PageNo">
</paging>
all you need is to provide total records and page number for it to run. The default query string parameters are "p" for page number and "s" for page size, however, it is customizable/localizable, you can change all the settings according to your own requirements.
you can install it from nuget:
Install-Package LazZiya.TagHelpers -Version 1.0.2
then you need to add the tag helper to the _ViewImports.cshtml file:
#addTagHelper *, LazZiya.TagHelpers
http://ziyad.info/en/articles/21-Paging_TagHelper_for_ASP_NET_Core
more documentations and live demo will be available soon.

Asp.net core Custom routing

I am trying to implement custom routing on an asp.net core application.
The desired result is the following:
http://Site_URL/MyController/Action/{Entity_SEO_Name}/
Entity_SEO_Name parameter will be a unique value saved into the database that it is going to help me identify the id of the entity that I am trying to display.
In order to achieve that I have implemented a custom route:
routes.MapMyCustomRoute(
name: "DoctorDetails",
template: " {controller=MyController}/{action=TestRoute}/{name?}");
public class MyTemplateRoute : TemplateRoute
{
public override async Task RouteAsync(RouteContext context)
{
//context.RouteData.Values are always empty. Here is the problem.
var seo_name = context.RouteData.Values["Entity_SEO_Name"];
int entityId = 0;
if (seo_name != null)
{
entityId = GetEntityIdFromDB(seo_name);
}
//Here i need to have the id and pass it to controller
context.RouteData.Values["id"] = entityId;
await base.RouteAsync(context);
}
}
My controller actionresult:
public ActionResult TestRoute(int id)
{
var entity = GetEntityById(id);
return Content("");
}
The problem with this approach is that the context.RouteData.Values are always empty.
Any ideas on how to move forward with this one ?
Your solution too complicated. You can have route template like
template: "{controller=Home}/{action=Index}/{seo?}"
and controller action just like
public ActionResult TestRoute(string seo)
{
var entity = GetEntityBySeo(seo);
return Content("");
}
It is enough, asp.net mvc is smart enough to bind seo variable to the parameter from url path.

Breeze OData: Get single entry with GetEntityByKey (EntitySetController)

Reading the documentation in Breeze website, to retrieve a single entity have to use the fetchEntityByKey.
manager.fetchEntityByKey(typeName, id, true)
.then(successFn)
.fail(failFn)
Problem 1: Metadata
When trying to use this method, an error is displayed because the metadata has not yet been loaded. More details about the error here.
The result is that whenever I need to retrieve a single entity, have to check if the metadata is loaded.
manager = new breeze.EntityManager(serviceName);
successFn = function(xhr) {}
failFn = function(xhr) {};
executeQueryFn = function() {
return manager.fetchEntityByKey(typeName, id, true).then(successFn).fail(failFn);
};
if (manager.metadataStore.isEmpty()) {
return manager.fetchMetadata().then(executeQueryFn).fail(failFn);
} else {
return executeQueryFn();
}
Question
How can I extend the breeze, creating a Get method to check if metadata is loaded, and if not, load it?
Problem 2: OData and EntitySetController
I would use the OData standard (with EntitySetController) in my APIs.
This page in Breeze documentation shows how, then follow this tutorial to deploy my app with OData.
The problem as you can see here and here, is that the EntitySetController follows the odata pattern, to retrieve an entity must use odata/entity(id), or to retrieve all entities you can use `odata/entity'.
Example
In controller:
[BreezeController]
public class passosController : EntitySetController<Passo>
{
[HttpGet]
public string Metadata()
{
return ContextProvider.Metadata();
}
[HttpGet, Queryable(AllowedQueryOptions = AllowedQueryOptions.All, PageSize = 20)]
public override IQueryable<T> Get()
{
return Repositorio.All();
}
[HttpGet]
protected override T GetEntityByKey(int key)
{
return Repositorio.Get(key);
}
}
When I use:
manager = new breeze.EntityManager("/odata/passos");
manager.fetchEntityByKey("Passo", 1, true)
.then(successFn)
.fail(failFn)
The url generated is: /odata/passos/Passos?$filter=Id eq 1
The correct should be: /odata/passos(2)
Question
How can I modify Breeze for when use fetchEntityByKey to retrieve entity use odata/entity(id)?

FluentSecurity 2.0 support for action with parameters

In my .net mvc 4 app I am using the latest release of FluentSecurity (1.4) in order to secure my actions.
Here is an example that illustrates my problem:
Suppose I have a controller with 2 edit actions (get and post):
public class MyController : Controller
{
//
// GET: /My/
public ActionResult Edit(decimal id)
{
var modelToReturn = GetFromDb(id);
return View(modelToReturn);
}
[HttpPost]
public ActionResult Edit(MyModel model)
{
Service.saveToDb(model);
return View(model);
}
}
Now, I would like to have a different security policy for each action. To do that I define (using fluent security):
configuration.For<MyController>(x => x.Edit(0))
.AddPolicy(new MyPolicy("my.VIEW.permission"));
configuration.For<MyController>(x => x.Edit(null))
.AddPolicy(new MyPolicy("my.EDIT.permission"));
The first configuration refers to the get while the second to the post.
If you wonder why I'm sending dummy params you can have a look here and here.
Problem is that fluent security can't tell the difference between those 2, hence this doesn't work.
Couldn't find a way to overcome it (I'm open for ideas) and I wonder if installing the new 2.0 beta release can resolve this issue.
Any ideas?
It is currently not possible to apply different policies to each signature in FluentSecurity. This is because FluentSecurity can not know what signature will be called by ASP.NET MVC. All it knows is the name of the action. So FluentSecurity has to treat both action signatures as a single action.
However, you can apply multiple policies to the same action (you are not limited to have a single policy per action). With this, you can apply an Http verb filter for each of the policies. Below is an example of what it could look like:
1) Create a base policy you can inherit from
public abstract class HttpVerbFilteredPolicy : ISecurityPolicy
{
private readonly List<HttpVerbs> _httpVerbs;
protected HttpVerbFilteredPolicy(params HttpVerbs[] httpVerbs)
{
_httpVerbs = httpVerbs.ToList();
}
public PolicyResult Enforce(ISecurityContext securityContext)
{
HttpVerbs httpVerb;
Enum.TryParse(securityContext.Data.HttpVerb, true, out httpVerb);
return !_httpVerbs.Contains(httpVerb)
? PolicyResult.CreateSuccessResult(this)
: EnforcePolicy(securityContext);
}
protected abstract PolicyResult EnforcePolicy(ISecurityContext securityContext);
}
2) Create your custom policy
public class CustomPolicy : HttpVerbFilteredPolicy
{
private readonly string _role;
public CustomPolicy(string role, params HttpVerbs[] httpVerbs) : base(httpVerbs)
{
_role = role;
}
protected override PolicyResult EnforcePolicy(ISecurityContext securityContext)
{
var accessAllowed = //... Do your checks here;
return accessAllowed
? PolicyResult.CreateSuccessResult(this)
: PolicyResult.CreateFailureResult(this, "Access denied");
}
}
3) Add the HTTP verb of the current request to the Data property of ISecurityContext and secure your actions
SecurityConfigurator.Configure(configuration =>
{
// General setup goes here...
configuration.For<MyController>(x => x.Edit(0)).AddPolicy(new CustomPolicy("my.VIEW.permission", HttpVerbs.Get));
configuration.For<MyController>(x => x.Edit(null)).AddPolicy(new CustomPolicy("my.EDIT.permission", HttpVerbs.Post));
configuration.Advanced.ModifySecurityContext(context => context.Data.HttpVerb = HttpContext.Current.Request.HttpMethod);
});