65.9K
CodeProject 正在变化。 阅读更多。
Home

Entity Framework 和 Enums(枚举)aka EFExtensions

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2011 年 8 月 23 日

CPOL

7分钟阅读

viewsIcon

17173

Entity Framework 对枚举的支持大概是最受期待的功能了,而且它居然不支持,这很奇怪。本文将展示一种使用模板来实现它的方法。

您想如何编写它

User foundUser = context.Users.FirstOrDefault(x =>
    x.Username == "OneUsername" &&
    x.CountryCode == CountryCodeEnum.UK &&
    x.AccessLevel == AccessLevelEnum.Admin &&
    x.Active);

CountryCode 的类型为 stringAccessLevelint,而 Activestring 类型,可以取 “Y” 或 “N”。

这里进行了两个重要的演示。首先是 enum 的支持,它可以是数值类型或 string 类型。其次,对 Oracle 数据库中可能存在的非标准布尔值(“Y” 和 “N”)的支持。

幕后

如果您对实现原理不太感兴趣,可以跳到 先决条件

在我上一篇文章中,我描述了如何添加对自定义布尔值(如“Y”和“N”)的支持,但我并没有真正解释它是如何工作的。

对于我们希望进行自定义映射的每个属性,我们都会创建一个复杂类型,该属性将成为一个复杂属性。新复杂类型的单个属性必须按照约定重命名为 Value,以便我们可以为其创建模板并通过反射轻松找到它(如果您愿意,也可以称之为硬编码)。

POCO 模板被调整为在生成的类中为复杂类型插入自定义代码。这些代码处理到 enum 的隐式转换,进行复杂类型与 enum 比较的操作符,重写 object 类的方法,辅助方法等。模板会检查 Entity Developer 在复杂类型上设置的属性。

对于自定义布尔值映射,POCO 模板被调整为检查复杂类型的 DbBool 名称,因此这里不需要额外的属性。

最重要的是,ObjectSet<> 被包装在一个 ObjectSetWrapper 中,它将在表达式树中,用实际的 Value 属性替换直接使用复杂类型的成员属性,例如:x.CountryCode => x.CountryCode.Value。对于布尔值,它将是:x.Active => x.Active.Value == “Y”

还有一个 EnumMapper,它可以处理 enum 与将替换在表达式树中的值之间的转换。为了支持 string 值,我创建了一个新属性,用于 enum

public enum CountryCodeEnum
{
    [EnumValue(Value = "US")]
    USA,
    [EnumValue(Value = "UK")]
    UK
}

EnumMapper 在将 enum 映射到值时会检查该属性,如果找到该属性,则设置 string 值,如果没有找到属性,则设置数值。

我希望这涵盖了所有重要方面。

必备组件

不幸的是,我找不到任何优雅地处理 EF 默认设计器的方法,所以我选择了 Entity Developer,来自 devart.com。它还捆绑了 dotConnect 产品,一个适用于各种数据库的 ADO.NET 提供程序。我使用它是因为它支持模型中的属性。MSSQL 有免费版本,但对您可以导入的实体数量有限制。我建议您从一开始就使用 Entity Developer 来创建您的模型,因为我在打开用 EF 默认设计器创建的模型时遇到了一些兼容性问题。

第二个先决条件是,您必须使用 Visual Studio 默认 T4 模板生成的 POCO 对象。否则(例如,如果您使用的是 Entity Developer 中的模板),您就需要自己调整 EFExtensions 附带的包含模板。

第三,从 github 这里下载最新版本的 EFExtensions 库。

动手实践

我们已经准备好了一切,现在让我们开始吧。首先,将下载的 EFExtensions 库导入到定义您模型的项目中。或者,如果您打算修改它,可以获取项目源代码并引用它。

在 Entity Developer 中打开您的模型。转到 Model -> Settings。在左侧,有一个树状导航,选择 Attributes。现在点击 Add.. 并找到 EFExtensions 程序集(如果您选择使用带源代码的项目,请在您的 bin\debug 目录中搜索它)。找到它后,点击 OK。取消选中 EFExtensions.EnumValueAttribute,因为它不需要。您应该看到类似这样的内容

接下来,转到您想要支持 enum 的属性。右键单击,选择 Migrate..,新窗口中的默认选择是 New complex type,这正是我们需要的。输入一个名称,我建议您设置一个约定,将所有要支持 enum 的复杂类型命名为 “Db<Name>Enum”。

点击 OK 后,属性将被重命名为复杂类型的名称,您不需要这样做,所以将其重命名回来。在 Model Explorer 的 Complex Types 下,找到您刚刚创建的类型,并将其单个属性的名称更改为 Value,请将此视为约定。对我来说,它看起来是

选择复杂类型,然后在 Properties 面板中找到 Attributes 属性。选择它,然后点击 (Collection) 右侧的小按钮,在新窗口中选择 DbEnumAttribute,然后点击指向右侧的箭头按钮,这样它就会被添加到 Selected Attributes 列表中。现在在 Properties 列表中,设置 Target Enum Type 的完整名称(包括完整的命名空间)。这就是您想要支持您复杂属性的 enum。在我的例子中,它是 Experimental.CountryCodeEnum。在图像中,您可以看到需要操作的箭头。

点击 OK 后,您可以继续处理下一个想要支持 enum 或自定义布尔值的属性。

要将属性映射到映射到 Y 和 N 的自定义布尔值,您只需创建一个名为 DbBool 的复杂类型,并带有一个也名为 Value 的单个属性。这里不需要添加属性。

如果您需要将实体中的更多字段映射到相同的复杂类型,请选择 Migrate..,然后选择 Existing complex type。

变得更脏

修改模型以支持 enum 很好,但您可以稍后完成。现在,我们必须深入 T4 模板,以正确生成复杂类型的代码,并自动包装 ObjectSets。如 先决条件 部分所述,它支持 Visual Studio 中的默认 POCO 模板。对于其他模板的支持,您可能需要修改 EFExtensions.ttinclude 并进行调整。

EFExtensions 文件夹中,找到 EFExtensions.ttinclude 文件并将其复制到您的 POCO 模板目录中。您不必将其包含在项目中,只需让它在同一个文件夹中。如果这不起作用,请尝试将其移动到项目的根文件夹。

使用默认 POCO 模板,您有 2 个 .tt 文件,一个以 .Context.tt 结尾。打开该文件并找到

using System;
using System.Data.Objects;
using System.Data.EntityClient;

添加 using EFExtensions;。接下来,替换

<#=Accessibility.ForReadOnlyProperty(entitySet)#> 
ObjectSet<<#=code.Escape(entitySet.ElementType)#>> <#=code.Escape(entitySet)#>
{
    get { return <#=code.FieldName(entitySet) #>  ?? 
    (<#=code.FieldName(entitySet)#> = 
    CreateObjectSet<<#=code.Escape(entitySet.ElementType)#>>
    ("<#=entitySet.Name#>")); }
}
private ObjectSet<<#=code.Escape(entitySet.ElementType)#>> 
<#=code.FieldName(entitySet)#>;

<#=Accessibility.ForReadOnlyProperty(entitySet)#> 
ObjectSetWrapper<<#=code.Escape(entitySet.ElementType)#>> 
<#=code.Escape(entitySet)#>
{
    get { return <#=code.FieldName(entitySet) #>  ?? 
    (<#=code.FieldName(entitySet)#> = 
    new ObjectSetWrapper<<#=code.Escape(entitySet.ElementType)#>>
    (CreateObjectSet<<#=code.Escape(entitySet.ElementType)#>>
    ("<#=entitySet.Name#>"))); }
}
private ObjectSetWrapper<<#=code.Escape
(entitySet.ElementType)#>> <#=code.FieldName(entitySet)#>;

现在打开另一个文件。在最顶部,找到

<#@ template language="C#" 
debug="false" hostspecific="true"#>
<#@ include file="EF.Utility.CS.ttinclude"#><#@ 

添加 EFExtensions.ttinclude

<#@ template language="C#" 
debug="false" hostspecific="true"#>
<#@ include file="EF.Utility.CS.ttinclude"#>
<#@ include file="EFExtensions.ttinclude"#><#@ 

找到写入复杂类型类名的那一行

<#=Accessibility.ForType(complex)#> partial class <#=code.Escape(complex)#>

添加代码以在复杂类型具有 DbEnumAttribute 属性时插入接口

<#=Accessibility.ForType(complex)#> partial class 
<#=code.Escape(complex)#> <#=CheckDbEnum(complex) ? 
": " + GetDbEnumInterface(complex) : ""#>

现在向下滚动并找到包含 region.End(); 的行,然后在之后是 EndNamespace(namespaceName); 类似这样的

    region.End();
#>
}
<#
    EndNamespace(namespaceName);
} 

region.End(); 之后立即插入 WriteDbBoolSupport(complex);WriteEnumSupport(complex);

    region.End();
 
    WriteDbBoolSupport(complex);
    WriteEnumSupport(complex);
#>
}
<#
    EndNamespace(namespaceName);
}

这里将插入额外代码。您现在可以保存并关闭文件了。

差不多了

这样,复杂类型就被创建了,自定义代码正在生成……还有什么?啊,是的,当您想为 enum 使用 string 值时,就像我为 CountryCodeEnum 所做的那样,UK 对应 “UK”,USA 对应 “US”,所以它不是 ToString() 映射。为了获得这些 string 值,请在每个 enum 常量上添加 EnumValueAttribute(Value = “您想要的值”)。让我们回顾一下我之前的例子

public enum CountryCodeEnum
{
    [EnumValue(Value = "US")]
    USA,
    [EnumValue(Value = "UK")]
    UK
}

如果未设置属性,则值将与执行 (int)CountryCodeEnum.USA 相同。如果您有 enum 在另一个项目中,不要忘记添加对 EFExtensions 的引用。

让我们来谈谈用法

现在我的 User 实体具有可以赋值和比较 enum 或布尔值的复杂属性,我还能做什么,而这是 Entity Framework 开箱即用无法做到的呢?那么,怎么样

User newUser = new User
{
    Active = true,
    CountryCode = CountryCodeEnum.UK,
    Username = "OneUsername",
    AccessLevel = AccessLevelEnum.User
}

您已经看到了 CountryCodeEnum 的样子,现在是 AccessLevelEnum

public enum AccessLevelEnum
{
    Deny = 0,
    User = 1,
    Admin = 99
}

获取所有非活动用户

IEnumerable inactiveUsers = context.Users.Where(x => !x.Active);

或者……所有来自 UK 的用户

IEnumerable inactiveUsers = context.Users.Where(x => x.CountryCode == CountryCodeEnum.UK);

而且,我还可以将其与 string 值进行比较

IEnumerable inactiveUsers = context.Users.Where(x => x.CountryCode.Value == "UK");

后记

恭喜您,现在您知道如何在 LINQ to Entities 中使用 enum 了。另外,我想提一下 Davy Landman 的帖子,它帮助我找到了解决方案的缺失部分:为 Entity Framework 中的实体添加枚举属性支持。但不幸的是,提出的解决方案仅支持数值类型的 enum。如果您发现任何错误,或想改进解决方案,可以在 github 上进行,欢迎贡献。

说了这么多,祝您玩得开心!

© . All rights reserved.