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

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);
}

Related

How to create custom function on OData RESTier

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?

How can I set content-range headers in .net mvc web api controller method?

I am using .net MVC Web API project template. This is my Get method in customer controller:
public IQueryable<Customer> Get()
{
CustomerRepository customer = new CustomerRepository ();
IQueryable<Customer> customer = lr.GetCustomer();
return data;
}
How can I add the content range headers along with data returned?:
content-range: item 0-9/100
**EDIT
I changed it to return HttpResponseMessage but still unsure about setting the content-range item. Not sure if I hard code "item 0-9/100" or if there is mechanism to know how many items to return?
public HttpResponseMessage Get()
{
CustomerRepository lr = new CustomerRepository();
IQueryable<Customer> data = lr.GetCustomer();
var resp = new HttpResponseMessage(HttpStatusCode.OK);
resp.Content = new ObjectContent<IQueryable<Customer>>(data, new JsonMediaTypeFormatter());
resp.Headers.Add("Content-Range", ???????)
return resp;
}
Answer to edited question.
The Content range header refers an index of the results returned in the Http Response. Item 0-9/100 means the response contains the first 10 items (indexes 0,1,2,3,4,5,6,7,8,9) of the 100 total.
It would make sense to return the index that matches the results returned from your method. You will need to determine what indexes are represented in your IQueryable<Customer> customer object and fill out the header appropriately.

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.

what's the best practice to attach a entity object which is detached from anthoer ObjectContext?

As mentioned in the title, how many methods are available?
I just have this case: I get a entity object from one ObjectContext, and then I detach the entity obejct from OjbectContext object, and return it.
Later, if I make some changes on this object, and I want to save the changes back to database. I think I should write like this, right? (Well, this works for me.)
public Url GetOneUrl()
{
Url u;
using(ServicesEntities ctx = new ServicesEntities())
{
u = (from t in ctx.Urls select t).FirstOrDefault<Url>();
ctx.Detach(u);
}
return u;
}
public void SaveToDB(Url url)
{
using(ServicesEntities ctx = new ServicesEntities())
{
var t = ctx.GetObjectByKey(_Url.EntityKey) as Url;
ctx.Detach(t);
ctx.Attach(url);
ctx.ObjectStateManager.ChangeObjectState(url, System.Data.EntityState.Modified);
ctx.SaveChanges();
}
}
Url url = GetOneUrl();
url.UrsString = "http://google.com"; //I just change the content.
SaveToDB(url);
OR
public void SaveToDB(Url url)
{
using(ServicesEntities ctx = new ServicesEntities())
{
var t = ctx.GetObjectByKey(_Url.EntityKey) as Url;
t = url; //this will make t.UrlString becomes "http://google.com"
ctx.ApplyCurrentValues<Url>("Urls", t);
ctx.SaveChanges();
}
}
This way is also works for me.
The first way will generate sql statement to update all the columns of Url table, but the second method will provide a sql script only update the "UrlString" Columns.
Both of them will have to retrieve a temp entity object from database which is the 't' in above code.
Are there any other methods to achieve this purpose? Or other better method you know about it? Or any official solution about this topic?
Many Thanks.
I don't understand your first example. Why do you first get entity from ObjectContext? It is not needed because you have just created new instance of the context. You can just use:
public void SaveToDB(Url url)
{
using(ServicesEntities ctx = new ServicesEntities())
{
ctx.Attach(url);
ctx.ObjectStateManager.ChangeObjectState(url, System.Data.EntityState.Modified);
ctx.SaveChanges();
}
}
In your second example you can just call:
public void SaveToDB(Url url)
{
using(ServicesEntities ctx = new ServicesEntities())
{
var t = ctx.GetObjectByKey(_Url.EntityKey) as Url; // Ensures that old values are loaded
ctx.ApplyCurrentValues<Url>("Urls", url);
ctx.SaveChanges();
}
}
Now the difference between two approaches is clear. First approach (Attach) does not need to query the DB first. Second approach (ApplyCurrentValues) needs to query the DB first to get old values.
You can use two additional approaches. First is just extension of your former approach. It allows you defining which properties were changed. Second approach is manual synchronization with loaded entity. This approach doesn't use any special methods. You will simply set loaded entity's properties to required values manually. This approach is useful if you work with object graph instead of single entity because EF is not able to automatically synchronize changes in relations.

Merge an Object that wen outside the datacontext

I have the following question:
It is easy to insert an oBject in database with a form.
Just create an object
link it to the fields in your from.
Post back to controller,
create a new datacontext and do datacontext.InsertOnSubmit(object)
.
public static void AddPage(string lang, Page page)
{
using (var db = new CardReaderDataContext())
{
page.Lang = lang;
page.URL = UrlHelper.CreateValidSeoUrl(page.Name, "-");
db.Pages.InsertOnSubmit(page);
db.SubmitChanges();
}
}
But if you want to update an object, it is a tedious job.
You do the same flow,
you get the object,
link it to your form,
post it, but THEN !!!
because it went outside your datacontext, you have to reload the object from the datacontext,
transfer all the variables and save it,
this is a little complex explained so I give an example:
To update an object that you modified in a form:
public static void Update(Page page)
{
using (var db = new CardReaderDataContext())
{
var _page = db.Pages.Where(p => p.Guid == page.Guid).Single();
_page.ModificationDate = DateTime.Now;
_page.Title = page.Title;
_page.Description = page.Description;
_page.Content = page.Content;
_page.Keywords = page.Keywords;
_page.Name = page.Name;
_page.WTLang = page.WTLang;
_page.WTSKU = page.WTSKU;
_page.WTTi = page.WTTi;
_page.WTUri = page.WTUri;
_page.URL = UrlHelper.CreateValidSeoUrl(page.Name, "-");
// _page.Order = GetMaxOrderByMenuGuid(page.MenuGuid);
db.SubmitChanges();
}
}
I don't know if it is clear, if it isn't comment me, I will edit
I think you're looking for DataContext.Attach, but you can only use that with linqtosql objects that have been serialised/deserialised.
Have a read of the answer to this question -
http://social.msdn.microsoft.com/forums/en-US/linqprojectgeneral/thread/384a1c03-3acf-43ef-9a25-b84f93025e63/
"It's also not a good idea to even
attempt to fetch the old version. By
doing that you are in effect turning
off optimistic concurrency, so unless
you intended that this is a bad
approach. What you need to do is
round trip both the original state and
the current state of the object."

Resources