Part6 – (10) DDD之贫血模型与充血模型
概念
1、贫血模型:一个类中只有属性或者成员变量,没有方法。
2、充血模型:一个类中既有属性、成员变量,也有方法。
需求:定义一个类保存用户的用户名、密码、积分;用户必须具有用户名;为了保证安全,密码采用密码的散列值保存;用户的初始积分为10分;每次登录成功奖励5个积分,每次登录失败扣3个积分。
//实体类
class User
{
public string UserName { get; set; }//用户名
public string PasswordHash { get; set; }//密码的散列值
public int Credit { get; set; }//积分
}
/**********************************************************/
//业务逻辑代码
User u1 = new User(); u1.UserName = "zcq"; u1.Credit = 10;
u1.PasswordHash = HashHelper.Hash("123456");//计算密码的散列值
string pwd = Console.ReadLine();
if(HashHelper.Hash(pwd)==u1.PasswordHash)
{
u1.Credit += 5;//登录增加5个积分
Console.WriteLine("登录成功");
}
Else
{
if (u1.Credit < 3)
Console.WriteLine("积分不足,无法扣减");
else
{
u1.Credit -= 3;//登录失败,则扣3个积分
Console.WriteLine("登录失败");
}
Console.WriteLine("登录失败");
//实体类
class User
{
public string UserName { get; init; }
public int Credit { get; private set; }
private string? passwordHash;
public User(string userName)
{
this.UserName = userName;
this.Credit =10;//初始积分
}
public void ChangePassword(string newValue)
{
if(newValue.Length<6)
{
throw new Exception("密码太短");
}
this.passwordHash =Hash(newValue);
}
public bool CheckPassword(string password)
{
string hash = HashHelper.Hash(password);
return passwordHash== hash;
}
public void DeductCredits(int delta)
{
if(delta<=0)
{
throw new Exception("额度不能为负值");
}
this.Credit -= delta;
}
public void AddCredits(int delta)
{
this.Credit += delta;
}
}
/**********************************************************/
//业务逻辑代码
User u1 = new User("yzk");
u1.ChangePassword("123456");
string pwd = Console.ReadLine();
if (u1.CheckPassword(pwd))
{
u1.AddCredits(5);
Console.WriteLine("登录成功");
}
else
{
u1.DeductCredits(5);
Console.WriteLine("登录失败");
}
Part6 – (11) EF Core对实体属性操作的秘密
引言
1、Why?为EF Core实现充血模型做准备。
2、EF Core是通过实体对象的属性的get、set来进行属性的读写吗?
3、答案:基于性能和对特殊功能支持的考虑,EF Core在读写属性的时候,如果可能,它会直接跳过get、set,而直接操作真正存储属性值的成员变量。
class Dog
{
public long Id { get; set; }
private string name;
public string Name
{
get
{
Console.WriteLine("get被调用");
return name;
}
set
{
Console.WriteLine("set被调用");
this.name = value;
}
}
}
Dog d1 = new Dog { Name= "goofy" };
Console.WriteLine("Dog初始化完毕");
ctx.Dogs.Add(d1);
ctx.SaveChanges();
Console.WriteLine("SaveChanges完毕");
Console.WriteLine("准备读取数据");
Dog d2 = ctx.Dogs.First(d=>d.Name== "goofy");
Console.WriteLine("读取数据完毕");
结论:
EF Core在读写实体对象的属性时,会查找属性对应的成员变量,如果能找到,EF Core会直接读写这个成员变量的值,而不是通过set和get代码块来读写。
//改一下Dog类
class Dog
{
public long Id { get; set; }
private string xingming;
public string Name
{
get
{
Console.WriteLine("get被调用");
return xingming;
}
set
{
Console.WriteLine("set被调用");
this.xingming = value;
}
}
}
结论:
1、EF Core会尝试按照命名规则去直接读写属性对应的成员变量,只有无法根据命名规则找到对应成员变量的时候,EF Core才会通过属性的get、set代码块来读写属性值。
2(*)、可以在FluentAPI中通过UsePropertyAccessMode()方法来修改默认的这个行为。
Part6 – (12) EF Core中充血模型的需求
充血模型实现的要求
一:属性是只读的或者是只能被类内部的代码修改。
二:定义有参数的构造方法。
三:有的成员变量没有对应属性,但是这些成员变量需要映射为数据表中的列,也就是我们需要把私有成员变量映射到数据表中的列。
四:有的属性是只读的,也就是它的值是从数据库中读取出来的,但是我们不能修改属性值。
五:有的属性不需要映射到数据列,仅在运行时被使用。
实现“一”
属性是只读的或者是只能被类内部的代码修改。
实现:把属性的set定义为private或者init,然后通过构造方法为这些属性赋予初始值。
实现“二”
定义有参数的构造方法。
原理: EF Core中的实体类如果没有无参的构造方法,则有参的构造方法中的参数的名字必须和属性的名字一致。为什么?
实现方式1:无参构造方法定义为private。
实现方式2:实体类中不定义无参构造方法,只定义有意义的有参构造方法,但是要求构造方法中的参数的名字和属性的名字一致。
实现“三”
不属于属性的成员变量映射为数据列。
实现:builder.Property(“成员变量名”)
实现“四”
从数据列中读取值的只读属性。
EF Core中提供了“支持字段”(backing field)来支持这种写法:在配置实体类的代码中,使用HasField(“成员变量名”)来配置属性。
实现“五”
有的属性不需要映射到数据列,仅在运行时被使用。
实现:使用Ignore()来配置忽略这个属性。
Part6 – (13) EF Core中实现充血模型
充血模型实体:
public record User
{
public int Id { get; init; }//特征一
public DateTime CreatedDateTime { get; init; }//特征一
public string UserName { get; private set; }//特征一
public int Credit { get; private set; }
private string? passwordHash;//特征三
private string? remark;
public string? Remark //特征四 只读不可写
{
get { return remark; }
}
public string? Tag { get; set; }//特征五
private User()//特征二 给EFCore从数据库中加载数据然后生成User对象返回用的
{
}
public User(string yhm)//特征二 故意构造和属性不一样名字的参数 给程序员用的
{
this.UserName = yhm;
this.CreatedDateTime = DateTime.Now;
this.Credit = 10;
}
public void ChangeUserName(string newValue)
{
this.UserName = newValue;
}
public void ChangePassword(string newValue)
{
if (newValue.Length < 6)
{
throw new ArgumentException("密码太短");
}
this.passwordHash = HashHelper.Hash(newValue);
}
}
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
internal class UserConfig : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.Property("passwordHash");//特征三
builder.Property(u => u.Remark).HasField("remark");//特征四
builder.Ignore(u => u.Tag);//特征五 Tag字段不映射到数据库
}
}
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
ServiceCollection services = new ServiceCollection();
services.AddDbContext<TestDbContext>(opt => {
string connStr = "Data Source=.;Initial Catalog=chongxue;Integrated Security=true";
opt.UseSqlServer(connStr);
});
var sp = services.BuildServiceProvider();
var ctx = sp.GetRequiredService<TestDbContext>();
/*
User u1 = new User("Zcq");
u1.Tag = "MyTag";
u1.ChangePassword("123456");
ctx.Users.Add(u1);
ctx.SaveChanges();*/
User u1 = ctx.Users.First(u => u.UserName == "Zcq");
Console.WriteLine(u1);
Part6 – (14) EF Core中实现值对象
值类型的需求
1、“商品”实体中的重量属性。我们如果把重量定义为double类型,那么其实是隐含了一个“重量单位”的领域知识,使用这个实体类的开发人员就需要知道这个领域知识,而且我们还要通过文档等形式把这个领域知识记录下来,这又面临一个文档和代码修改同步的问题。
2、实现:定义一个包含Value(数值)、Unit(单位)的Weight类型,然后把“商品”的重量属性设置为Weight类型。
3、很多数值类型的属性其实都是隐含了单位的,比如金额隐含了币种信息。
值类型的实现
1、“从属实体类型(owned entities)”:使用Fluent API中的OwnsOne等方法来配置。
2、在EF Core中,实体的属性可以定义为枚举类型,枚举类型的属性在数据库中默认是以整数类型来保存的。对于直接操作数据库的人员来讲,0、1、2这样的值没有“CNY”(人民币)、“USD”(美元)、“NZD”(新西兰元)等这样的字符串类型值可读性更强。EF Core中可以在Fluent API中用HasConversion<string>()把枚举类型的值配置成保存为字符串。
实体对象的构造:
record Region
{
public long Id { get; init; }
public MultilingualString Name { get; init; }
public Area Area { get; init; }
public RegionLevel Level { get; private set; }
public long? Population { get; private set; }
public Geo Location { get; init; }
private Region() { }
public Region(MultilingualString name, Area area, Geo location,
RegionLevel level)
{
this.Name = name;
this.Area = area;
this.Location = location;
this.Level = level;
}
public void ChangePopulation(long value)
{
this.Population = value;
}
public void ChangeLevel(RegionLevel value)
{
this.Level = value;
}
}
值对象Geo的实现:
record Geo{
public double Longitude { get; init; }
public double Latitude { get; init; }
public Geo(double longitude, double latitude)
{
if(longitude<-180||longitude>180)
throw new ArgumentException("longitude invalid");
if (latitude < -90 || latitude > 90)
throw new ArgumentException("longitude invalid");
this.Longitude = longitude;
this.Latitude = latitude;
}
}
值对象的实现:
record Area(double Value, AreaType Unit);
枚举类型的实现:
enum AreaType { SquareKM, Hectare, CnMu }
enum RegionLevel { Province, City, County, Town }
多语言的实现
record MultilingualString(string Chinese, string? English);
DbContext的实现和配置:
using Microsoft.EntityFrameworkCore;
class TestDbContext : DbContext
{
public DbSet<Region> Cities { get; private set; }
public TestDbContext(DbContextOptions<TestDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}
}
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
class RegionConfig : IEntityTypeConfiguration<Region>
{
public void Configure(EntityTypeBuilder<Region> builder)
{
builder.OwnsOne(c => c.Area, nb => {
nb.Property(e => e.Unit).HasMaxLength(20)
.IsUnicode(false).HasConversion<string>();
});
builder.OwnsOne(c => c.Location);
builder.Property(c => c.Level).HasMaxLength(20)
.IsUnicode(false).HasConversion<string>();
builder.OwnsOne(c => c.Name, nb => {
nb.Property(e => e.English).HasMaxLength(20).IsUnicode(false);
nb.Property(e => e.Chinese).HasMaxLength(20).IsUnicode(true);
});
}
}
DbContext 的配置与启动:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
class MyDesignTimeDbContextFactory : IDesignTimeDbContextFactory<TestDbContext>
{
public TestDbContext CreateDbContext(string[] args)
{
DbContextOptionsBuilder<TestDbContext> builder = new();
string connStr = "Data Source=.;Initial Catalog=valueobj1;Integrated Security=true";
builder.UseSqlServer(connStr);
return new TestDbContext(builder.Options);
}
}
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
ServiceCollection services = new ServiceCollection();
services.AddDbContext<TestDbContext>(opt => {
string connStr = "Data Source=.;Initial Catalog=valueobj1;Integrated Security=true";
opt.UseSqlServer(connStr);
});
var sp = services.BuildServiceProvider();
var ctx = sp.GetRequiredService<TestDbContext>();
MultilingualString name1 = new MultilingualString("北京", "BeiJing");
Area area1 = new Area(16410, AreaType.SquareKM);
Geo loc = new Geo(116.4074, 39.9042);
Region c1 = new Region(name1, area1, loc, RegionLevel.Province);
c1.ChangePopulation(21893100);
ctx.Cities.Add(c1);
ctx.SaveChanges();
Part6 – (15) 构建表达式树简化值对象比较
比较的麻烦:
ctx.Cities.Where(c=>c.Name==new MultilingualString(“北京”,“BeiJing”)) //不行。
//怎么办?
ctx.Cities.Where(c=>c.Name.Chinese== "北京"&&c.Name.English="BeiJing")
//使用封装好的方法 ExpressionHelper.MakeEqual
var cities = ctx.Cities.Where(ExpressionHelper.MakeEqual((Region c) => c.Name,
new MultilingualString("北京", "BeiJing")));
foreach(var c in cities)
{
Console.WriteLine(c);
}
使用封装好的方法:
using System;
using System.Linq;
using System.Linq.Expressions;
using static System.Linq.Expressions.Expression;
public class ExpressionHelper
{
/// <summary>
/// Users.SingleOrDefaultAsync(MakeEqual((User u) => u.PhoneNumber, phoneNumber))
/// </summary>
/// <typeparam name="TItem"></typeparam>
/// <typeparam name="TProp"></typeparam>
/// <param name="propAccessor"></param>
/// <param name="other"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public static Expression<Func<TItem, bool>> MakeEqual<TItem, TProp>(Expression<Func<TItem, TProp>> propAccessor, TProp? other)
where TItem : class
where TProp : class
{
var e1 = propAccessor.Parameters.Single();//提取出来参数
BinaryExpression? conditionalExpr = null;
foreach (var prop in typeof(TProp).GetProperties())
{
BinaryExpression equalExpr;
//other的prop属性的值
object? otherValue = null;
if (other != null)
{
otherValue = prop.GetValue(other);
}
Type propType = prop.PropertyType;
//访问待比较的属性
var leftExpr = MakeMemberAccess(
propAccessor.Body,//要取出来Body部分,不能带参数
prop
);
Expression rightExpr = Convert(Constant(otherValue), propType);
if (propType.IsPrimitive)//基本数据类型和复杂类型比较方法不一样
{
equalExpr = Equal(leftExpr, rightExpr);
}
else
{
equalExpr = MakeBinary(ExpressionType.Equal,
leftExpr, rightExpr, false,
prop.PropertyType.GetMethod("op_Equality")
);
}
if (conditionalExpr == null)
{
conditionalExpr = equalExpr;
}
else
{
conditionalExpr = AndAlso(conditionalExpr, equalExpr);
}
}
if (conditionalExpr == null)
{
throw new ArgumentException("There should be at least one property.");
}
return Lambda<Func<TItem, bool>>(conditionalExpr, e1);
}
}
本文版权归个人技术分享站点所有,发布者:chaoqiang,转转请注明出处:http://www.zhengchaoqiang.com/1607.html