How to create custom function on OData RESTier - asp.net-mvc

I'm referring to http://odata.github.io/RESTier/#03-01-Operations on how to create a custom method that takes in input and return a list of object.
Here's my custom method
[HttpGet]
[ODataRoute("Locations/PointLoc.Data.GetLocationsByMarketId()")]
public IHttpActionResult GetLocationsByMarketId()
{
var database = new Database();
var locations = database.Locations.GetAllLocationsByMarket(1);
return Ok(locations);
}
And here's how I set it in my DbDomain
protected EdmModel OnModelExtending(EdmModel model)
{
var ns = model.DeclaredNamespaces.First();
var location = model.FindDeclaredType(ns + "." + "Location");
var locations = EdmCoreModel.GetCollection(location.GetEdmTypeReference(isNullable: false));
var getLocationsWithMarketId = new EdmFunction(ns, "GetLocationsWithMarketId", locations, true, null, false);
getLocationsWithMarketId.AddParameter("bindingParameter", locations);
model.AddElement(getLocationsWithMarketId);
return model;
}
Can't get it to work. Keep getting OData Uri error like
'Locations/PointLoc.Data.GetLocationsByMarketId()' on the action 'GetLocationsByMarketId' in controller 'PointLoc' is not a valid OData path template. The request URI is not valid. Since the segment 'Locations' refers to a collection, this must be the last segment in the request URI or it must be followed by an function or action that can be bound to it otherwise all intermediate segments must refer to a single resource.
Wanted to access the Odata via "/Locations/GetLocationsByMarketId". How to do it?

Related

BreezeJs: Error: Unable to convert this endpoint to an IQueryable

I have asp.net core 1.1.0 project and trying the model of CodeCamp sample. In this we a controller which return Lookup data as below :
[BreezeController]
public class BreezeController : ApiController
{
[HttpGet]
public object Lookups()
{
var rooms = _repository.Rooms;
var tracks = _repository.Tracks;
var timeslots = _repository.TimeSlots;
return new { rooms, tracks, timeslots };
}
And the above Lookups is called in dataContext.js as below:
function getLookups() {
return EntityQuery.from('Lookups')
.using(manager).execute()
.to$q(querySucceeded, _queryFailed);
function querySucceeded(data) {
log('Retrieved [Lookups]', data, true);
return true;
}
}
Now, I am trying to follow same as above in my project its giving me error as below :
Get http://Localhost:12345//breeze/demo/Lookups 500(Internal server error)
Uncaught (in promise)
Error: Unable to convert this endpoint to an IQueryable
Any solution to above issue...its working fine in John Papa's Code camper project. My web api lookups code is working fine if I run it in browser but not with breezejs.
Breeze's .NET Core implementation expects a hidden first parameter. It uses this to perform the IQueryable filtering of a REST operation. For example, if you have an operation that looks like this:
[HttpGet]
public IQueryable<Order> Orders()
And you wanted to get all Orders with the Status of 123 that Cost less than $10 then the first parameter would be something like this:
{
"where":{
"Status":123,
"Cost":{
"lt":10
}
},
"select":[
"OrderId"
]
}
This is a significant departure from the previous version. The client can be changed to pass parameters compatable with this by adding:
breeze.config.initializeAdapterInstance("uriBuilder", "json");
I added this to my fetchMetadata call.
However, this causes a lot of problems if you have specific get methods with parameters and you want to call it from Swagger or another application.
Something Like this:
[HttpGet]
public IQueryable<Order> GetOrdersByStatusAndLessThanCost(int status, int cost)
Will generate a url like this:
GetOrdersByStatusAndLessThanCost?status=123&cost=10
Breeze assumes that the first parameter (status=123) is its JSON. So it tries to parse it out.
This gives the first most common error with migrating Breeze to .NET Core:
This EntityQuery ctor requires a valid json string. The following is not json: status=123
If you happened to pass in Json, but the result is not an IQueryable, then you will get this error:
Unable to convert this endpoint to an IQueryable
The key to all of this is to give breeze what it is looking for. For the example above the following URL would work:
GetOrdersByStatusAndLessThanCost?{}&status=123&cost=10
Note the added {}& as the first parameter. This tells breeze that there is not anything expected as far as filtering goes.
To get this working for Swashbuckle (and by extension Swagger\Open API) add this to your Startup.cs ConfigureServices method inside the call to services.AddSwaggerGen(c =>:
c.OperationFilter<AddBreezeParameter>();
And then create the file that is needed for that:
public class AddBreezeParameter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (context.MethodInfo.ReturnType.Name.StartsWith("IQueryable"))
{
if (operation.Parameters == null)
{
operation.Parameters = new List<OpenApiParameter>();
}
var exampleString = "<br>\":{}," +
"<br> \"where\":{" +
"<br> \"Status\":123," +
"<br> \"Cost\":{" +
"<br> \"lt\":10" +
"<br> }" +
"<br> }," +
"<br> \"select\":[" +
"<br> \"OrderId\"," +
"<br> \"OrderDateTime\"" +
"<br> ]" +
"<br>}";
var breezeJsonParam = new OpenApiParameter
{
Name = "{\"breezeJson",
In = ParameterLocation.Query,
AllowEmptyValue = true,
Description =
"Json used to query a REST resource. <br>Due to Breeze's nonstandardness and Swashbuckle's not able to customize to allow for it, this MUST start with \":{} and end with } In between those you can put your query if it is appropriate. If you do you must add a comma after the starting value and before you value. Here is an example: " +
exampleString,
AllowReserved = true
};
var schema = new OpenApiSchema {Type = "json", Default = new OpenApiString("\":{}}")};
breezeJsonParam.Schema = schema;
operation.Parameters.Insert(0, breezeJsonParam);
}
else
{
if (operation.Parameters == null)
{
operation.Parameters = new List<OpenApiParameter>();
}
var breezeJsonParam = new OpenApiParameter();
// Breeze looks for the first parameter so it can do an IQueryable Filter on it.
// We want it to not have anything for that parameter if it is not an IQueryable.
breezeJsonParam.Name = "{}&";
breezeJsonParam.In = ParameterLocation.Query;
breezeJsonParam.Description = "Do NOT modify this parameter. (It is here for Breeze compatibility.)";
var schema = new OpenApiSchema {Example = new OpenApiString(" ")};
//var schema = new OpenApiSchema {Type = "string", Default = new OpenApiString("\":{}}")};
breezeJsonParam.Schema = schema;
operation.Parameters.Insert(0, breezeJsonParam);
}
}
}

Web API 2.2 - OData v4 (Manually Parsing Uri + Expanding)

I have an ODataController with a Get method as such:
public IHttpActionResult Get(ODataQueryOptions<MyModel> queryOptions) {
IQueryable<MyModel> models = _Models.AsQueryable(); // _Models Defined in Controller as List<MyModel> and is already populated with nested data for both .LevelOne and .LevelOne.LevelTwo which are two other Lists.
Uri fullrequest = Request.GetRequestContext().Url.Request.RequestUri; // http://localhost:8080/odata/Root?$expand=LevelOne($expand=LevelTwo)
Uri serviceroot = new Uri(controller.GetLeftPart(UriPartial.Path).Replace("/Root", "")); // http://localhost:8080/odata
String metadata = service + "/$metadata"; // http://localhost:8080/odata/$metadata
IEdmModel model = EdmxReader.Parse(XmlTextReader.Create(metadata));
ODataUriParser parser = new ODataUriParser(model, serviceroot, fullrequest);
SelectExpandClause selectAndExpand = parser.ParseSelectAndExpand();
//Only one of the two below lines is ever commented in...
Request.ODataProperties().SelectExpandClause = queryOptions.SelectExpand.SelectExpandClause; // This line will work
Request.ODataProperties().SelectExpandClause = selectAndExpand; // This line will not work
return Ok(models);
}
using my manually parsed selectAndExpand does not expand the dataset, but using the predefined queryOptions one does. Any ideas why? Both objects appear to contain the same information while viewed in the debugger, but I must be missing something. I want to be able to parse the URI myself, without the need for the ODataQueryOptions at all.
What I ended up doing, was building a new ODataQueryOptions object based off the original request, and then pulling just the SelectExpandClause from that. It doesn't answer my initial question, but it is a somewhat working solution for not having to pass in a ODataQueryOptions parameter. See my Code below:
public IHttpActionResult Get() {
//Get Queryable Item (in this case just a list made queryable)
IQueryable<MyModel> models = _Models.AsQueryable();
//Create new ODataQueryContext based off initial request (required to create ODataQueryOptions)
ODataQueryContext selectAndExpandContext = new ODataQueryContext(Request.ODataProperties().Model, typeof(MyModel), Request.ODataProperties().Path);
//Create new ODataQueryOptions based off new context and original request
ODataQueryOptions<Employee> selectAndExpandOptions = new ODataQueryOptions<Employee>(selectAndExpandContext, Request);
//Attach Select + Expand options to be processed
if (selectAndExpandOptions.SelectExpand != null) {
Request.ODataProperties().SelectExpandClause = selectAndExpandOptions.SelectExpand.SelectExpandClause;
}
return Ok(models);
}

How do I generate an absolute OData URL for a given entity type?

I have a working OData implementation with routes setup in the typical way:
var builder = new ODataConventionModelBuilder();
builder.EntitySet<Person>("People");
configuration.Routes.MapODataRoute(routeName:"OData", routePrefix:"odata", model:builder.GetEdmModel());
I'm looking for a way to programatically generate an absolute URL for a registered entity set from outside of any OData action. For example, I want to request the OData endpoint for the Person type and get back "http://host/odata/People".
The standard URL helpers don't seem to apply since the OData routing is convention-based.
You may want to leverage the IEdmModel instance generated by the ODataConventionModelBuilder.GetEdmModel().
IEdmModel model = builder.GetEdmModel(); // the builder is what you defined in the question.
var entitySetName = "";
foreach (var temp in model.FindEntityContainer("Container").EntitySets())
{
if (temp.ElementType.Name == "Person")
{
entitySetName = temp.Name;
break;
}
}
return "http://host/odata/"+entitySetName;
Note: if you define more than one entity set for an entity type, the upper code only returns the first one.

How do I include a model with a RedirectToAction?

In the RedirectToAction below, I'd like to pass a viewmodel. How do I pass the model to the redirect?
I set a breakpoint to check the values of model to verify the model is created correctly. It is correct but the resulting view does not contain the values found in the model properties.
//
// model created up here...
//
return RedirectToAction("actionName", "controllerName", model);
ASP.NET MVC 4 RC
RedirectToAction returns a 302 response to the client browser and thus the browser will make a new GET request to the url in the location header value of the response came to the browser.
If you are trying to pass a simple lean-flat view model to the second action method, you can use this overload of the RedirectToAction method.
protected internal RedirectToRouteResult RedirectToAction(
string actionName,
string controllerName,
object routeValues
)
The RedirectToAction will convert the object passed(routeValues) to a query string and append that to the url(generated from the first 2 parameters we passed) and will embed the resulting url in the location header of the response.
Let's assume your view model is like this
public class StoreVm
{
public int StoreId { get; set; }
public string Name { get; set; }
public string Code { set; get; }
}
And you in your first action method, you can pass an object of this to the RedirectToAction method like this
var m = new Store { StoreId =101, Name = "Kroger", Code = "KRO"};
return RedirectToAction("Details","Store", m);
This code will send a 302 response to the browser with location header value as
Store/Details?StoreId=101&Name=Kroger&Code=KRO
Assuming your Details action method's parameter is of type StoreVm, the querystring param values will be properly mapped to the properties of the parameter.
public ActionResult Details(StoreVm model)
{
// model.Name & model.Id will have values mapped from the request querystring
// to do : Return something.
}
The above will work for passing small flat-lean view model. But if you want to pass a complex object, you should try to follow the PRG pattern.
PRG Pattern
PRG stands for POST - REDIRECT - GET. With this approach, you will issue a redirect response with a unique id in the querystring, using which the second GET action method can query the resource again and return something to the view.
int newStoreId=101;
return RedirectToAction("Details", "Store", new { storeId=newStoreId} );
This will create the url Store/Details?storeId=101
and in your Details GET action, using the storeId passed in, you will get/build the StoreVm object from somewhere (from a service or querying the database etc)
public ActionResult Details(string storeId)
{
// from the storeId value, get the entity/object/resource
var store = yourRepo.GetStore(storeId);
if(store!=null)
{
// Map the the view model
var storeVm = new StoreVm { Id=storeId, Name=store.Name,Code=store.Code};
return View(storeVm);
}
return View("StoreNotFound"); // view to render when we get invalid store id
}
TempData
Following the PRG pattern is a better solution to handle this use case. But if you don't want to do that and really want to pass some complex data across Stateless HTTP requests, you may use some temporary storage mechanism like TempData
TempData["NewCustomer"] = model;
return RedirectToAction("Index", "Users");
And read it in your GET Action method again.
public ActionResult Index()
{
var model=TempData["NewCustomer"] as Customer
return View(model);
}
TempData uses Session object behind the scene to store the data. But once the data is read the data is terminated.
Rachel has written a nice blog post explaining when to use TempData /ViewData. Worth to read.
Using TempData to pass model data to a redirect request in Asp.Net Core
In Asp.Net core, you cannot pass complex types in TempData. You can pass simple types like string, int, Guid etc.
If you absolutely want to pass a complex type object via TempData, you have 2 options.
1) Serialize your object to a string and pass that.
Here is a sample using Json.NET to serialize the object to a string
var s = Newtonsoft.Json.JsonConvert.SerializeObject(createUserVm);
TempData["newuser"] = s;
return RedirectToAction("Index", "Users");
Now in your Index action method, read this value from the TempData and deserialize it to your CreateUserViewModel class object.
public IActionResult Index()
{
if (TempData["newuser"] is string s)
{
var newUser = JsonConvert.DeserializeObject<CreateUserViewModel>(s);
// use newUser object now as needed
}
// to do : return something
}
2) Set a dictionary of simple types to TempData
var d = new Dictionary<string, string>
{
["FullName"] = rvm.FullName,
["Email"] = rvm.Email;
};
TempData["MyModelDict"] = d;
return RedirectToAction("Index", "Users");
and read it later
public IActionResult Index()
{
if (TempData["MyModelDict"] is Dictionary<string,string> dict)
{
var name = dict["Name"];
var email = dict["Email"];
}
// to do : return something
}
Another way to do it is to store it in the session.
var s = JsonConvert.SerializeObject(myView);
HttpContext.Session.SetString("myView", s);
and to get it back
string s = HttpContext.Session.GetString("myView");
myView = JsonConvert.DeserializeObject<MyView>(s);

Url.Action based on the current route

I'd like to generate a new URL based on the existing route, but will add a new parameter 'page'
Here are a few examples:
old: ~/localhost/something?what=2
new: ~/localhost/something?what=2&page=5
old: ~/localhost/Shoes
new: ~/localhost/Shoes/5
I can not just append &page=5 to existing url because routes may be different.
Some use the query string and some do not.
I had a similar issue, and took the approach of extending the UrlHelper. The code in the View looks like:
Page 2
The UrlHelper extension looks like:
using System.Web.Mvc;
using System.Web.Routing;
using System.Collections.Specialized;
public static class UrlHelperExtension
{
public static string AddPage(this UrlHelper helper, int page)
{
var routeValueDict = new RouteValueDictionary
{
{ "controller", helper.RequestContext.RouteData.Values["controller"] },
{ "action" , helper.RequestContext.RouteData.Values["action"]}
};
if (helper.RequestContext.RouteData.Values["id"] != null)
{
routeValueDict.Add("id", helper.RequestContext.RouteData.Values["id"]);
}
foreach (string name in helper.RequestContext.HttpContext.Request.QueryString)
{
routeValueDict.Add(name, helper.RequestContext.HttpContext.Request.QueryString[name]);
}
routeValueDict.Add("page", page);
return helper.RouteUrl(routeValueDict);
}
}
A couple of notes: I check for the ID, since I don't use it in all my routes. I add the Page route value at the end, so it is the last url parameter (otherwise you could add it in the initial constructor).
This seems like a good approach:
// Clone Current RouteData
var rdata = new RouteValueDictionary(Url.RequestContext.RouteData.Values);
// Get QueryString NameValueCollection
var qstring = Url.RequestContext.HttpContext.Request.QueryString;
// Pull in QueryString Values
foreach (var key in qstring.AllKeys) {
if (rdata.ContainsKey(key)) { continue; }
rdata[key] = qstring[key];
}
// Update RouteData
rdata["pageNo"] = "10";
// Build Url
var url = Url.RouteUrl(rdata);
and it avoids collisions such as ?controller=example&action=problem etc.
You could reconstruct a url by pulling out the parts of the existing route by way of the RouteData object. For instance, the following would render a url with the controller and action of the current route:
<%=Url.RouteUrl(new { controller = ViewContext.RouteData.Values["controller"],
action = ViewContext.RouteData.Values["action"] }) %>
To get you started, you could go with something like a custom extension method that generates the url with an additional "page" parameter. Adjust as necessary:
public static string UrlWithPage(this UrlHelper urlHelper, string name, int page)
{
string url = urlHelper.RouteUrl(
new {
controller = urlHelper.RequestContext.RouteData.Values["controller"],
action = urlHelper.RequestContext.RouteData.Values["action"],
id = urlHelper.RequestContext.RouteData.Values["id"],
page = page
}
);
return "" + name + "";
}
This will construct a properly formatted link based on the routing configuration, whether page is real segment in the url or just appended as a querystring.

Resources