.Net Core 教程 Part6 – (10)(11)(12)(13)(14)(15) DDD落地之充血模型

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's Blog站点所有,发布者:chaoqiang,转转请注明出处:http://www.zhengchaoqiang.com/1607.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-04 08:39
下一篇 2022-04-04 19:12

相关推荐

发表回复

您的电子邮箱地址不会被公开。

近期个人博客正在迁移中,原博客请移步此处,抱歉!