Get error swagger when change method private to public - c#

This is my code in UserController and it runs well.
private Identifier GetCurrentUser()
{
var identity = HttpContext.User.Identity as ClaimsIdentity;
if (identity != null)
{
var userClaims = identity.Claims;
return new Identifier
{
Id = userClaims.FirstOrDefault(o => o.Type == ClaimTypes.NameIdentifier)?.Value,
Role = userClaims.FirstOrDefault(o => o.Type == ClaimTypes.Role)?.Value,
};
}
return null;
}
However when I change this method from private to public I got this error with swagger.
I do not understand why I get this error. Please tell me the reason and teach me how to fix it. Thank for your attention

Like it has been pointed out in the comment, you have conflicting route names with api/GetUsers/{search} and api/GetUsers/{id}. It becomes difficult for the compiler to figure out which one you really want to use.
I recommend you change the GetUserById action method to this form so there's a distinction between the two routes.
[HttpGet("\GetUser\{id}")]
[Authorize(Roles = "Administrator,Staff")]
public IActionResult GetUser(string id)
Alternatively you could place the search term for GetUsers in the same class as the paginationFilter like this
public class SearchAndPagination
{
public PaginationFilter paginationFilter {get;set;}
public string search {get;set;}
}
Then pass as request body to GetUsers action
[HttpGet]
[Authorize(Roles = "Administrator,Staff")]
public IActionResult GetUsers([FromBody] SearchAndPagination paginationFilter)
{
var users = _userRepository.GetUsers(paginationFilter.paginationFilter, paginationFilter.search);
...
}

Related

http get request returning multiple values [duplicate]

I notice that there are a bunch of similar questions out there about this topic.
I'm getting this error when calling any of the methods below.
Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints.
I can't however sort out what is best practice in resolving the issue.
So far I haven't set up any specific routing middleware.
// api/menus/{menuId}/menuitems
[HttpGet("{menuId}/menuitems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId)
{
....
}
// api/menus/{menuId}/menuitems?userId={userId}
[HttpGet("{menuId}/menuitems")]
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId)
{
...
}
What you're trying to do is impossible because the actions are dynamically activated. The request data (such as a query string) cannot be bound until the framework knows the action signature. It can't know the action signature until it follows the route. Therefore, you can't make routing dependent on things the framework doesn't even know yet.
Long and short, you need to differentiate the routes in some way: either some other static path or making the userId a route param. However, you don't actually need separate actions here. All action params are optional by default. Therefore, you can just have:
[HttpGet("{menuId}/menuitems")]
public IActionResult GetMenuItemsByMenu(int menuId, int userId)
And then you can branch on whether userId == 0 (the default). That should be fine here, because there will never be a user with an id of 0, but you may also consider making the param nullable and then branching on userId.HasValue instead, which is a bit more explicit.
You can also continue to keep the logic separate, if you prefer, by utilizing private methods. For example:
[HttpGet("{menuId}/menuitems")]
public IActionResult GetMenuItems(int menuId, int userId) =>
userId == 0 ? GetMenuItemsByMenuId(menuId) : GetMenuItemsByUserId(menuId, userId);
private IActionResult GetMenuItemsByMenuId(int menuId)
{
...
}
private IActionResult GetMenuItemsByUserId(int menuId, int userId)
{
...
}
Action routes need to be unique to avoid route conflicts.
If willing to change the URL consider including the userId in the route
// api/menus/{menuId}/menuitems
[HttpGet("{menuId:int}/menuitems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId)
//....
}
// api/menus/{menuId}/menuitems/{userId}
[HttpGet("{menuId:int}/menuitems/{userId:int}")]
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId) {
//...
}
##Reference Routing to controller actions in ASP.NET Core
##Reference Routing in ASP.NET Core
You have the same route in your HttpGet attribute
Change to something like this :
// api/menus/{menuId}/menuitems
[HttpGet("{menuId}/getAllMenusItems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId)
{
....
}
// api/menus/{menuId}/menuitems?userId={userId}
[HttpGet("{menuId}/getMenuItemsFiltered")]
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId)
{
...
}
This is another solution that you can use for this kind of scenario:
Solution 1 and more complex, using IActionConstrain, and ModelBinders(this gives you the flexibility to bind your input to a specific DTO):
The problem you have is that your controller has the same routing for 2 different methods receiving different parameters.
Let me illustrate it with a similar example, you can have the 2 methods like this:
Get(string entityName, long id)
Get(string entityname, string timestamp)
So far this is valid, at least C# is not giving you an error because it is an overload of parameters. But with the controller, you have a problem, when aspnet receives the extra parameter it doesn't know where to redirect your request.
You can change the routing which is one solution.
Normally I prefer to keep the same names and wrap the parameters on a DtoClass, IntDto and StringDto for example
public class IntDto
{
public int i { get; set; }
}
public class StringDto
{
public string i { get; set; }
}
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
[HttpGet]
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
}
but still, you have the error. In order to bind your input to the specific type on your methods, I create a ModelBinder, for this scenario, it is below(see that I am trying to parse the parameter from the query string but I am using a discriminator header which is used normally for content negotiation between the client and the server(Content negotiation):
public class MyModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
dynamic model = null;
string contentType = bindingContext.HttpContext.Request.Headers.FirstOrDefault(x => x.Key == HeaderNames.Accept).Value;
var val = bindingContext.HttpContext.Request.QueryString.Value.Trim('?').Split('=')[1];
if (contentType == "application/myContentType.json")
{
model = new StringDto{i = val};
}
else model = new IntDto{ i = int.Parse(val)};
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
Then you need to create a ModelBinderProvider (see that if I am receiving trying to bind one of these types, then I use MyModelBinder)
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(IntDto) || context.Metadata.ModelType == typeof(StringDto))
return new MyModelBinder();
return null;
}
and register it into the container
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.ModelBinderProviders.Insert(0, new MyModelBinderProvider());
});
}
So far you didn't resolve the issue you have but we are close. In order to hit the controller actions now, you need to pass a header type on the request: application/json or application/myContentType.json. But in order to support conditional logic to determine whether or not an associated action method is valid or not to be selected for a given request, you can create your own ActionConstraint. Basically the idea here is to decorate your ActionMethod with this attribute to restrict the user to hit that action if he doesn't pass the correct media type. See below the code and how to use it
[AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
public class RequestHeaderMatchesMediaTypeAttribute : Attribute, IActionConstraint
{
private readonly string[] _mediaTypes;
private readonly string _requestHeaderToMatch;
public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch,
string[] mediaTypes)
{
_requestHeaderToMatch = requestHeaderToMatch;
_mediaTypes = mediaTypes;
}
public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch,
string[] mediaTypes, int order)
{
_requestHeaderToMatch = requestHeaderToMatch;
_mediaTypes = mediaTypes;
Order = order;
}
public int Order { get; set; }
public bool Accept(ActionConstraintContext context)
{
var requestHeaders = context.RouteContext.HttpContext.Request.Headers;
if (!requestHeaders.ContainsKey(_requestHeaderToMatch))
{
return false;
}
// if one of the media types matches, return true
foreach (var mediaType in _mediaTypes)
{
var mediaTypeMatches = string.Equals(requestHeaders[_requestHeaderToMatch].ToString(),
mediaType, StringComparison.OrdinalIgnoreCase);
if (mediaTypeMatches)
{
return true;
}
}
return false;
}
}
Here is your final change:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
[RequestHeaderMatchesMediaTypeAttribute("Accept", new[] { "application/json" })]
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
[RequestHeaderMatchesMediaTypeAttribute("Accept", new[] { "application/myContentType.json" })]
[HttpGet]
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
}
Now the error is gone if you run your app. But how you pass the parameters?:
This one is going to hit this method:
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
And this one the other one:
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
Solution 2: Routes constrains
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet("{i:int}")]
public IActionResult Get(int i)
{
return new JsonResult(i);
}
[HttpGet("{i}")]
public IActionResult Get(string i)
{
return new JsonResult(i);
}
}
This is a kind of test because I am using the default routing:
https://localhost:44374/weatherforecast/"test" should go to the one that receives the string parameter
https://localhost:44374/weatherforecast/1 should go to the one that receives an int parameter
In my case [HttpPost("[action]")] was written twice.
I got this error, and just needed to restart the service to get it working again. Probably because I was modifying the code, and it re-registered the same controller method somehow.
You can have a dispatcher endpoint that will get the calls from both endpoints and will call the right based on parameters.
(It will works fine if their are in same controller).
Example:
// api/menus/{menuId}/menuitems
[HttpGet("{menuId}/menuitems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId, int? userId)
{
if(userId.HasValue)
return GetMenuItemsByMenuAndUser(menuId, userId)
.... original logic
}
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId)
{
...
}

How do I resolve the issue the request matched multiple endpoints in .Net Core Web Api

I notice that there are a bunch of similar questions out there about this topic.
I'm getting this error when calling any of the methods below.
Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints.
I can't however sort out what is best practice in resolving the issue.
So far I haven't set up any specific routing middleware.
// api/menus/{menuId}/menuitems
[HttpGet("{menuId}/menuitems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId)
{
....
}
// api/menus/{menuId}/menuitems?userId={userId}
[HttpGet("{menuId}/menuitems")]
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId)
{
...
}
What you're trying to do is impossible because the actions are dynamically activated. The request data (such as a query string) cannot be bound until the framework knows the action signature. It can't know the action signature until it follows the route. Therefore, you can't make routing dependent on things the framework doesn't even know yet.
Long and short, you need to differentiate the routes in some way: either some other static path or making the userId a route param. However, you don't actually need separate actions here. All action params are optional by default. Therefore, you can just have:
[HttpGet("{menuId}/menuitems")]
public IActionResult GetMenuItemsByMenu(int menuId, int userId)
And then you can branch on whether userId == 0 (the default). That should be fine here, because there will never be a user with an id of 0, but you may also consider making the param nullable and then branching on userId.HasValue instead, which is a bit more explicit.
You can also continue to keep the logic separate, if you prefer, by utilizing private methods. For example:
[HttpGet("{menuId}/menuitems")]
public IActionResult GetMenuItems(int menuId, int userId) =>
userId == 0 ? GetMenuItemsByMenuId(menuId) : GetMenuItemsByUserId(menuId, userId);
private IActionResult GetMenuItemsByMenuId(int menuId)
{
...
}
private IActionResult GetMenuItemsByUserId(int menuId, int userId)
{
...
}
Action routes need to be unique to avoid route conflicts.
If willing to change the URL consider including the userId in the route
// api/menus/{menuId}/menuitems
[HttpGet("{menuId:int}/menuitems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId)
//....
}
// api/menus/{menuId}/menuitems/{userId}
[HttpGet("{menuId:int}/menuitems/{userId:int}")]
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId) {
//...
}
##Reference Routing to controller actions in ASP.NET Core
##Reference Routing in ASP.NET Core
You have the same route in your HttpGet attribute
Change to something like this :
// api/menus/{menuId}/menuitems
[HttpGet("{menuId}/getAllMenusItems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId)
{
....
}
// api/menus/{menuId}/menuitems?userId={userId}
[HttpGet("{menuId}/getMenuItemsFiltered")]
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId)
{
...
}
This is another solution that you can use for this kind of scenario:
Solution 1 and more complex, using IActionConstrain, and ModelBinders(this gives you the flexibility to bind your input to a specific DTO):
The problem you have is that your controller has the same routing for 2 different methods receiving different parameters.
Let me illustrate it with a similar example, you can have the 2 methods like this:
Get(string entityName, long id)
Get(string entityname, string timestamp)
So far this is valid, at least C# is not giving you an error because it is an overload of parameters. But with the controller, you have a problem, when aspnet receives the extra parameter it doesn't know where to redirect your request.
You can change the routing which is one solution.
Normally I prefer to keep the same names and wrap the parameters on a DtoClass, IntDto and StringDto for example
public class IntDto
{
public int i { get; set; }
}
public class StringDto
{
public string i { get; set; }
}
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
[HttpGet]
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
}
but still, you have the error. In order to bind your input to the specific type on your methods, I create a ModelBinder, for this scenario, it is below(see that I am trying to parse the parameter from the query string but I am using a discriminator header which is used normally for content negotiation between the client and the server(Content negotiation):
public class MyModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
dynamic model = null;
string contentType = bindingContext.HttpContext.Request.Headers.FirstOrDefault(x => x.Key == HeaderNames.Accept).Value;
var val = bindingContext.HttpContext.Request.QueryString.Value.Trim('?').Split('=')[1];
if (contentType == "application/myContentType.json")
{
model = new StringDto{i = val};
}
else model = new IntDto{ i = int.Parse(val)};
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
Then you need to create a ModelBinderProvider (see that if I am receiving trying to bind one of these types, then I use MyModelBinder)
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(IntDto) || context.Metadata.ModelType == typeof(StringDto))
return new MyModelBinder();
return null;
}
and register it into the container
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.ModelBinderProviders.Insert(0, new MyModelBinderProvider());
});
}
So far you didn't resolve the issue you have but we are close. In order to hit the controller actions now, you need to pass a header type on the request: application/json or application/myContentType.json. But in order to support conditional logic to determine whether or not an associated action method is valid or not to be selected for a given request, you can create your own ActionConstraint. Basically the idea here is to decorate your ActionMethod with this attribute to restrict the user to hit that action if he doesn't pass the correct media type. See below the code and how to use it
[AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
public class RequestHeaderMatchesMediaTypeAttribute : Attribute, IActionConstraint
{
private readonly string[] _mediaTypes;
private readonly string _requestHeaderToMatch;
public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch,
string[] mediaTypes)
{
_requestHeaderToMatch = requestHeaderToMatch;
_mediaTypes = mediaTypes;
}
public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch,
string[] mediaTypes, int order)
{
_requestHeaderToMatch = requestHeaderToMatch;
_mediaTypes = mediaTypes;
Order = order;
}
public int Order { get; set; }
public bool Accept(ActionConstraintContext context)
{
var requestHeaders = context.RouteContext.HttpContext.Request.Headers;
if (!requestHeaders.ContainsKey(_requestHeaderToMatch))
{
return false;
}
// if one of the media types matches, return true
foreach (var mediaType in _mediaTypes)
{
var mediaTypeMatches = string.Equals(requestHeaders[_requestHeaderToMatch].ToString(),
mediaType, StringComparison.OrdinalIgnoreCase);
if (mediaTypeMatches)
{
return true;
}
}
return false;
}
}
Here is your final change:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
[RequestHeaderMatchesMediaTypeAttribute("Accept", new[] { "application/json" })]
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
[RequestHeaderMatchesMediaTypeAttribute("Accept", new[] { "application/myContentType.json" })]
[HttpGet]
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
}
Now the error is gone if you run your app. But how you pass the parameters?:
This one is going to hit this method:
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
And this one the other one:
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
Solution 2: Routes constrains
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet("{i:int}")]
public IActionResult Get(int i)
{
return new JsonResult(i);
}
[HttpGet("{i}")]
public IActionResult Get(string i)
{
return new JsonResult(i);
}
}
This is a kind of test because I am using the default routing:
https://localhost:44374/weatherforecast/"test" should go to the one that receives the string parameter
https://localhost:44374/weatherforecast/1 should go to the one that receives an int parameter
In my case [HttpPost("[action]")] was written twice.
I got this error, and just needed to restart the service to get it working again. Probably because I was modifying the code, and it re-registered the same controller method somehow.
You can have a dispatcher endpoint that will get the calls from both endpoints and will call the right based on parameters.
(It will works fine if their are in same controller).
Example:
// api/menus/{menuId}/menuitems
[HttpGet("{menuId}/menuitems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId, int? userId)
{
if(userId.HasValue)
return GetMenuItemsByMenuAndUser(menuId, userId)
.... original logic
}
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId)
{
...
}

Build a route pattern based on Controller type and Acton name

Disclaimer: First of all, I want to mention that I looked all over the internet for an answer, read all the documentation, read all the question I could possibly find here, but no luck so far.
So, here's my case. I am building an API using ASP.NET Core 2.2 and I'm using HATEOAS (HAL specification and Halcyon library). I should provide links along with the resource itself. This what drove me towards HATEOAS in the first place. Some of the links are templated, since it might be a PUT method and id is to be specified by frontend.
The issue is, that my controllers can have very different routes (using attribute-based routing) and hardcoding links is a bad thing,cause if the route changes I need to remember to change the link where it's used as well. For this reason I decided to generate link based on Controller type and Action name. LinkGenerator is what I found, but it seems it returns null, if I don't specify all the parameters for the route. Here's a code example:
[Route("api/metadata")]
[ApiController]
public class MetadataController : ControllerBase
{
private readonly IMetadataProvider _metadataProvider;
private readonly LinkGenerator _linkGenerator;
public MetadataController(
IMetadataProvider metadataProvider,
LinkGenerator linkProvider)
{
_metadataProvider = metadataProvider;
_linkGenerator = linkProvider;
}
[HttpGet]
public IActionResult GetMetadata()
{
var metadata = _metadataProvider.GetMetadata();
// here url will be 'null', because last parameter is null
// and route requires parameter 'name' to be specified instead of 'null'
// EXPECTED: "api/metadata/{name}"
// ACTUAL: null
string url = _linkGenerator.GetPathByAction(
nameof(MetadataController.GetByName),
nameof(MetadataController).Replace(nameof(Controller), string.Empty),
null);
var response = new HALResponse(metadata)
.AddSelfLink(HttpContext.Request)
.AddLinks(new Link(name, url));
return Ok(response);
}
[HttpGet("{name}")]
public IActionResult GetByName(string name)
{
var metadata = _metadataProvider.GetMetadataForEntity(name);
return Ok(metadata);
}
}
How can I generate a link, so that it's not hardcoded and it is templated?
After a couple of hours of debugging the ASP.NET source code, I think I found a way to do this.
It seems, that LinkGenerator is intended to build a complete and valid url, so all the parameters are required. What I was looking for was actually a route pattern.
While debugging, I found a IEndpointAddressScheme<RouteValuesAddress> service injected into LinkGnerator. It is used to actually find the route patern. After that, LinkGenerator tries to fill all the parameters.
Here's the above code fixed and working:
[ApiController]
public class MetadataController : ControllerBase
{
private readonly IMetadataProvider _metadataProvider;
private readonly IEndpointAddressScheme<RouteValuesAddress> _endpointAddress;
public MetadataController(
IMetadataProvider metadataProvider,
IEndpointAddressScheme<RouteValuesAddress> endpointAddress)
{
_metadataProvider = metadataProvider;
_endpointAddress = endpointAddress;
}
[HttpGet]
public IActionResult GetMetadata()
{
var metadata = _metadataProvider.GetMetadata();
// EXPECTED: "api/metadata/{name}"
// ACTUAL: "api/metadata/{name}"
string actionName = nameof(MetadataController.GetById);
string controllerName = nameof(MetadataController).Replace(nameof(Controller), string.Empty);
var url = _endpointAddress.FindEndpoints(CreateAddress(actionName, controllerName))
.OfType<RouteEndpoint>()
.Select(x => x.RoutePattern)
.FirstOrDefault();;
var response = new HALResponse(metadata)
.AddSelfLink(HttpContext.Request)
.AddLinks(new Link(name, url));
return Ok(response);
}
[HttpGet("{name}")]
public IActionResult GetByName(string name)
{
var metadata = _metadataProvider.GetMetadataForEntity(name);
return Ok(metadata);
}
private static RouteValuesAddress CreateAddress(string action, string controller)
{
var explicitValues = new RouteValueDictionary(null);
var ambientValues = GetAmbientValues(httpContext);
explicitValues ["action"] = action;
explicitValues ["controller"] = controller;
return new RouteValuesAddress()
{
AmbientValues = ambientValues,
ExplicitValues = explicitValues
};
}
}

Extended Controller constructor does not have an instance of User

I have a basic controller that extends from Controller, the class is working fine, but I figured that I am using a lot of times the code to get the current User from the database. So I figured I should make a constructor and move the code that I use in every function there.
Basically, what I wanted to do is have the parameters ready for any of the methods in my controller.
So, this is what I have right now (and it is working fine):
public class UsersController : Controller
{
private DBContext db = new DBContext();
public ActionResult Info()
{
User user = db.Users.Where(m => m.username.Equals(User.Identity.Name)).FirstOrDefault();
return View(user);
}
public ActionResult Edit(int? id){
User user = db.Users.Where(m => m.username.Equals(User.Identity.Name)).FirstOrDefault();
if(user.id == id){
return View(user);
}
}
}
But my idea was to create something like this:
public class UsersController : Controller
{
private DBContext db = new DBContext();
private User _user;
public UsersController()
{
_user = db.Users.Where(m => m.username.Equals(User.Identity.Name)).FirstOrDefault();
}
public ActionResult Info()
{
return View(_user);
}
public ActionResult Edit(int? id){
if(_user.id == id){
return View(_user);
}
}
}
When I made these changes I get the following error:
Server Error in '/' Application.
Object reference not set to an instance of an object.
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.NullReferenceException: Object reference not set to an instance of an object.
I tried debugging and I found out that the problem is that my User is null when the constructor is called, so I am guessing, some other languages can call the parent constructor before adding or after adding their own customization, for example something like this:
public function __Construct($x){
$this->x = $x
parent::__construct();
}
or
public function __Construct($x){
parent::__construct();
$this->x = $x
}
I tried to do the same in my program, using base, but nothing seems to work and it always leads me to an error of some other nature.
I am not even sure that this is the right way to do it, because all I need is to have my User (Identity) created in the constructor
Sounds like the user isn't found, possibly because the user identity isn't populated on the thread's principal when the constructor for the controller is called.
My suggestion would be to avoid pulling the user data in the constructor and instead grab it when you need it. To avoid duplicating code, you can write a protected or private method (not an action method) to get it:
public class UsersController : Controller
{
private DBContext db = new DBContext();
private User GetCurrentUser()
{
return db.Users.Where(m => m.username.Equals(User.Identity.Name)).FirstOrDefault();
}
public ActionResult Info()
{
var user = GetCurrentUser();
return View(user);
}
public ActionResult Edit(int? id){
var user = GetCurrentUser();
if(user.id == id){
return View(user);
}
}
}
As I mentioned in my question comment, inheritance is a poor choice here. Instead what you're attempting to do is give non-specific data to a View. A better choice is to use an ActionFilter.
We need a class to store User Information for the view to consume:
public class UserInfo
{
public bool HasUser { get; set; }
public User User { get; set; }
}
We need a place to store the data that is non-specific to views. I prefer using ViewData (because this route provides strongly typed data and an easy way to debug this storage location):
public static class ViewDataExtensions
{
private const string UserInfoKey ="_UserInfo";
public static void GetUserInfo(this ViewData viewData)
{
return viewData.ContainsKey(UserInfoKey)
? viewData[UserInfoKey] as UserInfo
: null;
}
public static UserInfo SetUserInfo(this ViewData viewData, UserInfo userInfo)
{
viewData[UserInfoKey];
}
}
Next we need a way to populate that information when needed
public class AddUserToViewDataFilterAttribute : ActionFilterAttribute
{
private DBContext db = new DBContext();
public void OnActionExecuting(ActionExecutingContext context)
{
var user = context.Controller.User;
var userInfo = new UserInfo
{
HasUser = !string.IsNullOrEmpty(User.Identity?.Name),
User = !string.IsNullOrEmpty(User.Identity?.Name)
? db.Users
.Where(m => m.username.Equals(User.Identity.Name)).FirstOrDefault()
: null;
};
context.ControllerContext.ViewData.SetUserInfo(userInfo);
}
}
Populate it when needed:
public class MyController
{
public ActionResult DoesNotNeedUserInfo()
{
}
[AddUserToViewDataFilter]
public ActionResult NeedsUserInfo()
{
}
}
In the view:
#model <whatever>
#if (ViewData.GetUserInfo().HasUser) {
<div>#ViewData.GetUserInfo().User.Name</div>
}

ASP.NET MVC Attribute to only let user edit his/her own content

I have a controller method called Edit in which the user can edit data they had created like so ...
public ActionResult Edit(int id)
{
Submission submission = unit.SubmissionRepository.GetByID(id);
User user = unit.UserRepository.GetByUsername(User.Identity.Name);
//Make sure the submission belongs to the user
if (submission.UserID != user.UserID)
{
throw new SecurityException("Unauthorized access!");
}
//Carry out method
}
This method works fine however it is a little messy to put in every controller Edit method. Each table always has a UserID so I was wondering if there was an easier way to automate this via an [Authorize] Attribute or some other mechanism to make the code cleaner.
Yes, you could achieve that through a custom Authorize attribute:
public class MyAuthorizeAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var authorized = base.AuthorizeCore(httpContext);
if (!authorized)
{
return false;
}
var rd = httpContext.Request.RequestContext.RouteData;
var id = rd.Values["id"];
var userName = httpContext.User.Identity.Name;
Submission submission = unit.SubmissionRepository.GetByID(id);
User user = unit.UserRepository.GetByUsername(userName);
return submission.UserID == user.UserID;
}
}
and then:
[MyAuthorize]
public ActionResult Edit(int id)
{
// Carry out method
}
and let's suppose that you need to feed this submission instance that we fetched into the custom attribute as action parameter to avoid hitting the database once again you could do the following:
public class MyAuthorizeAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var authorized = base.AuthorizeCore(httpContext);
if (!authorized)
{
return false;
}
var rd = httpContext.Request.RequestContext.RouteData;
var id = rd.Values["id"];
var userName = httpContext.User.Identity.Name;
Submission submission = unit.SubmissionRepository.GetByID(id);
User user = unit.UserRepository.GetByUsername(userName);
rd.Values["model"] = submission;
return submission.UserID == user.UserID;
}
}
and then:
[MyAuthorize]
public ActionResult Edit(Submission model)
{
// Carry out method
}
I would suggest you pull the logic out of the action/controller and build a domain class to handle that logic.
Action methods should really only deal with getting data from and sending data to the view. You could create something generic enough to handle your needs but will also follow the single responsibility principal.
public class AuthorizedToEdit
{
protected override bool AuthorizeCore(string user, int itemId)
{
var userName = httpContext.User.Identity.Name;
var authUsers = SubmissionRepository.GetAuthoriedUsers(itemId);
return authUsers.Contains(user);
}
}
This would also allow you to have the flexibility later on to allow something like admin users
#if (Request.IsAuthenticated && User.IsInRole("Student"))
{
#Html.ActionLink("Edit", "Edit", new { id = item.StdID })
}
in my case, the loggedIn user is a student. so i say if the login request is authenticated, and if his role is student, then let the link for edit be accessible to him.
this below allows you to let the ordinary user OR the Admin perform edit also.
#if(Request.IsAuthenticated && User.IsInRole("Student") ||
User.IsInRole("Administrator"))
{
#Html.ActionLink("Edit", "Edit", new { id = item.StdID })
}
I recommend reading up on the AuthorizeAttribute (see here). Also, have you seen this post? It goes over how to override the authentication attribute innards and how to use IPrincipal and IIdentity.

Categories