I am trying to abstract the auto-generated ODataController class in VS 2013 because the code looks identical across different controllers except the name of the POCO, so, I did the following:
public abstract class ODataControllerBase<T,DB> : ODataController
where T : class, IIdentifiable, new()
where DB : DbContext, new()
{
protected DB _DataContext;
public ODataControllerBase() : base()
{
_DataContext = new DB();
}
// only one function shown for brevity
[Queryable]
public SingleResult<T> GetEntity([FromODataUri] int key)
{
return SingleResult.Create(_DataContext.Set<T>().Where(Entity => Entity.Id.Equals(key)));
}
}
IIdentifiable is an interface that forces the T parameter to have a readable/writable Id integer property.
The implementation looks like this (POCOs and DataContexts should've already been created)
public class MyObjectsController : ODataControllerBase<MyObject,MyDbContext>
{
public MyObjectsController() : base()
{
}
// That's it - done because all the repetitive code has been abstracted.
}
Now, my WebApiConfig's Register function contains the following only:
public static void Register(HttpConfiguration config)
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<MyObject>("MyObjects");
config.Routes.MapODataRoute("odata", "odata", builder.GetEdmModel());
}
I run the project, http://localhost:10000/odata/MyObjects and I get the response:
<m:error>
<m:code/>
<m:message xml:lang="en-US">No HTTP resource was found that
matches the request URI `http://localhost:10000/odata/MyObjects.`
</m:message>
<m:innererror>
<m:message>No routing convention was found to select an action
for the OData path with template '~/entityset'.
</m:message>
<m:type/>
<m:stacktrace/>
</m:innererror>
</m:error>
What is missing? What should I remove? Is this something we can't do, i.e. are we really required to inherit ODataController directly with no intermediate parent class?
In one of our projects We also use a generic ODataController base class where we actually use GetEntity for retrieving single entities and GetEntitySet for retrieving a list of entities.
According to your supplied URL and the resulting error message, the ODATA framework cannot find an ODataAction for ~/entityset. As you have given http://localhost:10000/odata/MyObjects as the example, the action in question cannot be public SingleResult<T> GetEntity([FromODataUri] int key) as this only corresponds to a query like this http://localhost:10000/odata/MyObjects(42).
Our code for a generic controller looks like this:
public abstract class OdataControllerBase<T> : ODataController
where T : class, IIdentifiable, new()
{
protected OdataControllerBase(/* ... */)
: base()
{
// ...
}
public virtual IHttpActionResult GetEntity([FromODataUri] long key, ODataQueryOptions<T> queryOptions)
{
// ...
return Ok(default(T));
}
public virtual async Task<IHttpActionResult> GetEntitySet(ODataQueryOptions<T> queryOptions)
{
// ...
return Ok<IEnumerable<T>>(default(List<T>));
}
public virtual IHttpActionResult Put([FromODataUri] long key, T modifiedEntity)
{
// ...
return Updated(default(T));
}
public virtual IHttpActionResult Post(T entityToBeCreated)
{
// ...
return Created(default(T));
}
[AcceptVerbs(HTTP_METHOD_PATCH, HTTP_METHOD_MERGE)]
public virtual IHttpActionResult Patch([FromODataUri] long key, Delta<T> delta)
{
// ...
return Updated(default(T));
}
public virtual IHttpActionResult Delete([FromODataUri] long key)
{
// ...
return Updated(default(T));
}
}
The code for a specific controller then is as short as this:
public partial class KeyNameValuesController : OdataControllerBase<T>
{
public KeyNameValuesController(/* ... */)
: base()
{
// there is nothing to be done here
}
}
However we found out that both Get methods (for single result and enumerable result) actually have to start with Get. First we tried List instead of GetEntitySet and this did not work, as the framework then expects a POST for the List action).
You can actually verify and diagnose the resolving process by supplying a custom IHttpActionSelector as described in Routing and Action Selection in ASP.NET Web API (ahving a look at ASP.NET WEB API 2: HTTP Message Lifecycle might also be worth it).
So actually it is possible to use GetEntity as your method name as you originally tried in your example and there is no need to rename it to simple Get. In addition, there is no need for any modification in your ODATA configuration.
To determine which action to invoke, the framework uses a routing table. The Visual Studio project template for Web API creates a default route:
routes.MapHttpRoute(
name: "API Default",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Routing by Action Name
With the default routing template, Web API uses the HTTP method to select the action. However, you can also create a route where the action name is included in the URI:
routes.MapHttpRoute(
name: "ActionApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
I configured config as follows:
config.Routes.MapHttpRoute(
name: "GetMessage",
routeTemplate: "api/{controller}/{action}/{quoteName}",
defaults: new { quoteName = RouterParameters.Optional }
);
Access your URI like this:
http://localhost:42201/api/Extract/GetMessage/Q3
OR
http://localhost:42201/api/Extract/GetMessage/?quotename=Q3
Related
I have the custom AuthorizeAttribute where I need to use one of the business layer services to validate some data in the database before giving user a permission to view the resource. In order to be able to allocate this service within the my AuthorizeAttribute I decided to use service location "anti-pattern", this is the code:
internal class AuthorizeGetGroupByIdAttribute : AuthorizeAttribute
{
private readonly IUserGroupService _userGroupService;
public AuthorizeGetGroupByIdAttribute()
{
_userGroupService = ServiceLocator.Instance.Resolve<IUserGroupService>();
}
//In this method I'm validating whether the user is a member of a group.
//If they are not they won't get a permission to view the resource, which is decorated with this attribute.
protected override bool IsAuthorized(HttpActionContext actionContext)
{
Dictionary<string, string> parameters = actionContext.Request.GetQueryNameValuePairs().ToDictionary(x => x.Key, x => x.Value);
int groupId = int.Parse(parameters["groupId"]);
int currentUserId = HttpContext.Current.User.Identity.GetUserId();
return _userGroupService.IsUserInGroup(currentUserId, groupId);
}
protected override void HandleUnauthorizedRequest(HttpActionContext actionContex)
{
if (!HttpContext.Current.User.Identity.IsAuthenticated)
{
base.HandleUnauthorizedRequest(actionContex);
}
else
{
actionContex.Response = new HttpResponseMessage(HttpStatusCode.Forbidden);
}
}
}
I have couple of other attributes like this in my application. Using service locator is probably not a good approach. After searching the web a little bit I found some people suggesting to use IAuthorizationFilter with dependency injection instead. But I don't know how to write this kind of IAuthorizationFilter. Can you help me writing IAuthorizationFilter that will do the same thing that the AuthorizeAttribute above?
So after struggling for a while I think I managed to resolve this issue. Here are the steps you have to do in order to that:
1) First you have to make GetGroupByIdAttribute passive, and by passive I mean an empty attribute without any logic within it (it will be used strictly for decoration purposes)
public class GetGroupByIdAttribute : Attribute
{
}
2) Then you have to mark a controller method, for which you want to add authorization, with this attribute.
[HttpPost]
[GetGroupById]
public IHttpActionResult GetGroupById(int groupId)
{
//Some code
}
3) In order to write your own IAuthorizationFilter you have to implement its method ExecuteAuthorizationFilterAsync. Here is the full class (I included comments to guide you through the code):
public class GetGroupByIdAuthorizationFilter : IAuthorizationFilter
{
public bool AllowMultiple { get; set; }
private readonly IUserGroupService _userGroupService;
//As you can see I'm using a constructor injection here
public GetGroupByIdAuthorizationFilter(IUserGroupService userGroupService)
{
_userGroupService = userGroupService;
}
public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
{
//First I check whether the method is marked with the attribute, if it is then check whether the current user has a permission to use this method
if (actionContext.ActionDescriptor.GetCustomAttributes<GetGroupByIdAttribute>().SingleOrDefault() != null)
{
Dictionary<string, string> parameters = actionContext.Request.GetQueryNameValuePairs().ToDictionary(x => x.Key, x => x.Value);
int groupId = int.Parse(parameters["groupId"]);
int currentUserId = HttpContext.Current.User.Identity.GetUserId();
//If the user is not allowed to view view the resource, then return 403 status code forbidden
if (!_userGroupService.IsUserInGroup(currentUserId, groupId))
{
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Forbidden));
}
}
//If this line was reached it means the user is allowed to use this method, so just return continuation() which basically means continue processing
return continuation();
}
}
4) The last step is to register your filter in the WebApiConfig.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Here I am registering Dependency Resolver
config.DependencyResolver = ServiceLocator.Instance.DependencyResolver;
//Then I resolve the service I want to use (which should be fine because this is basically the start of the application)
var userGroupService = ServiceLocator.Instance.Resolve<IUserGroupService>();
//And finally I'm registering the IAuthorizationFilter I created
config.Filters.Add(new GetGroupByIdAuthorizationFilter(userGroupService));
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
Now, if needed, I can create additional IActionFilters that use IUserGroupService and then inject this service at the start of the application, from WebApiConfig class, into all filters.
Perhaps try it like shown here:
Add the following public method to your class.
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
// gets the dependecies from the serviceProvider
// and creates an instance of the filter
return new GetGroupByIdAuthorizationFilter(
(IUserGroupService )serviceProvider.GetService(typeof(IUserGroupService )));
}
Also Add interface IFilterMetadata to your class.
Now when your class is to be created the DI notices that there is a CreateInstance method and will use that rather then the constructor.
Alternatively you can get the interface directly from the DI in your method by calling
context.HttpContext.Features.Get<IUserGroupService>()
So I already have 1 Web API set up and working great, but now that I am trying to set up my own admin panel ( which I did ), I need to use the DeleteUser() function from the Web API named AdminApi but I can't seem to get it working. I keep getting 404 error while giving the path that the API should be at.
Web Api Config:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
Global :
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
//Create the custom role and user
RoleActions roleActions = new RoleActions();
roleActions.AddUserAndRole();
}
AdminApi :
[Authorize(Roles = "admin")]
public class AdminApiController : ApiController
{
public string test()
{
return "test";
}
[HttpPost]
public string DeleteUser(string id)
{
ApplicationDbContext db = new ApplicationDbContext();
var user = db.Users.Find(id);
if (user != null)
{
string email = user.Email;
db.Users.Remove(user);
return "Succesfully deleted user : " + email;
}
else
return "Failed to delete user.";
}
}
Ajax :
function deleteUser (id)
{
$.ajax({
url: '../api/AdminApi/DeleteUser',
type: 'POST',
contentType: "application/json",
dataType: 'json',
data: JSON.stringify(id),
success: function (data) {
alert(data);
},
error: function (x, y, z) {
alert(x + '\n' + y + '\n' + z);
}
});
}
The ajax function is called on the page /Admin/AdminPage
so to get to the web api -> ../api/AdminApi
and the function to delete users is DeleteUser
-> ../api/AdminApi/DeleterUser
I don't get why I get a 404 error. I can understand if my function DeleteUser is not working since I haven't tested it, but I can't test it if I can't get in the function.
The issue is related to how you use the attribute:
[Route("DeleteUser")]
If you use the Attribute Route. at Method level what it does is to define new route or more routes therefore the way you should use it is like [Route('Url/path1/route1')]:
As an example of how it works:
//GET api/customer/GetMetaData
[Route('/api/customer/GetMetaData')]
public string Get2(){
//your code goes here
}
If you will be declaring several Routes in your class then you can use RoutePrefix attribute like [RoutePrefix('url')] at class level. This will set a new base URL for all methods your in Controller class.
For example:
[RoutePrefix("api2/some")]
public class SomeController : ApiController
{
// GET api2/some
[Route("")]
public IEnumerable<Some> Get() { ... }
// POST api2/some/DeleteUser/5
[HttpPost]
[Route("DeleteUser/{id:int}")]
public Some DeleteUser(int id) { ... }
}
Update
By default Web API looks at the routing URL first, what is in your [Route] I mean and it tries to match it against your post. However if your method has a complex object as parameter WebApi can't get the values from the request URI because the parameter is a complex type Web API uses a media-type formatter to read the value from the request body.
Since your string id is not a complex object and it is part of your Route WebApi expects it as part of your URL not the body. Try this instead:
[HttpPost]
public string DeleteUser([FromBody]string anotherName)
Is it possible to customize ASP.NET Web API's routing mechanism to route all requests to the API to one controller method?
If a request comes in to
www.mysite.com/api/products/
or
www.mysite.com/api/otherResource/7
All would be routed to my SuperDuperController's Get() method?
I ran into a case where I needed to do this. (Web API 2)
I first looked into creating custom IHttpControllerSelector and IHttpActionSelectors. However, that was a bit of a murky way around. So I finally settled on this dead simple implementation. All you have to do is setup a wildcard route. Example:
public class SuperDuperController : ApiController
{
[Route("api/{*url}")]
public HttpResponseMessage Get()
{
// url information
Request.RequestUri
// route values, including "url"
Request.GetRouteData().Values
}
}
Any GET request that starts with "api/" will get routed to the above method. That includes the above mentioned URLs in your question. You will have to dig out information from the Request or context objects yourself since this circumvents automatic route value and model parsing.
The good thing about this is you can still use other controllers as well (as long as their routes don't start with "api/").
I don't konw why you would want to do this and I certainly wouldn't recommend routing everything through one controller, however you could achieve this as follows. Assuming you are only ever going to have a resource with an optional id in your calls, add this to your WebApiConfig:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{resource}/{id}",
defaults: new { controller = "SuperDuper", id = RouteParameter.Optional }
);
}
}
Then define your controller method as follows:
public class SuperDuperController : ApiController
{
public IHttpActionResult Get(string resource, int? id = null)
{
return Ok();
}
}
You would need to decide on an appropriate IHttpActionResult to return for each different type of resource.
Alternatively using Attribute Routing, ensure that config.MapHttpAttributeRoutes() is present in your WebApiConfig and add the following attributes to your controller method:
[RoutePrefix("api")]
public class SuperDuperController : ApiController
{
[Route("{resource}/{id?}")]
public IHttpActionResult Get(string resource, int? id = null)
{
return Ok();
}
}
Doing a REST styled API I would like to know how I deal with duplicate action names:
public SchoolyearBrowseResponse Get(int userId)
{
return _service.GetSchoolyears(userId);
}
public SchoolyearOpenResponse Get(int id)
{
return _service.OpenSchoolyear(id);
}
It is said that the action`s name should be the request type. Now I have TWO Get methods with an int parameter which is not possible in C#.
Should I name the 2nd Get: Open(int id) ?
Doing a REST styled API I would like to know how I deal with duplicate action names
In a RESTful styled API you should never have to deal with such duplicates. In a RESTful styled API you are dealing with resources.
So in your particular case you have 2 resources:
a user
a school year
So you would have the following routes:
/users/{userId}/schoolyears -> which corresponds to your first action
/shoolyears/{id} -> which corresponds to your second action
So:
public class UsersController : ApiController
{
public SchoolyearBrowseResponse GetSchoolYears(int userId)
{
return _service.GetSchoolyears(userId);
}
}
and:
public class SchoolYearsController : ApiController
{
public SchoolyearOpenResponse Get(int id)
{
return _service.OpenSchoolyear(id);
}
}
and the final step is your routes:
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "UserSchoolYears",
routeTemplate: "api/{controller}/{userId}/schoolyears",
defaults: new { controller = "Users", action = "GetSchoolYears" }
);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
As a side note I would like to mention that a school year for a user (your SchoolyearBrowseResponse) class hardly makes sense. Normally for a given user you have a list of school years. And if you wanted to get a specific school year for a user you would use /users/{userId}/schoolyears/{schoolyearid}.
a typical solution would be naming then in the following order :)
_service.OpenSchoolyearByYear(id);
_service.OpenSchoolyearByUserId(id);
I'm trying to setup Web API routing for what I thought would be a very simple thing. However, it seems that routing in Web API is not consistent with different HTTP verbs. Suppose I have this controller with these actions...
public class AvalancheController : ApiControllerBase
{
// GET api/avalanche
public IEnumerable<Avalanche> Get() {}
// GET api/avalanche/5
public Avalanche Get(int id) {}
// GET api/avalanche/ActionTest/5
[ActionName("ActionTest")]
public Avalanche GetActionTest(int id) {}
// GET api/avalanche/ActionTest/2
[ActionName("ActionTest2")]
public Avalanche GetActionTest2(int id) {}
// POST api/avalanche
public void Post([FromBody]Avalanche value) {}
// PUT api/avalanche/5
public void Put(int id, [FromBody]Avalanche value) {}
// PUT api/avalanche/test/5
[ActionName("Test")]
public void PutTest(int id, [FromBody]Avalanche value) {}
// DELETE api/avalanche/5
public void Delete(int id) {}
}
and I have the following routes defined...
config.Routes.MapHttpRoute(
name: "ActionRoutes",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: new
{
controller = "Avalanche",
action = "(ActionTest|ActionTest2|Test)"
}
);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Then I end up with the following routes being defined...
GET api/Avalanche/ActionTest/{id}
GET api/Avalanche/ActionTest2/{id}
PUT api/Avalanche/Test/{id}
GET api/Avalanche
POST api/Avalanche
DELETE api/Avalanche/{id}
Why doesn't the default PUT route get picked up? What's different between the routing of the default GET and the default PUT? I've tried decorating the functions in every imaginable way but I get the same results.
Mainly I want to know how to get the default PUT route to be picked up. If you have any suggestions on how to modify these routes so that I don't have to have a route for each controller to specify action names that would be fantastic also.
Thanks!
Ian
EDIT: I noticed this morning that the following route is also not being defined..
GET api/Avalanche/{id}
Glad you've found solution for your problem. But I would provide my feedback based on my learning with REST services. Idea for REST webservice, is to resolve each url to a resource (or maybe entity) and depending upon HttpVerb, operation is decided. In this case, you've three GET operations, which works fine with your modification.
But I think controllers can also be re-arranged to have single GET operation and have single responsibility thus better maintainability. For ex:
AvalancheController
public class AvalancheController : ApiControllerBase
{
public IEnumerable<Avalanche> GET()
{
}
public void POST(Avalanche avalanche)
{
}
}
It can be assumed to deal with all avalanche (s) on top level, below are the operations to be defined.
GET : returns all avalanche
POST: inserts new avalanche
PUT: not used
DELETE: not used
AvalancheDetailsController
public class AvalancheDetailsController : ApiControllerBase
{
public Avalanche GET(int id)
{
}
public int PUT(int id)
{
}
public int DELETE(int id)
{
}
}
It can be assumed to deal with single avalanche, below are the operations to be defined.
GET : returns single avalanche
POST: not used
PUT: updates single avalanche
DELETE: deletes single avalanche
Now I assume we have clear distinction of between controllers. In the OP you've mentioned, there can be different GET operations, but it returns only single Avalanche. So, I would change GET method to take object as input and check for values i.e,
public class AvalanceRequest
{
public int? Id {get;set;}
public string Name {get;set;}
}
public class AvalancheDetailsController : ApiControllerBase
{
public Avalanche GET(AvalanceRequest request)
{
//write business logic based on parameters
if(request.Id.HasValue)
//return avalanche;
if(request.Name.IsNullOrEmpty())
//return avalanche
}
//other methods
}
Dealing with URL, I didn't really work with WebAPI but was trying ServiceStack to develop REST services. It allows to attach url's independent of controller names.
Url
api/Avalanche --> AvalancheController (Operations are called based on HttpVerb)
api/Avalanche/Id --> AvalancheDetailsController (Operations are called based on HttpVerb)
I don't know whether url's can be attached likewise in WebAPI, otherwise you end up having default config and call via. api/Avalanche and api/AvalancheDetails/id.
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
I am sorry for long post, hope it makes sense.