【繁星Code】如何在EF将实体注释写入数据库中

最近在项目中需要把各个字段的释义写到数据库中,该项目已经上线很长时间了,数据库中的字段没有上千也有上百个,要是一个项目一个项目打开然后再去找对应字段查看什么意思,估计要到明年过年了。由于项目中使用EntityFramework,本身这个项目只有手动设置字段注释的功能,Coder平时写代码的时候都懒得写注释,更别提能在配置数据库的时候将注释配置进去,所以如何在EF中自动将实体注释写入数据库,减轻Coder的压力(ru he tou lan)尤为重要。gitee地址: https://gitee.com/lbqman/Blog20210206.git
。下面进入正题。

一、实现思路



    

在FluentAPI中提供了HasComment方法,如下

11

1

    /// Configures a comment to be applied to the column

2

    ///  The type of the property being configured. 

3

    ///  The builder for the property being configured. 

4

    ///  The comment for the column. 

5

    ///  The same builder instance so that multiple calls can be chained. 

6

    public static PropertyBuilder HasComment(

7

      [NotNull] this PropertyBuilder propertyBuilder,

8

      [CanBeNull] string comment)

9

    {

10

      return (PropertyBuilder) propertyBuilder.HasComment(comment);

11

    }

也就是说我们要获取到实体对象的注释xml,然后读取对应的字段注释,自动调用改方法即可。需要解决的问题大概有以下几点。

  1. 如何获取当前配置的字段;
  2. 加载Xml,并根据字段获取对应的注释;
  3. 如何将枚举的各项信息都放入注释中;

二、实现方法

1.如何获取当前配置的字段

在获取xml的注释中,需要的信息有实体对应的类型以及对应的字段。而包含这两种类型的方法只有Property这个方法,该方法的出入参如下:

2

1

public virtual PropertyBuilder Property(

2

      [NotNull] Expression<Func> propertyExpression);

所以我们准备对这个方法进行改造。并且根据传入的propertyExpression获取字段名称。方法如下:

4

1

        public static PropertyBuilder SummaryProperty(

2

            this EntityTypeBuilder entityTypeBuilder,

3

            Expression<Func> propertyExpression)

4

            where TEntity : class

根据表达式获取字段名称如下:

10

1

        public static MemberInfo GetMember(this Expression<Func> expression)

2

        {

3

            MemberExpression memberExp;

4

            if (expression.Body is UnaryExpression unaryExpression)

5

                memberExp = unaryExpression.Operand as MemberExpression;

6

            else

7

                memberExp = expression.Body as MemberExpression;

8

9

            return memberExp?.Member;

10

        }

2.加载Xml,并根据字段获取对应的注释

VS中的Xml格式不在此处过多解释,属性、方法等注释可以根据规律去获取。此处需要注意的是当字段是从父类继承而来并且父类属于不同的dll时,需要获取父类所在dll的xml注释才行,另外枚举也需要同样去处理。虽然获取Xml数据的方法只在更新数据库时才调用,但是还是需要使用字典将数据缓存下来,方便下次快速获取。具体代码如下:

204

1

   /// 

2

    /// xml注释获取器

3

    /// 

4

    internal static class SummaryXmlCacheProvider

5

    {

6

        #region TClass

7

8

        /// 

9

        /// 根据类型初始化该类所在程序集的xml

10

        /// 

11

        /// 

12

        internal static void InitSummaryXml()

13

        {

14

            var assembly = Assembly.GetAssembly(typeof(TClass));

15

            SerializeXmlFromAssembly(assembly);

16

        }

17

18

        /// 

19

        /// 根据类型获取该类所在程序集的xml

20

        /// 

21

        /// 

22

        /// 

23

        internal static Dictionary GetSummaryXml()

24

        {

25

            var assembly = Assembly.GetAssembly(typeof(TClass));

26

            return SummaryCache[assembly];

27

        }

28

29

        /// 

30

        /// 获取该类在xml的key

31

        /// 

32

        /// 

33

        /// 

34

        internal static string GetClassTypeKey()

35

        {

36

            return TableSummaryRuleProvider.TypeSummaryKey(typeof(TClass).FullName);

37

        }

38

39

        #endregion

40

41

        #region TProperty

42

43

        /// 

44

        /// 根据类型以及字段初始化该类所在程序集的xml

45

        /// 

46

        /// 

47

        /// 

48

        /// 

49

        internal static void InitSummaryXml(Expression<Func> propertyExpression)

50

        {

51

            var propertyAssembly = GetPropertyAssembly(propertyExpression);

52

            SerializeXmlFromAssembly(propertyAssembly);

53

        }

54

55

        /// 

56

        /// 根据类型以及字段获取该类所在程序集的xml

57

        /// 

58

        /// 

59

        /// 

60

        /// 

61

        /// 

62

        internal static Dictionary GetSummaryXml(

63

            Expression<Func> propertyExpression)

64

        {

65

            var propertyAssembly = GetPropertyAssembly(propertyExpression);

66

            return SummaryCache[propertyAssembly];

67

        }

68

69

        /// 

70

        /// 获取该类以及字段所在xml的key

71

        /// 

72

        /// 

73

        /// 

74

        /// 

75

        /// 

76

        internal static string GetPropertyTypeKey(

77

            Expression<Func> propertyExpression)

78

        {

79

            var memberName = propertyExpression.GetMember().Name;

80

            var propertyInfo = GetPropertyInfo(propertyExpression);

81

            var propertyKey =

82

                $"{propertyInfo.DeclaringType.Namespace}.{propertyInfo.DeclaringType.Name}.{memberName}";

83

            return PropertySummaryRuleProvider.PropertyTypeSummaryKey(propertyKey);

84

        }

85

86

        #endregion

87

88

        #region TEnum

89

90

        /// 

91

        /// 获取枚举字段的描述信息

92

        /// 

93

        /// 

94

        /// 

95

        /// 

96

        /// 

97

        internal static string GetEnumPropertyDescription(Expression<Func> propertyExpression)

98

        {

99

            var propertyInfo = GetPropertyInfo(propertyExpression);

100

            if (!propertyInfo.PropertyType.IsEnum)

101

                return string.Empty;

102

            var enumType = propertyInfo.PropertyType;

103

            SerializeXmlFromAssembly(enumType.Assembly);

104

            var propertySummaryDic = SummaryCache[enumType.Assembly];

105

            var enumNames = enumType.GetEnumNames();

106

            var enumDescDic = enumType.GetNameAndValues();

107

            var enumSummaries = new List();

108

            foreach (var enumName in enumNames)

109

            {

110

                var propertyEnumKey = PropertySummaryRuleProvider.EnumTypeSummaryKey($"{enumType.FullName}.{enumName}");

111

                var enumSummary = propertySummaryDic.ContainsKey(propertyEnumKey)

112

                    ? propertySummaryDic[propertyEnumKey]

113

                    : string.Empty;

114

                var enumValue = enumDescDic[enumName];

115

                enumSummaries.Add(PropertySummaryRuleProvider.EnumTypeSummaryFormat(enumValue,enumName,enumSummary));

116

            }

117

118

            return string.Join(";", enumSummaries);

119

120

        }

121

122

        #endregion

123

124

        /// 

125

        /// 根据表达式获取属性所在的程序集

126

        /// 

127

        /// 

128

        /// 

129

        /// 

130

        /// 

131

        private static Assembly GetPropertyAssembly(

132

            Expression<Func> propertyExpression)

133

        {

134

            var propertyInfo = GetPropertyInfo(propertyExpression);

135

            var propertyAssembly = propertyInfo.Module.Assembly;

136

            return propertyAssembly;

137

        }

138

139

        /// 

140

        /// 根据表达式获取字段属性

141

        /// 

142

        /// 

143

        /// 

144

        /// 

145

        /// 

146

        private static PropertyInfo GetPropertyInfo(

147

            Expression<Func> propertyExpression)

148

        {

149

            var entityType = typeof(TClass);

150

            var memberName = propertyExpression.GetMember().Name;

151

            var propertyInfo = entityType.GetProperty(memberName, typeof(TProperty));

152

            if (propertyInfo == null || propertyInfo.DeclaringType == null)

153

                throw new ArgumentNullException($"this property {memberName} is not belong to {entityType.Name}");

154

155

            return propertyInfo;

156

        }

157

158

        /// 

159

        /// 根据程序集初始化xml

160

        /// 

161

        /// 

162

        private static void SerializeXmlFromAssembly(Assembly assembly)

163

        {

164

            var assemblyPath = assembly.Location;

165

            var lastIndexOf = assemblyPath.LastIndexOf(".dll", StringComparison.Ordinal);

166

            var xmlPath = assemblyPath.Remove(lastIndexOf, 4) + ".xml";

167

168

            if (SummaryCache.ContainsKey(assembly))

169

                return;

170

            var xmlDic = new Dictionary();

171

            if (!File.Exists(xmlPath))

172

            {

173

                Console.WriteLine($"未能加载xml文件,原因:xml文件不存在,path:{xmlPath}");

174

                SummaryCache.Add(assembly, xmlDic);

175

                return;

176

            }

177

178

            var doc = new XmlDocument();

179

            doc.Load(xmlPath);

180

            var members = doc.SelectNodes("doc/members/member");

181

            if (members == null)

182

            {

183

                Console.WriteLine($"未能加载xml文件,原因:doc/members/member节点不存在");

184

                SummaryCache.Add(assembly, xmlDic);

185

                return;

186

            }

187

188

            foreach (XmlElement member in members)

189

            {

190

                var name = member.Attributes["name"].InnerText.Trim();

191

                if (string.IsNullOrWhiteSpace(name))

192

                    continue;

193

                xmlDic.Add(name, member.SelectSingleNode("summary")?.InnerText.Trim());

194

            }

195

196

            SummaryCache.Add(assembly, xmlDic);

197

        }

198

199

        /// 

200

        /// xml注释缓存

201

        /// 

202

        private static Dictionary<Assembly, Dictionary> SummaryCache { get; } =

203

            new Dictionary<Assembly, Dictionary>();

204

    }

3.如何将枚举的各项信息都放入注释中

上面的两个步骤已经根据表达式将字段的注释获取到,直接调用EF提供的HasComment即可。见代码:

15

1

        public static PropertyBuilder SummaryProperty(

2

            this EntityTypeBuilder entityTypeBuilder,

3

            Expression<Func> propertyExpression)

4

            where TEntity : class

5

        {

6

            SummaryXmlCacheProvider.InitSummaryXml(propertyExpression);

7

            var entitySummaryDic = SummaryXmlCacheProvider.GetSummaryXml(propertyExpression);

8

            var propertyKey = SummaryXmlCacheProvider.GetPropertyTypeKey(propertyExpression);

9

            var summary = entitySummaryDic.ContainsKey(propertyKey) ? entitySummaryDic[propertyKey] : string.Empty;

10

            var enumDescription = SummaryXmlCacheProvider.GetEnumPropertyDescription(propertyExpression);

11

            summary = string.IsNullOrWhiteSpace(enumDescription) ? summary : $"{summary}:{enumDescription}";

12

            return string.IsNullOrWhiteSpace(summary)

13

                ? entityTypeBuilder.Property(propertyExpression)

14

                : entityTypeBuilder.Property(propertyExpression).HasComment(summary);

15

        }

同时也可以设置表的注释以及表的名称。如下:

26

1

public static EntityTypeBuilder SummaryToTable(

2

            this EntityTypeBuilder entityTypeBuilder, bool hasTableComment = true,

3

            Func tablePrefix = null)

4

            where TEntity : class

5

        {

6

            var tableName = GetTableName(tablePrefix);

7

            return hasTableComment

8

                ? entityTypeBuilder.ToTable(tableName)

9

                    .SummaryHasComment()

10

                : entityTypeBuilder.ToTable(tableName);

11

        }

12

13

        public static EntityTypeBuilder SummaryHasComment(

14

            this EntityTypeBuilder entityTypeBuilder) where TEntity : class

15

        {

16

            SummaryXmlCacheProvider.InitSummaryXml();

17

            var entityDic = SummaryXmlCacheProvider.GetSummaryXml();

18

            var tableKey = SummaryXmlCacheProvider.GetClassTypeKey();

19

            var summary = entityDic.ContainsKey(tableKey) ? entityDic[tableKey] : string.Empty;

20

            return string.IsNullOrWhiteSpace(summary) ? entityTypeBuilder : entityTypeBuilder.HasComment(summary);

21

        }

22

23

        private static string GetTableName(Func tablePrefix)

24

        {

25

            return typeof(TEntity).Name.Replace("Entity", $"Tb{tablePrefix?.Invoke()}");

26

        }

搞定。

三、效果展示

运行Add-Migration InitDb即可查看生成的代码。

1

    public partial class InitDb : Migration

2

    {

3

        protected override void Up(MigrationBuilder migrationBuilder)

4

        {

5

            migrationBuilder.CreateTable(

6

                name: "TbGood",

7

                columns: table => new

8

                {

9

                    Id = table.Column(nullable: false, comment: "主键")

10

                        .Annotation("SqlServer:Identity", "1, 1"),

11

                    CreateTime = table.Column(nullable: false, comment: "创建时间"),

12

                    CreateName = table.Column(maxLength: 64, nullable: false, comment: "创建人姓名"),

13

                    UpdateTime = table.Column(nullable: true, comment: "更新时间"),

14

                    UpdateName = table.Column(maxLength: 64, nullable: true, comment: "更新人姓名"),

15

                    Name = table.Column(maxLength: 64, nullable: false, comment: "商品名称"),

16

                    GoodType = table.Column(nullable: false, comment: "物品类型:(0,Electronic) 电子产品;(1,Clothes) 衣帽服装;(2,Food) 食品;(3,Other) 其他物品"),

17

                    Description = table.Column(maxLength: 2048, nullable: true, comment: "物品描述"),

18

                    Store = table.Column(nullable: false, comment: "储存量")

19

                },

20

                constraints: table =>

21

                {

22

                    table.PrimaryKey("PK_TbGood", x => x.Id);

23

                },

24

                comment: "商品实体类");

25

26

            migrationBuilder.CreateTable(

27

                name: "TbOrder",

28

                columns: table => new

29

                {

30

                    Id = table.Column(nullable: false, comment: "主键")

31

                        .Annotation("SqlServer:Identity", "1, 1"),

32

                    CreateTime = table.Column(nullable: false, comment: "创建时间"),

33

                    CreateName = table.Column(maxLength: 64, nullable: false, comment: "创建人姓名"),

34

                    UpdateTime = table.Column(nullable: true, comment: "更新时间"),

35

                    UpdateName = table.Column(maxLength: 64, nullable: true, comment: "更新人姓名"),

36

                    GoodId = table.Column(nullable: false, comment: "商品Id"),

37

                    OrderStatus = table.Column(nullable: false, comment: "订单状态:(0,Ordered) 已下单;(1,Payed) 已付款;(2,Complete) 已付款;(3,Cancel) 已取消"),

38

                    OrderTime = table.Column(nullable: false, comment: "下订单时间"),

39

                    Address = table.Column(maxLength: 2048, nullable: false, comment: "订单地址"),

40

                    UserName = table.Column(maxLength: 16, nullable: false, comment: "收件人姓名"),

41

                    TotalAmount = table.Column(nullable: false, comment: "总金额")

42

                },

43

                constraints: table =>

44

                {

45

                    table.PrimaryKey("PK_TbOrder", x => x.Id);

46

                },

47

                comment: "订单实体类");

48

        }

49

50

        protected override void Down(MigrationBuilder migrationBuilder)

51

        {

52

            migrationBuilder.DropTable(

53

                name: "TbGood");

54

55

            migrationBuilder.DropTable(

56

                name: "TbOrder");

57

        }

58

    }

四、写在最后

此种方法是在我目前能想起来比较方便生成注释的方法了,另外还想过EntityTypeBuilder中的Property方法,最后还是放弃了,因为EntityTypeBuilder是由ModelBuilder生成,而ModelBuilder又是ModelSource中的CreateModel方法产生,最后一路深扒到DbContext中,为了搞这个注释得不偿失,最后才采取了上述的方法。若是大佬有更好的方法,恭请分享。