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