I tried to convert ASP.NET WEB API to ASP.NET CORE WEB API and have errors
My code in ASP.NET WebAPI
public class TestController : ApiController
{
// GET /test
public object Get()
{
return "get";
}
// GET /test?id={id}
public object Get(string id)
{
return id;
}
// GET /test?id={id}&anyParam={anyParam}
public object Get(string id, string anyParam)
{
return id + anyParam;
}
}
config.Routes.MapHttpRoute("Controller", "{controller}");
Try to convert it to ASP.NET Core 2.1 / 3.0
[ApiController]
[Route("{controller}")]
public class TestController : ControllerBase
{
// GET /test
public object Get()
{
return "get";
}
// GET /test?id={id}
public object Get(string id)
{
return id;
}
// GET /test?id={id}&anyParam={anyParam}
public object Get(string id, string anyParam)
{
return id + anyParam;
}
}
services.AddControllers();
app.UseRouting();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
And i have in ASP.NET Core
AmbiguousMatchException: The request matched multiple endpoints
The sensible solution is just have one method that takes three parameters.
But, sensible solutions don't make for the most interesting stackoverflow answers, so here is how you can do this with two custom attributes, one which states the parameters that are required, and another which states which parameters are excluded:
public class RequireRequestParameterAttribute : ActionMethodSelectorAttribute
{
private readonly string[] _requiredNames;
public RequireRequestParameterAttribute(params string[] names)
{
this._requiredNames = names;
}
public override bool IsValidForRequest(
RouteContext routeContext,
ActionDescriptor action
) =>
this._requiredNames
.All(
routeContext
.HttpContext
.Request
.Query
.ContainsKey
);
}
public class DisallowRequestParameterAttribute : ActionMethodSelectorAttribute
{
private readonly string[] _forbiddenNames;
public DisallowRequestParameterAttribute(params string[] names)
{
this._forbiddenNames = names;
}
public override bool IsValidForRequest(
RouteContext routeContext,
ActionDescriptor action
) =>
!(this._forbiddenNames
.Any(
routeContext
.HttpContext
.Request
.Query
.ContainsKey
)
);
}
Now you can apply the attributes as follows:
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
// GET test
public object Get()
{
return "Get";
}
// GET test?id={id}
[RequireRequestParameter("id")]
[DisallowRequestParameter("anyParam")]
public object Get(string id)
{
return id;
}
// GET test?id={id}&anyParam={anyParam}
[RequireRequestParameter("id", "anyParam")]
public object Get(string id, string anyParam)
{
return $"{id}: {anyParam}";
}
}
This means if you add another method with a third parameter, you have the maintenance burden of adding or modifying the DisallowRequestParameter attribute on the other methods.
I look your generated urls on actions and they are both /test which cause AmbiguousMatchException because your parameters are GET and are optional.
I think you can have same names on actions but you need define different ROUTE attribute (diff urls) on actions. Eg. you can not use default route with polymorphism on controller actions.
[Route("Home/About")]
MVC controllers Mapping of controllers now takes place inside
UseEndpoints.
Add MapControllers if the app uses attribute routing.
Source
https://learn.microsoft.com/cs-cz/aspnet/core/mvc/controllers/routing?view=aspnetcore-3.0#attribute-routing
Thanks to daremachine with his answer I was able to find information on Google
First step in ASP.NET Core we need class which inherit ActionMethodSelectorAttribute
public class RequireRequestValueAttribute : ActionMethodSelectorAttribute
{
public RequireRequestValueAttribute(string name, string value = null)
{
Name = name;
Value = value;
}
public string Name { get; }
public string Value { get; }
public StringComparison ComparisonType { get; } = StringComparison.OrdinalIgnoreCase;
private bool ValueIsValid(object value)
{
return ValueIsValid(value?.ToString());
}
private bool ValueIsValid(string value)
{
if (Value == null)
{
return true;
}
return string.Equals(value, Value, ComparisonType);
}
public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
{
var value = default(object);
if (routeContext.RouteData.Values.TryGetValue(Name, out value) && ValueIsValid(value))
return true;
if (routeContext.RouteData.DataTokens.TryGetValue(Name, out value) && ValueIsValid(value))
return true;
if (routeContext.HttpContext.Request.Query.ContainsKey(Name))
{
var values = routeContext.HttpContext.Request.Query[Name];
if (values.Count <= 0)
{
if (ValueIsValid(null))
return true;
}
else if (values.Any(v => ValueIsValid(v)))
return true;
}
return false;
}
}
Then we can add to question methods [RequireRequestValue("")], the controller will look like this
[ApiController]
[Route("{controller}")]
public class TestController : ControllerBase
{
// GET /test
public object Get()
{
return "get";
}
// GET /test?id={id}
[RequireRequestValue("id")]
public object Get(string id)
{
return id;
}
}
But it can't polymorphism two similar fields, type id in my question
For asp net core 2. If you try to implement the same logic as was in web api controllers then use Microsoft.AspNetCore.Mvc.WebApiCompatShim. This nuget package provides compatibility in ASP.NET Core MVC with ASP.NET Web API 2 to simplify migration of existing Web API implementations. Please check this answer. Starting with ASP.NET Core 3.0, the Microsoft.AspNetCore.Mvc.WebApiCompatShim package is no longer available.
Related
How can I show only APIs of type GET in Swagger page and hide others?
I found that the attribute [ApiExplorerSettings(IgnoreApi = true)]
can hide the API from Swagger page, but I have lot of APIs to hide and I need an approach to hide the APIs depending on its HTTP type.
I've tried this approach :
public class SwaggerFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
var nonGetPaths = swaggerDoc.Paths.Where(x => x.Value.Operations.First().Key != OperationType.Get);
var count=nonGetPaths.Count();
foreach (var item in nonGetPaths)
{
swaggerDoc.Paths.Remove(item.Key);
}
}
}
but it didn't work
Write a custom filter like this:
public class SwaggerFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
foreach (var path in swaggerDoc.Paths)
{
foreach (var key in path.Value.Operations.Keys )
{
if (key != OperationType.Get)
{
swaggerDoc.Paths.Remove(path.Key);
}
}
}
}
}
Then configure in program.cs(.Net 6)
//.......
builder.Services.AddSwaggerGen(x=>x.DocumentFilter<SwaggerFilter>());
//......
I don't add [ApiExplorerSettings(IgnoreApi = true)] in my apicontroller and it works all fine.
But, Make sure Get endpoint and other type of endpoint have different route in the same controller, You can add attribute route like [HttpGet("/get")] on Get endpoint. If you just write like this in the same controller:
[HttpPost]
public IActionResult Post()
{
return Ok();
}
[HttpGet]
public IActionResult Get()
{
return NotFound();
}
Get and Post endpoint will have the same path. swaggerDoc.Paths.Remove(xxx); will remove all of them.
Reuslt:
Before
After
I have a logic to apply in case the request received is a BadRequest, to do this I have created a filter:
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
// Apply logic
}
}
}
In Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options => { options.Filters.Add<ValidateModelAttribute>(); });
}
Controller:
[Route("api/[controller]")]
[ApiController]
public class VerifyController : ControllerBase
{
[Route("test")]
[HttpPost]
[ValidateModel]
public ActionResult<Guid> validationTest(PersonalInfo personalInfo)
{
return null;
}
}
Model:
public class PersonalInfo
{
public string FirstName { get; set; }
[RegularExpression("\\d{4}-?\\d{2}-?\\d{2}", ErrorMessage = "Date must be properly formatted according to ISO 8601")]
public string BirthDate { get; set; }
}
The thing is when I put a break point on the line:
if (!context.ModelState.IsValid)
execution reaches this line only if the request I send is valid. Why it is not passing the filter if I send a bad request?
The [ApiController] attribute that you've applied to your controller adds Automatic HTTP 400 Responses to the MVC pipeline, which means that your custom filter and action aren't executed if ModelState is invalid.
I see a few options for affecting how this works:
Remove the [ApiController] attribute
Although you can just remove the [ApiController] attribute, this would also cause the loss of some of the other features it provides, such as Binding source parameter inference.
Disable only the Automatic HTTP 400 Responses
Here's an example from the docs that shows how to disable just this feature:
services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
// ...
options.SuppressModelStateInvalidFilter = true;
// ...
}
This code goes inside of your Startup's ConfigureServices method.
Customise the automatic response that gets generated
If you just want to provide a custom response to the caller, you can customise what gets returned. I've already described how this works in another answer, here.
An example of intersection for logging is describe in Log automatic 400 responses
Add configuration in Startup.ConfigureServices.
services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
// To preserve the default behavior, capture the original delegate to call later.
var builtInFactory = options.InvalidModelStateResponseFactory;
options.InvalidModelStateResponseFactory = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Startup>>();
// Perform logging here.
//E.g. logger.LogError($”{context.ModelState}”);
logger.LogWarning(context.ModelState.ModelStateErrorsToString());
// Invoke the default behavior, which produces a ValidationProblemDetails response.
// To produce a custom response, return a different implementation of IActionResult instead.
return builtInFactory(context);
};
});
public static String ModelStateErrorsToString(this ModelStateDictionary modelState)
{
IEnumerable<ModelError> allErrors = modelState.Values.SelectMany(v => v.Errors);
StringBuilder sb = new StringBuilder();
foreach (ModelError error in allErrors)
{
sb.AppendLine($"error {error.ErrorMessage} {error.Exception}");
}
return sb.ToString();
}
As the attribute filter in the life cycle of the .Net Core you can’t handle it. The filter layer with ModelState will run after the model binding.
You can handle it with .Net Core middleware as the following https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-2.1&tabs=aspnetcore2x
If you want to SuppressModelStateInvalidFilter on individual action, consider to use custom attribute suggested on https://learn.microsoft.com/en-us/answers/questions/297568/how-to-suppress-suppressmodelstateinvalidfilter-at.html. (And similar answer https://github.com/aspnet/Mvc/issues/8575)
public class SuppressModelStateInvalidFilterAttribute : Attribute, IActionModelConvention
{
private const string FilterTypeName = "ModelStateInvalidFilterFactory";
public void Apply(ActionModel action)
{
for (var i = 0; i < action.Filters.Count; i++)
{
//if (action.Filters[i] is ModelStateInvalidFilter)
if (action.Filters[i].GetType().Name == FilterTypeName)
{
action.Filters.RemoveAt(i);
break;
}
}
}
}
Example of use
[ApiController]
public class PersonController
{
[SuppressModelStateInvalidFilter]
public ActionResult<Person> Get() => new Person();
}
I'm trying to implement authorization in my ASP.NET Core 2.0 Web app.
This app has like 20 models, each with a controller implementing at least a CRUD. I found these two pages and I liked the idea of using a handler to authorize requisitions. I would like initially to implement authorization by user, i.e., a user has only permission to see/edit his own entities. All my database entities have an OwnerId field.
These examples I found seem to only work for one specific controller.
So, my question is: is it possible to create one authorization handler for all controllers?
Have you found a solution or workaround yet that works with the authorization handler or authorization attributes? I have the exact same setup as you do.
I was trying to create a generic attribute to serve all may Entity CRUD owner checks, but generic attributes are not allowed by design.
The only two (unsatisfying) solutions that I came up with are:
Within the controller action, get the ownerId from the User, forward it all the way to your CRUD and include there a check for the ownerId. However, the code must be duplicated for every action in every controller.
[HttpGet("{id}"]
public async Task<IActionResult> GetById(int id)
{
var stringGuid = User.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
if (String.IsNullOrWhiteSpace(stringGuid)) return Unauthorized();
var ownerGuid = new Guid(stringGuid);
var entity = _yourCrudInstance.GetById(id, ownerGuid);
return Ok(entity);
}
Add a method to your CRUD repository like bool IsOwner(Guid ownerId) and use this method when creating the custom authorization handler (by creating a custom requirement together with a custom handler). This eliminates code duplication in the controller, because you can create a new policy with this custom authorization handler and consequently you can simply decorate every action with a [Authorize(Policy = "yourOwnershipPolicy")]. But still, there must be a service created for each and every controller. Moreover, the IsOwner(...) method adds an additional database call compared to solution 1 - one db call for checking the ownership (during authorization check) and one db call for actually getting the entity (by working through the controller action).
[Authorize(Policy = "yourOwnershipPolicy")]
public async Task<IActionResult> GetById(int id)
{
var entity = _yourCrudInstance.GetById(id);
return Ok(entity);
}
I am going with the first solution until I found a way to create a generic authorization handling for my generic CRUD repository, because one may forget creating the required authorization policy for a new entity, but one cannot forget to supply the parameter ownerId to .GetById(id, ownerGuid), provided there is no overload method, or the code doesn't compile.
Update:
I found a third solution in which was able to create a kind of generic authorization attribute. The trick was to use the type of concrete repository as input parameter in the authorization attribute. Yet, there is still a limitation: The authorization attribute must be copied for every type of Id, for example int Id, Guid id, etc. But still, this reduces repeated code to the types of ids. In most cases, people only have one type of id, probably int or Guid.
Here some code that demonstrates my architecture. It is heavily summarized and redacted, but should compile successfully. My original code is working and in production:
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
[Route("api/yourcontroller")]
public class YourApiController : Controller
{
private readonly YourEntityXYZRepository _repo;
public YourApiController(YourDbContext yourDbContext)
{
_repo = new YourEntityXYZRepository(yourDbContext);
}
[HttpGet("{id}")]
[AuthorizeOwnerIntId(typeof(YourEntityXYZRepository), Policy = "YourCustomPolicy")]
public async Task<IActionResult> GetById(int id)
{
var entity = _repo.GetById(id);
return Ok(entity);
}
}
// The "generic" authorization attribute for type int id
// Similar authorization attributes for every type of id must be created additionally, for example Guid
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class AuthorizeOwnerIntIdAttribute : AuthorizeAttribute, IAuthorizationFilter
{
private object _entityRepositoryObject;
private IAsyncOwnerIntId _entityRepository;
private readonly Type _TCrudRepository;
public AuthorizeOwnerIntIdAttribute(Type TCrudRepository)
{
_TCrudRepository = TCrudRepository;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var yourDbContext = context.HttpContext.RequestServices.GetService<YourDbContext>();
_entityRepositoryObject = Activator.CreateInstance(_TCrudRepository, yourDbContext);
_entityRepository = _entityRepositoryObject as IAsyncOwnerIntId;
var user = context.HttpContext.User;
if (!user.Identity.IsAuthenticated)
{
// it isn't needed to set unauthorized result
// as the base class already requires the user to be authenticated
// this also makes redirect to a login page work properly
// context.Result = new UnauthorizedResult();
return;
}
// get entityId from uri
var idString = context.RouteData.Values["id"].ToString();
if (!int.TryParse(idString, out var entityId))
{
context.Result = new UnauthorizedResult();
return;
}
// get subjectId from user claims
var ownerIdString = context.HttpContext.User.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
if (!Guid.TryParse(ownerIdString, out var ownerGuid))
{
context.Result = new UnauthorizedResult();
return;
}
if (!_entityRepository.IsEntityOwner(entityId, ownerGuid))
{
context.Result = new UnauthorizedResult();
}
}
}
// Your concrete repository
public class YourEntityXYZRepository : AsyncCrud<YourEntityXYZ, int>,
IAsyncOwnerIntId // Note that type concrete IAsyncOwnerIntId is only implemented in concrete repository
{
public YourEntityXYZRepository(YourDbContext yourDbContext) : base(yourDbContext)
{
}
}
// Your generic Crud repository
public abstract class AsyncCrud<TEntity, TId> : IAsyncCrud<TEntity, TId>
where TEntity : class, IEntityUniqueIdentifier<TId>, IEntityOwner
where TId : struct
{
protected YourDbContext YourDbContext;
public AsyncCrud(YourDbContext yourDbContext)
{
YourDbContext = yourDbContext;
}
// Note that the following single concrete implementation satisfies both interface members
// bool IsEntityOwner(TId id, Guid ownerGuid); from IAsyncCrud<TEntity, TId> and
// bool IsEntityOwner(int id, Guid ownerGuid); from IAsyncOwnerIntId
public bool IsEntityOwner(TId id, Guid ownerGuid)
{
var entity = YourDbContext.Set<TEntity>().Find(id);
if (entity != null && entity.OwnerGuid == ownerGuid)
{
return true;
}
return false;
}
// Further implementations (redacted)
public Task<bool> SaveContext() { throw new NotImplementedException(); }
public Task<TEntity> Update(TEntity entity){ throw new NotImplementedException(); }
public Task<TEntity> Create(TEntity entity, Guid ownerGuid) { throw new NotImplementedException(); }
public Task<bool> Delete(TId id) { throw new NotImplementedException(); }
public Task<bool> DoesEntityExist(TId id) { throw new NotImplementedException(); }
public virtual Task<TEntity> GetById(TId id) { throw new NotImplementedException(); }
}
// The interface for the Crud operations
public interface IAsyncCrud<TEntity, TId>
where TEntity : class, IEntityUniqueIdentifier<TId>
where TId : struct
{
bool IsEntityOwner(TId id, Guid ownerGuid);
Task<bool> DoesEntityExist(TId id);
Task<TEntity> GetById(TId id);
Task<TEntity> Create(TEntity entity, Guid ownerGuid);
Task<TEntity> Update(TEntity entity);
Task<bool> Delete(TId id);
Task<bool> SaveContext();
}
// The interface for the concrete type method for int id
// Similar interfaces for every type of id must be created additionally, for example Guid
public interface IAsyncOwnerIntId
{
bool IsEntityOwner(int id, Guid ownerGuid);
}
// Typical db context
public class YourDbContext : DbContext
{
public YourDbContext(DbContextOptions<YourDbContext> options) : base(options)
{
}
public DbSet<YourEntityXYZ> YourEntityXYZ { get; set; }
}
public class YourEntityXYZ : IEntityUniqueIdentifier<int>, IEntityOwner
{
public int Id { get; set; }
public Guid? OwnerGuid { get; set; }
// ... Additonal custom properties
}
public interface IEntityUniqueIdentifier<TId>
where TId : struct
{
TId Id { get; set; }
}
public interface IEntityOwner
{
Guid? OwnerGuid { get; set; }
}
Assume for a moment that I have an abstract controller
public abstract class ResourceController<TResource> : ApiController where TResource: Resource,new()
{
[Route("{id}")]
[HttpGet]
public async Task<IHttpActionResult> FindById([FromUri] string id)
{
TResource resource = null;
// go fetch the resource from a database or something
return Ok(resource)
}
[Route("")]
[HttpPost]
public async Task<IHttpActionResult> Create(TResource resource)
{
TResource resource = null;
// go create the resource or something
return CreatedAtRoute("XXXXX", new { id = resource.Id }, resource);
}
// more methods
}
[RoutePrefix("foo")]
public class FooResourceController : ResourceController<Foo>
{
}
[RoutePrefix("baa")]
public class BaaResourceController : ResourceController<Baa>
{
}
public class Resource
{
public string Id { get; set; }
// some other properties all resources shared
}
At this stage all the actions work, except for creating a new resource. Without overriding the Create method in every subclass, how do I find the correct route of the FindById of the respective controllers from the ResourceController Create method?
For example, if I create a foo resource with id 123 then it would return foo/123. If I created a resource baa with id 456, then it woulds return baa/456.
I'm unable to name the route using attributes, since only one can exist for the application.
Had the same problem. I fixed it by using the Created method in combination with the calling url.
This will only work if yout post doesn't have a dedicated template
My get:
[HttpGet("{id:int}")]
public async Task<IActionResult> GetAsync(int id)
{
try
{
var codetabel = await _reader.GetAsync(id);
var model = Mapper.Map<TCodeTabelModel>(codetabel);
return OkResult(model);
}
catch ....
}
And post:
[HttpPost()]
public async Task<IActionResult> InsertAsync(TCodeTabelModel model)
{
if (!ModelState.IsValid)
return BadRequestResult(ModelState);
try
{
var entity = Mapper.Map<TCodeTabelEntity>(model);
var insertedEntity = await _writer.InsertAsync(entity);
return Created($"{Request.Path.Value}/{insertedEntity.Id}" , Mapper.Map<TCodeTabelModel>(insertedEntity));
}
catch ....
}
I have a simple controller with a parameter-less Get and a Get that takes in an id.
public class BooksController : ApiController
{
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
public string Get(Identity id)
{
return "value";
}
}
Identity is a custom struct that, simplified, looks like this:
public struct Identity
{
// ....
public Identity(Guid value)
{
internalValue = value;
}
}
However, I receive an error stating that multiple actions were found that match the request if I attempt to navigate to either endpoint. The BooksController works just fine if the Get for a single resource takes in a Guid rather than my Identity struct.
My custom model binder and wire-up:
public class IdentityModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
Identity identity = new Identity(Guid.Parse(bindingContext.ValueProvider.GetValue(bindingContext.ModelName).AttemptedValue));
bindingContext.Model = identity;
return true;
}
}
GlobalConfiguration.Configuration.BindParameter(typeof(Identity), new IdentityModelBinder());
Note, the above binds fine if any parameters are added to the parameter-less Get. That is, if I change my BooksController to:
public class BooksController : ApiController
{
public IEnumerable<string> Get(int page, string searchTerm)
{
return new string[] { "value1", "value2" };
}
public string Get(Identity id)
{
return "value";
}
}
My route configuration is just the out of the box example:
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
Then I can correctly navigate to both endpoints. How do I go about setting up my BooksController to allow both the parameter-less Get and a Get that takes in my custom Identity?
I think I've found the source of my troubles and I don't see a way around it.
Within the Web API ApiControllerActionSelector, specifically in the ActionCacheSelectorItem.ctor, the logic for resolving action parameters is:
_actionParameterNames.Add(
actionDescriptor,
actionBinding.ParameterBindings
.Where(binding => !binding.Descriptor.IsOptional && TypeHelper.IsSimpleUnderlyingType(binding.Descriptor.ParameterType) && binding.WillReadUri())
.Select(binding => binding.Descriptor.Prefix ?? binding.Descriptor.ParameterName).ToArray());
Unfortunately, this means that only simple types (primitive, datetime, guid, etc) will be included in the parameter list. I can get my struct to show up in the parameter list if I extend the filter logic above to also consider if the parameter can be converted from a string like so:
_actionParameterNames.Add(
actionDescriptor,
actionBinding.ParameterBindings
.Where(binding => !binding.Descriptor.IsOptional &&
(TypeHelper.IsSimpleUnderlyingType(binding.Descriptor.ParameterType) ||
TypeHelper.HasStringConverter(binding.Descriptor.ParameterType)) &&
binding.WillReadUri())
.Select(binding => binding.Descriptor.Prefix ?? binding.Descriptor.ParameterName).ToArray());
I am hoping I'm wrong. Will come back and mark this as the answer after the cooldown period if it turns out I'm stuck.