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;
}
}
Related
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.
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.
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)?
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;
}
}
}
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);
});