尝试序列化[NotMapped] Entity Framework属性以供Breeze使用

时间:2015-08-19 20:45:37

标签: c# json entity-framework odata breeze

这是一个令人费解的问题,所以请耐心等待。我们目前在我们的服务器上使用Entity Framework 6.1.1和OData 5.6,以及客户端的Breeze JS 1.5.4。简而言之,我们在模型上获取[NotMapped]属性以将其序列化为json并传递给客户端时遇到问题。

这是我们的模型:

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

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

因为我们使用的是OData,而不是通过默认的JsonMediaTypeFormatter进行序列化,所以它会通过OdataMediaTypeFormatter完全忽略具有[NotMapped]属性的任何内容。 我们可以通过手动将属性添加到modelBuilder来解决此问题。然而,当尝试与Breeze集成时,这成为一个问题,因为它们有自己的自定义EdmBuilder,必须用于保存可导航属性之类的东西,并且我们不能使用标准ODataConventionModelBuilder。此自定义构建器似乎不允许对模型进行任何级别的控制。是否可以强制OData正确序列化这些属性,并保留与Breeze一起投诉的元数据?有没有人尝试过类似的东西?

旁注: 我们试图避免在数据库中存储或只是为这些数据创建虚拟列,因为我们需要其中的5个属性,但如果我们将更多的时间用于此,这可能会成为我们的行动方案。

先谢谢

1 个答案:

答案 0 :(得分:2)

在序列化方面,令你害怕的是由微风提供的中间EdmBuilder。请参阅:https://github.com/Breeze/breeze.server.labs/blob/master/EdmBuilder.cs

由于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.

您可以尝试使用自定义序列化程序的解决方案,类似于本文中所示的内容。 Using OData in webapi for properties known only at runtime

自定义序列化程序看起来大致如此(注意:这不起作用..继续阅读,下面......)

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;
    }
}

问题在于底层的ODataJsonLightPropertySerializer会在模型尝试写入时检查模型是否存在。它调用Microsoft.Data.OData.WriterValidationUtils类中的ValidatePropertyDefined方法。

internal static IEdmProperty ValidatePropertyDefined(string propertyName, IEdmStructuredType owningStructuredType)

这会因运行时异常而失败:

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

底线是需要在模型中定义属性才能对其进行序列化。你可以想象重写序列化层的大部分内容,但OData框架中有很多内部/静态/私有/非虚拟位使这种情况令人不快。

最终,解决方案最终会以Breeze强制您生成模型的方式呈现。假设代码优先实现,您可以将其他模型元数据直接注入到EntityFramework生成的XmlDocument中。采用Breeze EdmBuilder中的方法,稍作修改:

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);
        }
    }
}

这会将属性放入OData层要使用的元数据中,并且不需要任何其他步骤来促进序列化。但是,您需要注意如何塑造模型元数据,因为任何需求/规范都将反映在Breeze中的客户端验证中。

我已在Breeze提供的ODataBreezejs示例中验证了此方法的CRUD操作。 https://github.com/Breeze/breeze.js.samples/tree/master/net/ODataBreezejsSample