OData support two routes which returns a single entity and a collection - asp.net-core

I'd like to define two routes so I can access my entities. The code example below however returns an exception whenever I try to access my swagger.json file.
Bad Request - Error in query syntax.
There are a total of 2 routes I need;
/EventLog(MeetingId={keyMeetingId},Id={keyId}) -> Returns a single entity
Search(MeetingId={keyMeetingId} -> Returns multiple records
Builder;
var eventLog = builder.EntitySet<EventLog<object>>(nameof(EventLog<object>)).EntityType;
eventLog.HasKey(x => new { x.MeetingId, x.Id });
var eventLogSearch = eventLog.Collection
.Action("Search")
.ReturnsCollectionFromEntitySet<EventLog<object>>(nameof(EventLog<object>));
eventLogSearch.Parameter<string>("MeetingId")
.Required();
Startup:
app.UseMvc(
builder =>
{
builder.ServiceProvider.GetRequiredService<ODataOptions>()
.UrlKeyDelimiter = Parentheses;
builder.Count();
builder.Expand()
.Select()
.OrderBy()
.Filter();
builder.MapVersionedODataRoutes("odata", "api", modelBuilder.GetEdmModels());
builder.EnableDependencyInjection();
});
I've tried several other solutions, but they result in either a 404 or the error like above. Any suggestions on how to support both routes?
I'm using asp.net core 2.2

Related

Ajax call for JSON using EF Core is failing/aborting

I'm at a loss with why this isn't working...
I have a .NET Core app using EF Core, and I'm making an Ajax call via jQuery to my controller to retrieve some data from the database via EF Core. Debugging the call via my developer tools in the browser (IE/Chrome) results in a status of failed/aborted. Yet when I step through my method in my controller, the method seems to be able to retrieve the data from the database via EF Core just fine.
Here's my controller:
public ActionResult GetInfo(string term)
{
using (var dbContext = new DatabaseContext())
{
// use DbContext to get data from the database
var retrievedData = dbContext.TableName.Where(...);
return Json(retrievedData.Select(data => new {
id = data.id,
text = data.text
}));
}
}
And here's the jQuery:
$(#element).select2({
...
ajax: {
url: $(#element).attr("data-getinfo"),
dataType: 'json', // tried this with jsonp and application/JSON with no luck
contentType: 'application/json; charset=utf-8',
delay: 250,
data: function (params) {
return: { term: params.term};
},
processResults: function (data) {
return {
results: $.map(data, function (item) {
return {
id: item.id, text: item.text
}
})
}
},
},
....
});
The Ajax calls work with previous apps I've worked on, but they used MVC 5 and EF 6. This also works if I retrieve dummy data, IE instead of using EF Core to get the data, I return fake data built into the controller. What gives?
To clarify the root of your problem: You are querying your database and returning an IEnumerable as a JsonResult. But first, you need to understand one step before. Calling .Where returns an IQueryable. You can think of an IQueryable as if it is a TSQL command that was not yet execute on the database. Only calls that will enumerate the results will trigger the materialization of the query.
So you did this:
// .Where returns an IQueryable. You can "chain" more wheres later.
// the query will still not be executed
var retrievedData = dbContext.TableName.Where(...);
// This then returns an IEnumerable to the client.
// The Select will materialize (execute) the query
return Json(retrievedData.Select(data => new {
id = data.id,
text = data.text
}));
The problem with your code is: .Select returns an IEnumerable which enumerates the results. But, by the time the browser or whatever client you are dealing with starts to enumerate the results, your database connection is already closed, because you used using block around your dbContext (which is kind of correct.. see comments in the end).
So, to fix it, you need basically to enumerate the results yourself or not close the connection (let the framework close for you when the request is finished..). This minor change fix the problem:
// ToList() will enumerate all the results in memory
var retrievedData = dbContext.TableName.Where(...).ToList();
Other comments:
You don't need (also shouldn't) manage the creation of the dbContext by yourself. You can register it in the DI container and the framework will do the rest for you. You can take a look in the EF Core docs to have an idea on how it is done.
Not an ideal solution, but I got it working. I suspect it might have to do with how .NET Core or EF Core was returning data to the browser, but I can't say for sure yet.
I ended up using Json.NET for a workaround. Performance isn't bad (I tried a query with hundreds of records and it only took a couple of seconds at most), and I was already using it for an external API call.
public ActionResult GetInfo(string term)
{
using (var dbContext = new DatabaseContext())
{
// use DbContext to get data from the database
var retrievedData = dbContext.TableName.Where(...);
var initJson = Json(retrievedData.Select(data => new {
id = data.id,
text = data.text
}));
var serializedJson = Newtonsoft.Json.JsonConvert.SerializeObject(initJson);
var deserializedJson = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedJson);
return Json(deserializedJson);
}
}

JSON API design - express

I want to write a JSON API.
My problem is, that sometimes I want to query for an ID, sometimes for a String.
One option would be to add a querystring, for example:
example.com/user/RandomName
example.com/user/1234556778898?id=true
and use it like:
api.get('user/:input', function(req, res) {
if(req.query.id) {
User.find({ '_id': req.params.input }, cb);
} else {
User.find({ 'name': req.params.input }, cb);
}
};
But this seems like bad practice to me, since it leads to a bunch of conditional expressions.
Are there more elegant ways?
I would suggest handling two endpoints. One for getting ALL the users and one for getting a SPECIFC user by ID.
example.com/users
example.com/users/:id
The second endpoint can be used to find a specific user by id.
The first endpoint can be used to find all users, but filters can be applied to this endpoint.
For example: example.com/users?name=RandomName
By doing this, you can very easily create a query in your Node service based on the parameters that are in the URL.
api.get('/users', function(req, res) {
// generate the query object based on URL parameters
var queryObject = {};
for (var key in req.query) {
queryObject[key] = req.query[key];
}
// find the users with the filter applied.
User.find(queryObject, cb);
};
By constructing your endpoints this way, you are following a RESTful API standard which will make it very easy for others to understand your code and your API. In addition, you are constructing an adaptable API as you can now filter your users by any field by adding the field as a parameter to the URL.
See this response for more information on when to use path parameters vs URL parameters.

Multiple actions were found that match the request with multiple PUT webapi actions

I have my routes setup like this to allow action-based routing for my webapi controllers:
config.Routes.MapHttpRoute("DefaultApiWithIdAndAction", "{controller}/{id}/{action}", null, new { id = #"\d+" });
config.Routes.MapHttpRoute("DefaultApiWithId", "{controller}/{id}", null, new {id = #"\d+"});
config.Routes.MapHttpRoute("DefaultApiWithAction", "{controller}/{action}");
config.Routes.MapHttpRoute("DefaultApiGet", "{controller}", new { action = "Get" },
new { httpMethod = new HttpMethodConstraint(HttpMethod.Get) });
config.Routes.MapHttpRoute("DefaultApiPost", "{controller}", new {action = "Post"},
new {httpMethod = new HttpMethodConstraint(HttpMethod.Post)});
Here are all of the types of routes I want to support. They all work except for the default PUT without an action. Action based PUT requests work just fine for some reason.
GET users
GET users/1
POST users
PUT users/1 <- thinks its a duplicate route
PUT users/1/assignrole <- of this route even though this one works
DEL users/1
Here is how I defined my controller actions:
public UserModel Put(int id, UserModel model)
[ActionName("assignrole")]
public UserModel PutAssignRole(int id, RoleModel model)
I would have thought that they are different due to the action name being different but mvc is not seeing it that way. What am I doing wrong?
You need to differentiate the signatures of the two methods. Change your first route to:
config.Routes.MapHttpRoute("DefaultApiWithIdAndAction",
"{controller}/{id2}/{action}",
null,
new { id2 = #"\d+" });
and then change your second action to:
[ActionName("assignrole")]
public UserModel PutAssignRole(int id2, RoleModel model)
{
...
}
Just to give you all an update, I have abandoned trying to do this with traditional webapi routing. I have adopted attribute-based routing using attributerouting.net since it appears to be the solution most are pushing to solve this issue. I made my decision mostly since the attributerouting.net functionality is being rolled into WebAPI 2 for VS2013 release. The syntax is slightly different but the features are almost exactly the same. Its a huge improvement. Even stackoverflow uses it for their routes, which helped solidify my decision even more.
You could try and restrict the first routing definition to accept just this one action since it doesn't seem to be used for any other route anyway:
config.Routes.MapHttpRoute("DefaultApiWithIdAndAction",
"{controller}/{id}/{action}",
null,
new { id = #"\d+", action="assignrole" });

How to format iqueryable mvc 4 web api requests?

I'd like to use iqueryable on all my collections so that I get all of the odata features. However I need to format the response of the request with the following fields;
{
href: "url to resouce",
length: 10,
items: [
"(IQueryable results)"
]
}
Formatting the response isnt the hard part but keeping all of the odata "stuff" working is.
So my code looks like;
MyFormattedResponse.Href = "poop.com";
MyFormattedResponse.Length = 0;
MyFormattedResponse.Items = _myRepo.Gets();
Request.CreateResponse(HttpStatusCode.OK, MyFormattedResponse);
But the error I get is:
The action 'Get' on controller 'MyResource' with return type
'MyObject' cannot support querying. Ensure the type of the returned
content is IEnumerable, IQueryable, or a generic form of either
interface.
Is this something I can construct in a media formatter or perhaps a filter?
I really want to keep the odata awesomeness...
Take a look at this answer I've provided:
Web API Queryable - how to apply AutoMapper?
Your case is similar, you can do something like this:
public MyFormattedResponse Get(ODataQueryOptions<Person> query)
{
var results = query.ApplyTo(_myRepo.Gets()) as IEnumerable<Person>;
return new MyFormattedResponse() { Href = "foo.com", Length = 5, Items = results };
}
Make sure you remove the [Queryable] attribute.

LockRecursionException calling Route.GetVirtualPath from another route's GetVirtualData in .Net 4

I have a route defined last in my ASP.Net MVC 2 app that will map old urls that are no longer used to the appropriate new urls to be redirected to. This route returns the action and controller that is responsible for actually performing the redirect and it also returns a url to the controller action which is the url to redirect to. Since the route is responsible for generating the new url to redirect to, it is calling the router to get the appropriate urls. This has worked just fine with .Net 3.5, but when I upgraded to .Net 4, the GetVirtualPath method throws a System.Threading.LockRecursionException: "Recursive read lock acquisitions not allowed in this mode.". The following code resolves the problem but is pretty ugly:
public static string GetActionUrl(HttpContextBase context, string routeName, object routeValues)
{
RequestContext requestContext = new RequestContext(context, new RouteData());
VirtualPathData vp = null;
try
{
vp = _Routes.GetVirtualPath(requestContext, routeName, new RouteValueDictionary(routeValues));
}
catch (System.Threading.LockRecursionException)
{
var tmpRoutes = new RouteCollection();
Router.RegisterRoutes(tmpRoutes);
vp = tmpRoutes.GetVirtualPath(requestContext, routeName, new RouteValueDictionary(routeValues));
}
if (vp == null)
throw new Exception(String.Format("Could not find named route {0}", routeName));
return vp.VirtualPath;
}
Does anybody know what changes in .Net 4 might have caused this error? Also, is calling a route from another route's GetRouteData method just bad practice and something I should not be doing at all?
As you figured out, it is not supported to call a route in the global route table from within another route in the global route table.
The global route table is a thread-safe collection to enable multiple readers or a single router. Unfortunately the code you have was never supported, even in .NET 3.5, though in some scenarios it may have coincidentally worked.
As a general note, routes should function independent of one another, so I'm not sure what your scenario is here.
v3.5 RouteCollection uses the following code:
private ReaderWriterLockSlim _rwLock;
public IDisposable GetReadLock()
{
this._rwLock.EnterReadLock();
return new ReadLockDisposable(this._rwLock);
}
v4.0 RouteCollection uses the following code:
private ReaderWriterLock _rwLock;
public IDisposable GetReadLock()
{
this._rwLock.AcquireReaderLock(-1);
return new ReadLockDisposable(this._rwLock);
}
GetRouteData(HttpContextBase httpContext) in both versions use the following code:
public RouteData GetRouteData(HttpContextBase httpContext)
{
...
using (this.GetReadLock())
{
GetVirtualPath uses the same logic.
The ReaderWriterLock used in v4.0 does not allow Recursive read locks by default which is why the error is occurring.
Copy routes to a new RouteCollection for second query, or change the ReaderWriterLock mode by reflecting in.