Handling calculated properties with breezejs and web api
Asked Answered
D

1

6

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.

Destrier answered 13/5, 2013 at 14:6 Comment(0)
R
14

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);
Rolandrolanda answered 14/5, 2013 at 1:3 Comment(4)
I did exactly as you described but the calculated property is not getting it's value from query result, it's always empty even when data is transmitted to the client. is there any hidden magic? the behavior you described as a comment is not taking place "//but will be set for existing entities during query materialization"!Luteolin
I just found out that the problem was the case miss match between the data and the defined client side property.this sample is working as it is expected.Luteolin
@Rolandrolanda it appears there is an issue when using camelCase naming convention. It tries to map yourType.CalcProperty not yourType.calcProperty. Everything else is camelCased but the server sends down Pascal Case and this seems to ignore it :(Mollee
Why do you think there is something wrong with camelCase convention. It does exactly what it says it will do: translate PascalCase server-side property names to client-side camelCase property names. Of course it will "misunderstand" a camelCase server property name; those don't exist in its domain. If they exist in your domain, write yourself a custom NamingConvention that meets your need. It's a really simple API and writing a custom NamingConvention is exactly what we'd hope/expect you to do in the situation you've described.Rolandrolanda

© 2022 - 2024 — McMap. All rights reserved.