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

Trying to serialize [NotMapped] Entity Framework property for use by 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 个属性,但如果我们在其中投入太多时间,这可能最终会成为我们的行动方针。

提前致谢

在序列化方面,害你的是breeze提供的中间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 class.

中的 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 框架中有很多 internal/static/private/non-virtual 位让人不愉快。

不过,最终以 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 层使用,并且不需要任何额外的步骤来促进序列化。但是,您需要注意如何塑造模型元数据,因为任何 requirements/specs 都将反映在 Breeze 的客户端验证中。

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