Handling calculated properties with breezejs and web api - breeze

I'm experimenting with BreezeJS with Web API using the BreezeControllerAttribute. How should calculated properties on an entity be exposed? The only way I've found to do this reliably is to create an intermediate DTO that inherits from the entity or use a projection. Normally I would use a readonly property for this scenario, but those appear to be ignored.

When Breeze maps JSON property data to entities, it ignores properties that it does not recognize. That's why your server class's calculated property data are discarded even though you see them in the JSON on the wire.
Fortunately, you can teach Breeze to recognize the property by registering it as an unmapped property. I'll show you how. Let me give some background first.
Background
Your calculated property would be "known" to the Breeze client had it been a property calculated by the database. Database-backed properties (regular and calculated) are picked up in metadata as mapped properties.
But in your case (if I understand correctly) the property is defined in the logic of the server-side class, not in the database. Therefore it is not among the mapped properties in metadata. It is hidden from metadata. It is an unmapped instance property.
I assume you're not hiding it from the serializer. If you look at the network traffic for a query of the class, you can see your calculated property data arriving at the client. The problem is that Breeze is ignoring it when it "materializes" entities from these query results.
Solution with example
The solution is to register the calculated property in the MetadataStore.
I modified the entityExtensionTests.js of the DocCode sample to include this scenario; you can get that code from GitHub or wait for the next Breeze release.
Or just follow along with the code below, starting with this snippet from the Employee class in NorthwindModel.cs:
// Unmapped, server-side calculated property
[NotMapped] // Hidden from Entity Framework; still serialized to the client
public string FullName {
get { return LastName +
(String.IsNullOrWhiteSpace(FirstName)? "" : (", " + FirstName)); }
}
And here is the automated test in entityExtensionTests.js
test("unmapped property can be set by a calculated property of the server class", 2,
function () {
var store = cloneModuleMetadataStore(); // clones the Northwind MetadataStore
// custom Employee constructor
var employeeCtor = function () {
//'Fullname' is a server-side calculated property of the Employee class
// This unmapped property will be empty for new entities
// but will be set for existing entities during query materialization
this.FullName = "";
};
// register the custom constructor
store.registerEntityTypeCtor("Employee", employeeCtor);
var fullProp = store.getEntityType('Employee').getProperty('FullName');
ok(fullProp && fullProp.isUnmapped,
"'FullName' should be an unmapped property after registration");
var em = newEm(store); // helper creates a manager using this MetadataStore
var query = EntityQuery.from('Employees').using(em);
stop(); // going async
query.execute().then(success).fail(handleFail).fin(start);
function success(data) {
var first = data.results[0];
var full = first.FullName();
// passing test confirms that the FulllName property has a value
ok(full, "queried 'Employee' should have a fullname ('Last, First'); it is "+full);
}
});
What you need to do is in this small part of the test example:
var yourTypeCtor = function () {
this.calculatedProperty = ""; // "" or instance of whatever type is is supposed to be
};
// register your custom constructor
store.registerEntityTypeCtor("YourType", yourTypeCtor);

Related

Breeze 1-To-Many Unidirectional Navigation Property not Populated

Breeze Version: 1.5.3
I'm experiencing something similiar to some older questions on SO but it seems like this "bug" is reoccurring:
I have a 1-To-Many unidirectional navigation property which is not populated. I have checked the metadata and the response from the server. I've even debugged into breeze and the node (or rawEntity) seems to be perfect.
I've tried to track it down and came to the conclusion, that it happens, because no "inverse"-Property is found for my Navigation Property and the mergeRelatedEntities-Function returning without updating the target Entity:
function mergeRelatedEntities(mc, navigationProperty, targetEntity, rawEntity) {
var relatedEntities = mergeRelatedEntitiesCore(mc, rawEntity, navigationProperty);
if (relatedEntities == null) return;
var inverseProperty = navigationProperty.inverse;
if (!inverseProperty) return;
var originalRelatedEntities = targetEntity.getProperty(navigationProperty.name);
originalRelatedEntities.wasLoaded = true;
relatedEntities.forEach(function (relatedEntity) {
if (typeof relatedEntity === 'function') {
mc.deferredFns.push(function () {
relatedEntity = relatedEntity();
updateRelatedEntityInCollection(relatedEntity, originalRelatedEntities, targetEntity, inverseProperty);
});
} else {
updateRelatedEntityInCollection(relatedEntity, originalRelatedEntities, targetEntity, inverseProperty);
}
});
}
Older Posts:
Non scalar navigation properties are not populating with "nodb" conception
and
Breeze (1.4.5) unidirectional one-to-many: navigation collection not populated
Edited 11. May 2015
Okay I start to understand what Ward meant with the unmapped properties (by finding a similar question from 2 years ago: Handling calculated properties with breezejs and web api)
What I have so far:
function iUIConfigConstructorTool() {
this.ConfigToCurrentUSetting = null;
};
function iUIConfigConstructorAppl() {
this.ConfigToCurrentUSetting = null;
};
function iUIConfigConstructorWidget() {
this.ConfigToCurrentUSetting = null;
};
function iUIConfigInitializer(uiConfigObject) {
// initializing other properties
};
this.manager.metadataStore.registerEntityTypeCtor("Tool", iUIConfigConstructorTool, iUIConfigInitializer);
this.manager.metadataStore.registerEntityTypeCtor("Appl", iUIConfigConstructorAppl, iUIConfigInitializer);
this.manager.metadataStore.registerEntityTypeCtor("Widget", iUIConfigConstructorWidget, iUIConfigInitializer);
This does what I want. Is there a way, to do this over the metamodel from the server? Because I define my calculated properties on the server and the metamodel is delivered by the server, I don't want to change the client-side implementation if I add a new Navigation-Property. So I'd need something like a flag in the metamodel to tell breeze, that this property needs to be filled as it comes over the wire without ForeignKeys etc.
Maybe in other words: We are doing "sub queries" on the server side (e.g. find Customers with it's Orders but only up to a certain Date) for each queried object and deliver this to breeze (in a separate property than the real orders-property of the Customer). Our problem is: How do we unpack this sub-query because there is no direct connection in metadata but we need the connection for the logic.
Please update your question with:
how you obtained/created metadata
metadata for the two endpoints (just the nav props, pk prop, and fk props will do)
the exact query expression
Of course a repro in plunker would be most welcome.
update 7 May 2015
If I understand your comment correctly, the navigation property (properties) in question are to be maintained by you (w/ server-supplied info), not by Breeze.
That leads me to suggest that you maintain them as unmapped properties rather than mapped, navigation properties. Does that make sense?

MvcSiteMapProvider ISiteMapBuilder in conjunction with IDynamicNodeProvider

I'm using MvcSiteMapProvider 4.4.3 to dynamically build a sitemap from the database. I'm following this article: https://github.com/maartenba/MvcSiteMapProvider/wiki/Multiple-Sitemaps-in-One-Application because I'm using multiple sitemaps.
This works and this is the basic structure which is returned:
Home
News
Products
About
Contact
One of the nodes (/Products) should be dynamically populated again based on different data. So for this I need a IDynamicNodeProvider implementation on the /Productsnode? (please correct me if i'm wrong?)
Anyway, I think I do need the above. Documentation shows ways to do this on a node defined in XML and on a node defined using attributes on controller actions, but not 'manually' in a ISiteMapBuilder. So if I set the .DynamicNodeProvider property of the ISiteMapNode instance it doesn't seem to get instantiated... The .HasDynamicNodeProvider property also returns false.
Looking at the source, i see PluginProvider-stuff which is related to DynamicNodeProviderStrategy and there you go, they've lost me...
How do I create a ISiteMapNode for "/Products" in my ISiteMapBuilder so that it's descendents (/Products/Cat and /Products/Cat/Product) are dynamically loaded from the database?
You can do this with ISiteMapBuilder, but you are probably better off instead implementing ISiteMapNodeProvider. The reason is because adding the nodes to the SiteMap must be done at the very end of the process after all nodes have been instantiated to ensure that every node is correctly mapped to a parent node (except of course, the root node which doesn't need a parent). This was the major design change that was done in 4.3.0.
The default SiteMapBuilder class is now already set up to ensure
The nodes are properly mapped to their parent nodes
There is only 1 root node
All nodes are added to the SiteMap
The visitors are executed last after the SiteMap is completely built
It dosen't make sense to add more than one ISiteMapBuilder instance because this makes it possible to circumvent this important logic. Therefore, it is best if you do not implement ISiteMapBuilder, but instead implement ISiteMapNodeProvider.
The SiteMapBuilder class takes an ISiteMapNodeProvider as a dependency through its constructor. You can use the CompositeSiteMapNodeProvider class to handle multiplicity on this interface so you can add more than one ISiteMapNodeProvider implementation, if needed.
The ISiteMapNodeProvider interface looks like this:
public interface ISiteMapNodeProvider
{
IEnumerable<ISiteMapNodeToParentRelation> GetSiteMapNodes(ISiteMapNodeHelper helper);
}
There is just 1 method to implement. In addition, many of the common (but optional) services are injected through the interface automatically from the SiteMapBuilder class through ISiteMapNodeHelper.
This class is at a lower level than IDynamicNodeProvider. You are interacting with ISiteMapNode directly, but all interaction with the SiteMap class is handled by SiteMapBuilder. The ISiteMapNode is wrapped in a ISiteMapNodeToParentRelation instance, which is just there to ensure that its parent node key can be tracked until the time it is added to the SiteMap object.
Your SiteMapNodeProvider should look something like this:
public class CustomSiteMapNodeProvider
: ISiteMapNodeProvider
{
private readonly string sourceName = "CustomSiteMapNodeProvider";
#region ISiteMapNodeProvider Members
public IEnumerable<ISiteMapNodeToParentRelation> GetSiteMapNodes(ISiteMapNodeHelper helper)
{
var result = new List<ISiteMapNodeToParentRelation>();
using (var db = new DatabaseContextClass())
{
foreach (var category in db.Categories.ToList())
{
var categoryRelation = this.GetCategoryRelation("Products", category, helper);
result.Add(categoryRelation);
}
foreach (var product in db.Products.Include("Category"))
{
var productRelation = this.GetProductRelation("Category_" + product.CategoryId, product, helper);
result.Add(productRelation);
}
}
return result;
}
#endregion
protected virtual ISiteMapNodeToParentRelation GetCategoryRelation(string parentKey, Category category, ISiteMapNodeHelper helper)
{
string key = "Category_" + category.Id;
var result = helper.CreateNode(key, parentKey, this.sourceName);
var node = result.Node;
node.Title = category.Name;
// Populate other node properties here
// Important - always set up your routes (including any custom params)
node.Area = "MyArea"; // Required - set to empty string if not using areas
node.Controller = "Category"; // Required
node.Action = "Index"; // Required
node.RouteValues.Add("id", category.Id.ToString());
return result;
}
protected virtual ISiteMapNodeToParentRelation GetProductRelation(string parentKey, Product product, ISiteMapNodeHelper helper)
{
string key = "Product_" + product.Id;
var result = helper.CreateNode(key, parentKey, this.sourceName);
var node = result.Node;
node.Title = product.Name;
// Populate other node properties here
// Important - always set up your routes (including any custom params)
node.Area = "MyArea"; // Required - set to empty string if not using areas
node.Controller = "Product"; // Required
node.Action = "Index"; // Required
node.RouteValues.Add("id", product.Id.ToString());
node.RouteValues.Add("categoryId", product.CategoryId.ToString()); // Optional - use if you have a many-to-many relationship.
return result;
}
}
The above example assumes that you have added a node by other means that has the key set to "Products", which all categories will be children of. You can of course adjust this to meet your needs.
It is typically best if you only implement this interface 1 time and use a single database connection to load the entire SiteMap. You can always refactor this into multiple classes that each handle a single table on your side of the interface to separate concerns. But it is generally best if you put all of the key mapping logic between related entities together to make it easier to maintain.
For additional examples of implementations of this interface, see XmlSiteMapNodeProvider and ReflectionSiteMapNodeProvider.

breezejs: how to access enum metadata

I've noticed that in the metadata there's an object entityType but also an object enumType.
We use manager.metadataStore.getEntityType() to access the metadata of an Entity.
How can we do it for a given enum ? How would I create the enum on the client side out of the metadata ?
Also, when I assign an enum value to a property, I'd like to to it by name instead of by value.
For instance, assuming that Status is of type myEnum:
myEntity.Status = myEnum.Valid;
instead of
myEntity.Status = 1;
Does breeze have any helper function to access the values of an enum ?
This issue is still open as I write. But you might want to take a look at the work-around described in the answer to this SO question.
I am assuming that you are talking about data properties that are defined as .NET enums on the server, and you want additional metadata about these properties to be made available on the Breeze client.
Unfortunately, Breeze does not yet support any metadata on enum types other than the name of the .NET type backing the enum value. This is the 'enumType' property that will appear on any dataProperty that is backed by an .NET Enum on the server. (We do need to document this better)
Please add a feature request for this to the Breeze User Voice. It's a good idea and we do take these suggestions very seriously.
Well this is not exact solution to your question but definitely can help people who are generating metadata offline.
I am using NancyFx(No EF) + Breeze + AngularJS for my web project and generating breeze metadata offline(using EF methods at development) and then using it in js file.
I also encountered similar situation where I want to get all Enum values to bind dropdowns and to display EnumName corresponding to EnumValue(Id). I searched over net but there was not much as per my scenario.
So I have written raw JS methods
1. To extract all enums and their values(Id & Name) in a JS dictionary(associated array) from metadata.
var enumDictionary = {};
JSON.parse(window.app.metadata).schema.enumType.forEach(function (enumType) {
var newEnumValues = [];
enumType.member.forEach(function (enumValue) {
var newEnumValue = { id: enumValue.value, name: enumValue.name };
newEnumValues.push(newEnumValue);
});
enumDictionary[enumType.name] = newEnumValues;
});
I created a method to get all enum values for a specific enum. This will be used for binding a dropdown.
function GetEnumDictionary(enumName) {
return enumDictionary[enumName];
}
Another method I created to get specific Enum name on basis of value.
function GetEnumDictionaryValue(enumName, enumValueId) {
var result = null;
enumDictionary[enumName].some(function (enumValue) {
if (enumValue.id == enumValueId) {
result = enumValue.name;
return;
}
});
return result;
}

breezejs entity query with select returning undefined

With the code below I am having an issue where not all the columns are return data in the data.results array. For example if col4 is null in the database for row 1 then data.results[0] does not contain an element for col4, but row 2 has a value then data.results[1] will contain the value for col4. I would like each return item in the array to contain all items with the database value or null. If null can't be returned then an empty string would do.
var query = new breeze.EntityQuery()
.from('mytable')
.where('col1', 'substringof', '2')
.select('col1,col2,col3,col4')
.orderBy('col1')
.take(200);
return _manager
.executeQuery(query)
.then(function (data) {
return data.results;
})
.fail(queryFailed);
}
By default breeze does not serialize null values in its JSON results. This was a deliberate choice to reduce the breeze payload over the wire. And.. this is not typically an issue with queries that return "entities". i.e. data for which breeze metadata exists. Because the properties are already defined on such entities.
But if you are returning an anonymous result as you are, then this can be a problem. You can obviously work around it because you know the properties that you are requesting and can update them after the query if they are not in the result.
But you can also change breeze's default configuration to accommodate this via the "BreezeConfig" class.
BreezeConfig enables customization of components supporting Breeze-related operations in the Web API. BreezeConfig defines default behaviors; you can substitute your own behaviors by deriving from it and overriding its virtual methods. Breeze.NET will discover your subclass among the assemblies referenced by your project and use it instead of BreezeConfig.
To use BreezeConfig to configure the Json.Net serializer with specific settings. You can replace those settings by writing a subclass of BreezeConfig that overrides the 'CreateJsonSerializerSettings' method as shown in this example:
public class CustomBreezeConfig : Breeze.WebApi.BreezeConfig {
protected override JsonSerializerSettings CreateJsonSerializerSettings() {
var baseSettings = base.CreateJsonSerializerSettings();
baseSettings.NullValueHandling = NullValueHandling.Include;
return baseSettings;
}
Hope this helps.

While Adding New Entity, Identity value is not generated

I am running into problem, where i extend the Entity to expose hasValidationError. Without that it works fine. Also i found that if I supply the ID before adding the entity it works fine as well. Why is the ID field not auto generating once the entity is extended on the client.
I am now using little different version of the code(i find it more intuitive to extend the entity this way), but it still errors out in the same way.
var Country = function () {
console.log("Country initialized");
var self = this;
self.Country_ID = ko.observable("");
self.Country_Code = ko.observable("");
self.Country_Name = ko.observable().extend({
validation: {
validator: function (val, someOtherVal) {
return false;//val === someOtherVal;
},
message: 'Invalid Value!',
params: 5
}
});
var prop = ko.observable(false);
var onChange = function () {
var hasError = self.entityAspect.getValidationErrors().length > 0;
if (prop() === hasError) {
// collection changed even though entity net error state is unchanged
prop.valueHasMutated(); // force notification
} else {
prop(hasError); // change the value and notify
}
};
// observable property is wired up; now add it to the entity
self.hasValidationErrors = prop;
//dummy property to wireup event
//should not be used for any other purpose
self.hasError = ko.computed(
{
read: function () {
self.entityAspect // ... and when errors collection changes
.validationErrorsChanged.subscribe(onChange);
},
// required because entityAspect property will not be available till Query
// return some data
deferEvaluation: true
});
self.fullName = ko.computed(
function () {
return self.Country_Code() + " --- " + self.Country_Name();
});
};
store.registerEntityTypeCtor("Country", Country);
and then in the button click i am using the following code to create new entity.
var countryType = manager.metadataStore.getEntityType("Country");
var newCountry = countryType.createEntity();
//newCountry.Country_ID(200); //if i add this line no errors occurs
newCountry.Country_Code("India");
self.list.push(newCountry);
manager.addEntity(newCountry); // validation error occurs right after this line
self.selectedItem(newCountry);
self.list.valueHasMutated();
Entities will only get their own autogenerated key if the metadata for their type specifies that this is supported. i.e.
if (myEntityType.autoGeneratedKeyType === AutoGeneratedKeyType.Identity)
This setting means that the key property of the entity is automatically generated by the server, typically for an 'Identity' column on your database.
or
if (myEntityType.autoGeneratedKeyType === AutoGeneratedKeyType.KeyGenerated)
This setting means that you have a server side KeyGenerator that can generate the key for you.
By default, however, myEntityType.autoGeneratedKeyType will equal AutoGeneratedKeyType.None.
In either of the other two cases, breeze will generate a temporary key on the client and then fix it up after a save completes with a 'real' key generated on the server.
If you do not need this capability, then simply create your own ctor for your type that generates a unique key and set it there. see MetadataStore.registerEntityTypeCtor for more details on how to register your ctor.
We are planning on improving our documentation in this area but haven't yet gotten there. I hope this helps.
Perhaps there is nothing wrong at all. How do you know that the id generation is failing? Is it a negative number after adding the newCountry to the manager? It should be.
What is the validation error that you are getting? Does it relate to Country_ID? Perhaps you have a validation constraint (e.g., minimum value) on Country_ID?
The addhasValidationErrorsProperty entity initializer works as intended. I just added a teaching test to the DocCode sample (see "Can create employee after registering addhasValidationErrorsProperty initializer" in entityExtensionTests.js). We haven't deployed it as I write this but you can get it from GitHub.
It follows your example as best I can with the Northwind Employee entity which has an identity id (Employee_ID). The test shows adding the initializer that I wrote in the previous post (not as you may have rewritten it). It shows that the new Employee's id is zero before adding to the manager and becomes -1 after adding to the manager. The -1 is the temporary id of the new employee; it receives a permanent value after save.
The default validationOptions of an EntityManager are set so to validate an entity when it is attached (or added) to the manager. You can change that behavior to suit your needs.
The new employee is in an invalid state when created; it lacks the required First and Last name values (the test displays these errors). Therefore the hasValidationErrors observable becomes true after adding the new employee to the manager and the hasValidationErrors observable raises a change notification that a Knockout UI would hear; these test shows these two points.

Resources