如何让C#枚举与数据库中的表保持同步

10

这是一个有些简化的例子(我改变了它以隐藏实际代码)。 我有一个由数据库驱动的应用程序和一个正在独立开发的小工具,旨在与应用程序配合使用。有一个表定义了一个枚举,但它可能随时间而发生变化。假设某个应用程序(医学?)需要精确跟踪人的性别。

按id从sex表中选择*;

id | mnemonic | description
0  | U        | Unknown sex
1  | M        | Male
2  | F        | Female
3  | T        | Trans-gender

我的 C# 枚举:

public enum SexType
{
    /// <summary>Unknown sex.</summary>
    [Description("U")]
    Unknown = 0,

    /// <summary>Male sex.</summary>
    [Description("M")]
    Male = 1,

    /// <summary>Female sex.</summary>
    [Description("F")]
    Female = 2

    /// <summary>Trans-gender sex.</summary>
    [Description("T")]
    TransGender = 3,
}

在这里,我不应该假设ID是连续的序列;即使看起来是这样,这些枚举标志也不能组合。

一些逻辑在SQL中完成;一些逻辑在C#代码中完成。例如,我可能会有一个函数:

// Here we get id for some record from the database.
public static void IsMaleOrFemale(int sexId)
{
    if (!Enum.IsDefined(typeof(SexType), sexId))
    {
        string message = String.Format("There is no sex type with id {0}.", 
            sexId);
        throw new ArgumentException(message, "sexId");
    }

    var sexType = (SexType) sexId;
    return sexType == SexType.Male || sexType == SexType.Female;
}

只要表格和枚举定义不变,这个方法就可以很好地工作。但是表格可能会发生变化。我不能依赖于id列或mnemonic列维持它们的值。我认为最好的方法是编写一个单元测试来确保表格与我的枚举定义同步。我试图将值匹配到id,将描述属性匹配到mnemonic列。
那么,如何通过查看“enum SexType”以编程方式获取所有配对列表:(0,“U”),(1,“M”),(2,“F”),(3,“T”)
这个想法是紧密比较这个有序列表和select id, mnemonic from sex order by is asc;

哇,我从来没有想过对那个进行单元测试。我喜欢那个想法! - Katie Kilian
6个回答

9
为什么不将SexType从枚举更改为类(或结构体),并在运行时从数据库中填充列表?
您的结构体(或类)将如下所示:
public struct SexType
{
  public string Type;
  public string Code;
  public int Value;
}

然后你可以从数据库中填充一个List<SexType>(如果你使用的是EF,则可以拉下一个类型为SexType的实体列表,或者任何你的解决方案允许的内容)。

假设你正在使用Linq和EF,请在应用程序启动时进行急切加载,然后你就可以开始了。


这种方法的困难在于基于该列表实现IsMaleOrFemale函数。我不知道ID是否会保持不变,如果“M”/“F”将被重命名为“MA”/“FE”等等。使用静态枚举+单元测试,我能够做出假设,然后进行测试。 - Hamish Grubijan
你是说表格中的数据可能会实际更改为 MA/FE 吗?假设无论你做什么,这都会引起问题 - 除非你不真正需要枚举与表格匹配,它只需要具有大致等效的值...或者我错过了什么? - AllenG
是的,实际表格(不涉及性别)可能会更改为“MA”/“FE”或“MALE”/“FEMALE”。我可能仍然会使用结构体,但我需要额外的数组来存储所有记录,以及记住映射的代码+数据结构。然后进行单元测试,确保一切同步。如果测试通过,那就没问题了。如果测试失败,则需要重写一些代码。 - Hamish Grubijan
显然,你的情况可能有所不同,但在我工作过需要类似功能的每个组织中(我猜测你从事某种形式的医疗保健),我们都建立了一个自己的表来设置性别的 ANSI 值,任何引用性别的内容都使用该表的引用(例如,Sex = 1)而不是(Sex = "MALE"),或者直接从该表中获取值。不确定这对你是否可行。 - AllenG

8

请查看Tangible T4Editor

我使用它来完成这个工作。

安装它,然后将这个文件添加到您的项目中(此博客帖子提供更多信息):

EnumGenerator.ttinclude

<#@ template debug="true" hostSpecific="true" #>
<#@ output extension=".generated.cs" #>
<#@ Assembly Name="System.Data" #>
<#@ import namespace="System.Data" #>
<#@ import namespace="System.Data.SqlClient" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#  
    string tableName = Path.GetFileNameWithoutExtension(Host.TemplateFile);
    string path = Path.GetDirectoryName(Host.TemplateFile);
    string columnId = "ID";
    string columnName = "NAME";
    string connectionString = "[your connection string]";
#>
using System; 
using System.CodeDom.Compiler;  

namespace Your.NameSpace.Enums
{     
    /// <summary>
    /// <#= tableName #> auto generated enumeration
    /// </summary>
    [GeneratedCode("TextTemplatingFileGenerator", "10")]
    public enum <#= tableName #>
    { 
<#
    SqlConnection conn = new SqlConnection(connectionString);
    string command = string.Format("select {0}, {1} from {2} order by {0}", columnId, columnName, tableName);
    SqlCommand comm = new SqlCommand(command, conn);

    conn.Open();

    SqlDataReader reader = comm.ExecuteReader();
    bool loop = reader.Read(); 

    while(loop)
    { 
#>
        /// <summary>
        /// <#= reader[columnName] #> configuration setting.
        /// </summary>
        <#= reader[columnName] #> = <#= reader[columnId] #><# loop = reader.Read(); #><#= loop ? ",\r\n" : string.Empty #> 
<#  } 
#>  }
} 
<#+     private string Pascalize(object value)
        {
            Regex rx = new Regex(@"(?:^|[^a-zA-Z]+)(?<first>[a-zA-Z])(?<reminder>[a-zA-Z0-9]+)");
            return rx.Replace(value.ToString(), m => m.Groups["first"].ToString().ToUpper() + m.Groups["reminder"].ToString().ToLower());
        }      

        private string GetSubNamespace()
        {
            Regex rx = new Regex(@"(?:.+Services\s)");
            string path = Path.GetDirectoryName(Host.TemplateFile);
            return rx.Replace(path, string.Empty).Replace("\\", ".");
        }
#>

(填写您的类的命名空间和连接字符串)

然后,您只需添加一个带有单行<#@ include file="EnumGenerator.ttinclude" #>`的空白TT文件。此文件的名称应与表的名称相同,并且此表的列必须命名为“ID”和“NAME”,除非在枚举生成器类中更改它。

每当您保存TT文件时,枚举将自动生成。


嗯...这看起来很有趣。那么,免费版可以处理这个吗,还是我需要购买专业版许可证? - Hamish Grubijan

5
var choices = Enumerable.Zip(
    Enum.GetNames(typeof(SexType)),
    Enum.GetValues(typeof(SexType)).Cast<SexType>(),
    (name, value) => Tuple.Create(name, value));

谢谢,就快完成了!有一件事我需要不同的是,我想要得到一个描述属性的数组,而不是枚举类型的名称。 - Hamish Grubijan
1
给编辑:请不要这样做。GetNamesGetValues的文档非常清楚地说明它们已经按枚举值的顺序返回了值和名称,因此在此处排序是不必要的。 - mqp

2

如果你将枚举值命名为U、M、F和T,那么使用Enum类的静态方法可以更加方便地完成所有工作。

如果不这样做,你需要使用一些反射技术来获取Description属性。

public IEnumerable<Tuple<string, int>> GetEnumValuePairs(Type enumType)
{
    if(!enumType.IsEnum)
    {
        throw new ArgumentException();
    }

    List<Tuple<string, int>> result = new List<Tuple<string, int>>();

    foreach (var value in Enum.GetValues(enumType))
    {
        string fieldName = Enum.GetName(enumType, value);

        FieldInfo fieldInfo = enumType.GetField(fieldName);
        var descAttribute = fieldInfo.GetCustomAttributes(false).Where(a => a is DescriptionAttribute).Cast<DescriptionAttribute>().FirstOrDefault();

        // ideally check if descAttribute is null here
        result.Add(Tuple.Create(descAttribute.Description, (int)value));
    }

    return result;
}

谢谢,实际表格中大约有十几个,具有描述性的名称确实有所帮助。 - Hamish Grubijan
是的,我代码中那部分反射机制是从Description属性中提取值,因此结果是“男性”,“女性”等。 - Matt Greer

1

我会使用T4文本模板,当模板被处理(保存和/或构建)时,它会动态生成列举。

然后,这个包含C#代码的模板将根据你的数据库内容为你生成一个.cs文件中的枚举定义。

当然,这意味着你必须在保存/编译时访问数据库,但这是唯一可以保证在编译时这两者一致的方法。

在运行时,你可能需要对数据库进行一次性检查,以确保枚举和数据库中的值一致。


1
List<Tuple<int, string>> pairs = new List<Tuple<int,string>>();
     Type enumType = typeof(SexType);
     foreach (SexType enumValue in Enum.GetValues(enumType))
     {
        var customAttributes = enumType.GetField(Enum.GetName(enumType, enumValue)).
           GetCustomAttributes(typeof(DescriptionAttribute), false);

        DescriptionAttribute descriptionAttribute = 
           (DescriptionAttribute)customAttributes.FirstOrDefault(attr => 
              attr is DescriptionAttribute);

        if (descriptionAttribute != null)
           pairs.Add(new Tuple<int, string>((int)enumValue, descriptionAttribute.Description));
     }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接