Search code examples
c#jsonentity-frameworkodatabreeze

Trying to serialize [NotMapped] Entity Framework property for use by Breeze


This is a bit of a convoluted problem, so bear with me. We are currently using Entity Framework 6.1.1 on our server with OData 5.6, and Breeze JS 1.5.4 on the client side. In short, we're having issues getting [NotMapped] properties on our Model to serialize into json and get passed to the client.

Here is our Model:

public class Request 
{
    ...
    public int UserId { get; set; }

    [NotMapped]
    public string UserName {get; set; }
}

Because we're using OData, rather than being serialized through the default JsonMediaTypeFormatter, it goes through the OdataMediaTypeFormatter which completely ignores anything with the [NotMapped] attribute. We could work around this problem by manually adding the properties to the modelBuilder. However this becomes an issue when trying to integrate with Breeze, because they have their own custom EdmBuilder that must be used for things like navigable properties to be preserved, and we cannot use the standard ODataConventionModelBuilder. This custom builder doesn't seem to allow for any level of control over the models. Is it at all possible to force OData to properly serialize these properties and also keep metadata that's complaint with Breeze? Has anyone tried something similar before?

Side note: We're trying to avoid storing or just making dummy columns in the db for this data, seeing as we need 5 of these properties, but this may wind up being our course of action if we dump too much more time into this.

Thanks in Advance


Solution

  • In terms of serialization, what is hurting you is the intermediate EdmBuilder that is supplied by breeze. See: https://github.com/Breeze/breeze.server.labs/blob/master/EdmBuilder.cs

    Because of the limitations defined in the comments of the EdmBuilder.cs

    We need the EDM both to define the Web API OData route and as a source of metadata for the Breeze client.  The Web API OData literature recommends the System.Web.Http.OData.Builder.ODataConventionModelBuilder.
    That component is suffient for route definition but fails as a source of metadata for Breeze because (as of this writing) it neglects to include the foreign key definitions Breeze requires to maintain navigation properties of client-side JavaScript entities.
    This EDM Builder ask the EF DbContext to supply the metadata which satisfy both route definition and Breeze.
    You're only getting the metadata the EntityFramework chooses to expose.  This prevents the OData formatters/serializers from including the property - it's not mapped in the model metadata.
    

    You could attempt a solution with a custom serializer, similar to what is represented in this article. Using OData in webapi for properties known only at runtime

    A custom serializer would look roughly like this (Note: this DOES NOT work.. continue reading, below...)

    public class CustomEntitySerializer : ODataEntityTypeSerializer
    {
        public CustomEntitySerializer(ODataSerializerProvider serializerProvider) : base(serializerProvider) {    }
    
        public override ODataEntry CreateEntry(SelectExpandNode selectExpandNode, EntityInstanceContext entityInstanceContext)
        {
            ODataEntry entry = base.CreateEntry(selectExpandNode, entityInstanceContext);           
    
            Request item = entityInstanceContext.EntityInstance as Request;
            if (entry != null && item != null)
            {
                // add your "NotMapped" property here.
                entry.Properties = new List<ODataProperty>(entry.Properties) { new ODataProperty { Name = "UserName", Value = item.UserName} };
            }
            return entry;
        }
    }
    

    The trouble with this is that the underlying ODataJsonLightPropertySerializer checks the model for the existence of the property as it's attempting to write. It calls the ValidatePropertyDefined method in the Microsoft.Data.OData.WriterValidationUtils class.

    internal static IEdmProperty ValidatePropertyDefined(string propertyName, IEdmStructuredType owningStructuredType)
    

    This will fail you with the runtime exception:

    The property 'UserName' does not exist on type 'YourNamespace.Models.Request'
    . Make sure to only use property names that are defined by the type.","type":"Microsoft.Data.OData.ODataException"
    ,"stacktrace":" at Microsoft.Data.OData.WriterValidationUtils.ValidatePropertyDefined(String propertyName
    , IEdmStructuredType owningStructuredType)\r\n at Microsoft.Data.OData.JsonLight.ODataJsonLightPropertySerializer
    .WriteProperty(ODataProperty property, IEdmStructuredType owningType, Boolean isTopLevel, Boolean allowStreamProperty
    , DuplicatePropertyNamesChecker duplicatePropertyNamesChecker, ProjectedPropertiesAnnotation projectedProperties
    

    Bottom line is that the property needs to be defined in the model in order to serialize it. You could conceivably rewrite large portions of the serialization layer, but there are lots of internal/static/private/non-virtual bits in the OData framework that make that unpleasant.

    A solution is ultimately presented in the way Breeze is forcing you to generate the model, though. Assuming a code-first implementation, you can inject additional model metadata directly into the XmlDocument produced by EntityFramework. Take the method in the Breeze EdmBuilder, with some slight modifications:

    static IEdmModel GetCodeFirstEdm<T>(this T dbContext)  where T : DbContext
    {
        // create the XmlDoc from the EF metadata
        XmlDocument metadataDocument = new XmlDocument();
        using (var stream = new MemoryStream())
        using (var writer = XmlWriter.Create(stream))
        {
            System.Data.Entity.Infrastructure.EdmxWriter.WriteEdmx(dbContext, writer);
            stream.Position = 0;
            metadataDocument.Load(stream);
        }
    
        // to support proper xpath queries
        var nsm = new XmlNamespaceManager(metadataDocument.NameTable);
        nsm.AddNamespace("ssdl", "http://schemas.microsoft.com/ado/2009/02/edm/ssdl");
        nsm.AddNamespace("edmx", "http://schemas.microsoft.com/ado/2009/11/edmx");
        nsm.AddNamespace("edm", "http://schemas.microsoft.com/ado/2009/11/edm");
    
        // find the node we want to work with & add the 1..N property metadata
        var typeElement = metadataDocument.SelectSingleNode("//edmx:Edmx/edmx:Runtime/edmx:ConceptualModels/edm:Schema/edm:EntityType[@Name=\"Request\"]", nsm);
    
        // effectively, we want to insert this.
        // <Property Name="UserName" Type="String" MaxLength="1000" FixedLength="false" Unicode="true" Nullable="true" />
        var propElement = metadataDocument.CreateElement(null, "Property", "http://schemas.microsoft.com/ado/2009/11/edm");
        propElement.SetAttribute("Name", "UserName");
        propElement.SetAttribute("Type", "String");
        propElement.SetAttribute("FixedLength", "false");
        propElement.SetAttribute("Unicode", "true");
        propElement.SetAttribute("Nullable", "true");
    
        // append the node to the type element
        typeElement.AppendChild(propElement);
    
        // now we're going to save the updated xml doc and parse it.
        using (var stream = new MemoryStream())
        {
            metadataDocument.Save(stream);
            stream.Position = 0;
            using (var reader = XmlReader.Create(stream))
            {
                return EdmxReader.Parse(reader);
            }
        }
    }
    

    This will place the property into the metadata to be consumed by the OData layer and make any additional steps to promote serialization unnecessary. You will, however, need to be mindful of how you shape the model metadata, as any requirements/specs will be reflected in the client side validation in Breeze.

    I have validated the CRUD operations of this approach in the ODataBreezejs sample provided by Breeze. https://github.com/Breeze/breeze.js.samples/tree/master/net/ODataBreezejsSample