Loading Breeze Navigation Properties offline - javascript

I am using the latest versions of Angular, breeze, EF.
I am constructing a complex object on a client called a Quote which is added to a job. This has a QuoteMeasure added to it. One of the properties of QuoteMeasure is a navigation property called measure:
var quote = em.createEntity("Quote", { id: breeze.core.getUuid() }),
quoteMeasure,
measure;
measure = _getMeasureFromLookups(4);
quoteMeasure = em.createEntity("QuoteMeasure", { id: breeze.core.getUuid(), quoteId: quote.id });
I have tried the following which executes a query to the server
quoteMeasure.measureId = measure.id;
quoteMeasure.entityAspect.loadNavigationProperty("measure").then(function () {
console.log(quoteMeasure.measure);
});
quote.quoteMeasures.push(quoteMeasure);
job.quotes.push(quote);
to url /Breeze/Data/Measure?$filter=Id%20eq%204&
which does not exist. I would ideally like to set the navigation property manually as it is static data and previously obtained from a breeze query lookups on the server:
[HttpGet]
public object Lookups()
{
var measures = UnitOfWork.MeasureRepository.Get(null, q => q.OrderBy(m => m.Ordinal)).ToList();
return new { measures = measures };
}
This is what the function _getMeasureFromLookups does, it looks up the previously stored measure. I would like to do assign it this way:
quoteMeasure.measure = measure;
But I get the following meaningless error on the client:
Error: A is undefined M#//llhst/X/Scripts/breeze.min.js:1 d/f.set#//llhst/X/Scripts/breeze.min.js:5 _createNewQuote#//llhst/X/Scripts/app/services/jobService.js:76
This I assume is because a full tree of objects has been downloaded via the lookup rather than an individual measure entity. In http://www.breezejs.com/documentation/navigation-properties there is a section on 'Omitting navigation properties' but then it neglects to tell you how to do this.
So my question is what is best practise for loading navigation property data offline? How can I modify the sample above so that it works?

If I understand your requirement correctly, you should be able to construct your quote and quoteMeasure entities as follows:
var quote = em.createEntity("Quote", { id: breeze.core.getUuid() });
//the assignment quoteId: quote.id is the same as quote.quoteMeasures.push(quoteMeasure)
//you don't need to add it again to the collection
var quoteMeasure = em.createEntity("QuoteMeasure", { id: breeze.core.getUuid(), quoteId: quote.id });
var measure = _getMeasureFromLookups(4);
quoteMeasure.measure = measure;
//or
//quoteMeasure.measureId = measure.id
//your _getMeasureFromLookups should look something like this
function _getMeasureFromLookups(measureId) {
//getEntityByKey will look up Measure from client cache
return em.getEntityByKey('Measure', measureId);
}
Calling loadNavigationProperty will initiate a query to the server.
The 'Omitting navigation properties' section actually tells you how you can omit the principal side of the association. So for example, to apply it to your EF model, if you don't want a Quote to be able to navigate to all QuoteMeasures, you can do the following:
//EF Model on Server
public class Quote {
//Simply remove or comment this collection navigation property
//public virtual ICollection<QuoteMeasure> QuoteMeasures { get; set; }
}
Hope this helps.

Seems the problem was the ommission of these statements:
Configuration.ProxyCreationEnabled = false;
Configuration.LazyLoadingEnabled = false;
Not having this caused the preloading of not only the navigation properties but all of theirs as well which caused the obscure error I noted above. No other modifications were required to get the code working.

EntityAspect.loadNavigationProperty() always makes a request to the server. If you want properties be loaded without making a separate request, do Eager loading with EF.
If you have several properties which are null when breeze fetches them, and you don't want to make several loadNavigationProperty calls, use EntityQuery.expand() method. You can list any properties you need to be loaded

Related

Breeze **expand** vs. EntityFramework **include** for eager loading of navigation properties

a) In order to load a navigation property of an entity with EntityFramework on server side I can use include:
public virtual IQueryable<TEntity> All(){
IQueryable<TEntity> initialQuery = Context.Set<TEntity>();
IQueryable<TEntity> extendedQuery = initialQuery.Include('MyNavigationProperty');
return extendedQuery;
}
Also see https://msdn.microsoft.com/en-us/magazine/hh205756.aspx
b) Breeze allows to load navigation properties on client side with expand:
var navQuery = breeze.EntityQuery.from('MyEntity')
.expand('MyNavigationProperty');
Also see https://breeze.github.io/doc-js/navigation-properties.html
=> Should I use both of those options if I want to load a navigation property? If not, what are the pros and cons of defining the eagerly loaded navigation properties on server or client side? Are there performance or security issues I have to consider when choosing one of the options?
Is it for example possible to hack the client code to retrieve more navigation properties than the original code would load?
Here someone states that using either include or expand would be enough:
Breeze does not expand a navigation property
However, I am still unsure how/when to use them.
It seems to be like this (please correct me or add further information if you want):
a) The Breeze .expand option is enabled by default. In order to disable or restrict it the annotation EnableBreezeQuery can be applied in a domain controller:
[HttpGet]
[AllowAnonymous]
[EnableBreezeQuery(MaxExpansionDepth = 0)]
public IQueryable<Network> NetworkForEntryPageBy()
{
return _unitOfWork.NetworkRepository.All();
}
This will forbid the client to use breeze expand.
Also see
https://github.com/Breeze/breeze.server.net/issues/12
https://github.com/Breeze/breeze.server.net/blob/master/Tests/Test.WebApi2.EF6/Controllers/NorthwindIBModelController.cs
https://github.com/IdeaBlade/Breeze/pull/35
b) If breeze expand is enabled, it can be used to override the server side includes. I included for example the 'Pictures' navigation property on server side and expanded the 'Companies' navigation property on client side. I first expected that both navigation properties would be available. However, only the 'Companies' list is filled on client side:
[HttpGet]
[AllowAnonymous]
[EnableBreezeQuery(MaxExpansionDepth = 10)]
public IQueryable<Network> NetworkForEntryPageBy()
{
return _unitOfWork.NetworkRepository.All().Include('Pictures');
}
self.networksForEntryPage = function () {
var query = breeze.EntityQuery
.from(routeconfig.networksForEntryPageUrl).expand('Companies');
return self.executeQuery(query);
};
=> Pictures are empty
=> Companies are not empty and can be used by the client.

Filtering and only display certain information using Web API

I am developing a website that is mostly written in asp.net and Javascript and I am using a lot of ajax on it to retreive and display information from a SQL database.
To perform all these operations I am using Web API as the communication path to perform server related tasks.
There are quite a few places on my website I will only want to display certain information. For example, I have the following route of which I may consume: api/customers/orders/order/5
This would retreive data for Order #5. However, what if some places on my website only need to display the Order # or Order Description or something? and what happens if I want to filter the database a bit more, to perhaps only display orders with specific information. By using the above URL it would return everything about the order to the web browser and seems a bit unnecessary.
If I have several needs for different filters then I don't understand how the routing would work for as some bits would be optional.
Any recommendations would be greatly appreciated! Or perhaps i'm missing something!
Thanks
You can do something like following. You can create request filter like specified in below link.
public class RequestFilterAttribute : Attribute, IHasRequestFilter
{
#region IHasRequestFilter Members
public IHasRequestFilter Copy()
{
return this;
}
public int Priority
{
get { return -100; }
}
public void RequestFilter(IHttpRequest req, IHttpResponse res, object requestDto)
{
var query = req.QueryString["q"] ?? req.QueryString["query"];
var limit = req.QueryString["limit"];
var offset = req.QueryString["offset"];
var user = requestDto as QueryBase;
if (user == null) { return; }
user.Query = query;
user.Limit = limit.IsEmpty() ? int.MaxValue : int.Parse(limit);
user.Offset = offset.IsEmpty() ? 0 : int.Parse(offset);
}
#endregion
}
.Net WebAPI URI convention for advanced searching /filtering

breezejs v1.3.6 breaks my application

I've updated my client library and server web api dll to latest version.
Now whenever I do an expand on a query, I get that kind of error:
unable to locate property: Mandate on type: MandateHistory:#Dom.DirectDebit
with the query being :
var query = breeze.EntityQuery.from("MandatesHistory")
.where("Mandate.Id", "==", mandatId).expand("Mandate");
return manager.executeQuery(query.using(service));
If I downgrade to 1.3.3 (the client library only), everything works fine.
I would have like to try 1.3.4 or 1.3.5 but I can't find them on the website....
What has changed between 1.3.3 and 1.3.6 that could break my application ?
EDIT
THIS IS THE CODE CAUSING ISSUES :
In 1.3.6, in the function parseCsdNavProperty, the following code was added:
var constraint = association.referentialConstraint;
if (!constraint) {
// TODO: Revisit this later - right now we just ignore many-many and assocs with missing constraints.
return;
// Think about adding this back later.
//if (association.end[0].multiplicity == "*" && association.end[1].multiplicity == "*") {
// // many to many relation
// ???
//} else {
// throw new Error("Foreign Key Associations must be turned on for this model");
//}
}
Basically, for the navigation property MandateHistory.Mandate, there is no contraint found, so the code just return. This is the cause of my issue.
In version 1.3.3, there was no check on constraint because first there was the following check which returns false in my case (isScalar is false):
if (toEnd && isScalar) {
var constraint = association.referentialConstraint;
if (constraint) {
var principal = constraint.principal;
var dependent = constraint.dependent;
var propRefs;
if (csdlProperty.fromRole === principal.role) {
propRefs = toArray(principal.propertyRef);
} else {
propRefs = toArray(dependent.propertyRef);
}
// will be used later by np._update
fkNamesOnServer = propRefs.map(__pluck("name"));
}
}
Can the breeze team look into this ?
SOLUTION
Following Jay's suggestion, the .net model had to be changed in order to explicitly set the foreign key association between MandateHistory and Mandate:
public class MandateHistory
{
[ForeignKey("Mandate")]
public int Mandate_Id { get; set; }
public virtual Mandate Mandate { get; set; }
}
My guess is that you are missing referential constraints in your model. i.e. the Entity Framework thinks that you are not exposing foreign keys. See Foreign keys in the Entity Framework.
Breeze requires the foreign keys in order to perform it's automatic object linking logic.
This is also described here: Breeze navigation properties

Backbone Sub-Collections & Resources

I'm trying to figure out a Collection/Model system that can handle retrieving
data given the context it's asked from, for example:
Available "root" resources:
/api/accounts
/api/datacenters
/api/networks
/api/servers
/api/volumes
Available "sub" resources:
/api/accounts/:id
/api/accounts/:id/datacenters
/api/accounts/:id/datacenters/:id/networks
/api/accounts/:id/datacenters/:id/networks/:id/servers
/api/accounts/:id/datacenters/:id/networks/:id/servers/:id/volumes
/api/accounts/:id/networks
/api/accounts/:id/networks/:id/servers
/api/accounts/:id/networks/:id/servers/:id/volumes
/api/accounts/:id/servers
/api/accounts/:id/servers/:id/volumes
/api/accounts/:id/volumes
Then, given the Collection/Model system, I would be able to do things like:
// get the first account
var account = AccountCollection.fetch().first()
// get only the datacenters associated to that account
account.get('datacenters')
// get only the servers associated to the first datacenter's first network
account.get('datacenters').first().get('networks').first().get('servers')
Not sure if that makes sense, so let me know if I need to clarify anything.
The biggest kicker as to why I want to be able to do this, is that if the
request being made (ie account.get('datacenters').first().get('networks'))
hasn't be made (the networks of that datacenter aren't loaded on the client)
that it is made then (or can be fetch()d perhaps?)
Any help you can give would be appreciated!
You can pass options to fetch that will be translated to querystring params.
For example:
// get the first account
var account = AccountCollection.fetch({data: {pagesize: 1, sort: "date_desc"}});
Would translate to:
/api/accounts?pagesize=1&sort=date_desc
It is not quite a fluent DSL but it is expressive and efficient since it only transmits the objects requested rather than filtering post fetch.
Edit:
You can lazy load your sub collections and use the same fetch params technique to filter down your list by query string criteria:
var Account = Backbone.Model.extend({
initialize: function() {
this.datacenters = new Datacenters;
this.datacenters.url = "/api/account/" + this.id + '/datacenters';
}
});
Then from an account instance:
account.datacenters.fetch({data: {...}});
Backbone docs on fetching nested models and collections

webOS/Ares : read JSON from URL, assign to label

I've used the webOS Ares tool to create a relatively simple App. It displays an image and underneath the image are two labels. One is static, and the other label should be updated with new information by tapping the image.
When I tap the image, I wish to obtain a JSON object via a URL (http://jonathanstark.com/card/api/latest). The typcial JSON that is returned looks like this:
{"balance":{"amount":"0","amount_formatted":"$0.00","balance_id":"28087","created_at":"2011-08-09T12:17:02-0700","message":"My balance is $0.00 as of Aug 9th at 3:17pm EDT (America\/New_York)"}}
I want to parse the JSON's "amount_formatted" field and assign the result to the dynamic label (called cardBalance in main-chrome.js). I know that the JSON should return a single object, per the API.
If that goes well, I will create an additional label and convert/assign the "created_at" field to an additional label, but I want to walk before I run.
I'm having some trouble using AJAX to get the JSON, parse the JSON, and assign a string to one of the labels.
After I get this working, I plan to see if I can load this result on the application's load instead of first requiring the user to tap.
So far, this is my code in the main-assistant.js file. jCard is the image.
Code:
function MainAssistant(argFromPusher) {}
MainAssistant.prototype = {
setup: function() {
Ares.setupSceneAssistant(this);
},
cleanup: function() {
Ares.cleanupSceneAssistant(this);
},
giveCoffeeTap: function(inSender, event) {
window.location = "http://jonathanstark.com/card/#give-a-coffee";
},
jcardImageTap: function(inSender, event) {
//get "amount_formatted" in JSON from http://jonathanstark.com/card/api/latest
//and assign it to the "updatedBalance" label.
// I need to use Ajax.Request here.
Mojo.Log.info("Requesting latest card balance from Jonathan's Card");
var balanceRequest = new Ajax.Request("http://jonathanstark.com/card/api/latest", {
method: 'get',
evalJSON: 'false',
onSuccess: this.balanceRequestSuccess.bind(this),
onFailure: this.balanceRequestFailure.bind(this)
});
//After I can get the balance working, also get "created_at", parse it, and reformat it in the local time prefs.
},
//Test
balanceRequestSuccess: function(balanceResponse) {
//Chrome says that the page is returning X-JSON.
balanceJSON = balanceResponse.headerJSON;
var balanceAmtFromWeb = balanceJSON.getElementsByTagName("amount_formatted");
Mojo.Log.info(balanceAmtFromWeb[0]);
//The label I wish to update is named "updatedBalance" in main-chrome.js
updatedBalance.label = balanceAmtFromWeb[0];
},
balanceRequestFailure: function(balanceResponse) {
Mojo.Log.info("Failed to get the card balance: " + balanceResponse.getAllHeaders());
Mojo.Log.info(balanceResponse.responseText);
Mojo.Controller.errorDialog("Failed to load the latest card balance.");
},
//End test
btnGiveCoffeeTap: function(inSender, event) {
window.location = "http://jonathanstark.com/card/#give-a-coffee";
}
};
Here is a screenshot of the application running in the Chrome browser:
In the browser, I get some additional errors that weren't present in the Ares log viewer:
XMLHttpRequest cannot load http://jonathanstark.com/card/api/latest. Origin https://ares.palm.com is not allowed by Access-Control-Allow-Origin.
and
Refused to get unsafe header "X-JSON"
Any assistance is appreciated.
Ajax is the right tool for the job. Since webOS comes packaged with the Prototype library, try using it's Ajax.Request function to do the job. To see some examples of it, you can check out the source code to a webOS app I wrote, Plogger, that accesses Blogger on webOS using Ajax calls. In particular, the source for my post-list-assistant is probably the cleanest to look at to get the idea.
Ajax is pretty much the way you want to get data, even if it sometimes feels like overkill, since it's one of the few ways you can get asynchronous behavior in JavaScript. Otherwise you'd end up with code that hangs the interface while waiting on a response from a server (JavaScript is single threaded).

Categories

Resources