简单的解决方案
我也想过这个问题。记录不适合我的目的,因为它需要与 EF Core 交互。
我建议一种简单且低成本的方法:
- 向类添加一个复制构造函数;
- 使克隆期间更改的属性可用于初始化;
- 通过具有初始化列表的复制构造函数克隆具有更改的对象:
var a = new SomeItem("name", "abracadabra");
var b = new SomeItem(a) {Description="descr"};
简单的代码
var a = new SomeItem("name", "abracadabra");
var b = new SomeItem(a) {Description="descr"};
public class SomeItem
{
private string name;
private string description;
public SomeItem(string name, string description)
{
Name = name;
Description = description;
}
public SomeItem(SomeItem another): this(another.Name, another.Description)
{
}
public string Name
{
get => name;
init => name = value;
}
public string Description
{
get => description;
init => description = value;
}
}
扩展解决方案
如果在编译时不知道最终类型,那么这种方法很容易扩展。假设有一个类“ValueObject”,我们需要克隆它的派生类型。
注意:对于某些地方的错误翻译,我深表歉意。使用 google.translate 获得的英文版
附加代码
using System.Linq.Expressions;
using Light.GuardClauses;
using JetBrains.Annotations;
using static DotNext.Linq.Expressions.ExpressionBuilder;
using ValueObject = Company.Domain....;
/// <summary>
/// The plagiarizer creates a copy of the object with a change in its individual properties using an initializer
/// </summary>
/// <remarks> The foreign object must define a copy constructor, and mutable members must support initialization </remarks>
public struct Plagiarist {
/// <summary>
/// Object to be copied
/// </summary>
private readonly object _alienObject;
/// <summary>
/// Type <see cref="_alienObject" />
/// </summary>
private Type _type => _alienObject.GetType();
/// <summary>
/// Object parsing Expression
/// </summary>
private readonly ParsingInitializationExpression _parser = new();
public Plagiarist(object alienObject) {
_alienObject = alienObject.MustNotBeNullReference();
if (!CopyConstructorIs())
throw new ArgumentException($"Type {_type.FullName} must implement a copy constructor");
}
/// <summary>
/// Does the object we want to plagiarize have a copy constructor?
/// </summary>
/// <returns>True - there is a copy constructor, otherwise - false</returns>
[Pure]
private bool CopyConstructorIs() {
return _type.GetConstructor(new[] { _type }) is not null;
}
/// <summary>
/// Returns a copy of a foreign object with a change in its individual properties using an initializer
/// </summary>
/// <param name="initializer">
/// <see cref="Expression" /> create an object with initialization of those fields,
/// which need to be changed:
/// <code>() => new T() {Member1 = "Changed value1", Member2 = "Changed value2"}</code>
/// or <see cref="Expression" /> create an anonymous type with initialization of those fields
/// that need to be changed:
/// <code>() => new {Member1 = "Changed value1", Member2 = "Changed value2"}</code>
/// </param>
/// <returns></returns>
[Pure]
public object Plagiarize(Expression<Func<object>> initializer) {
var (newValues, constructParam) = _parser.ParseInitialization(initializer);
var constrCopies = _type.New(_alienObject.Const().Convert(_type));
Expression plagiarist = (newValues.Count, constructParam.Count) switch {
(> 0, _) => Expression.MemberInit(constrCopies, newValues.Values),
(0, > 0) => Expression.MemberInit(constrCopies, ConstructorInInitializationList(constructParam).Values),
_ => constrCopies
};
var plagiarize = Expression.Lambda<Func<object>>(plagiarist).Compile();
return plagiarize();
}
[Pure]
public Dictionary<string, MemberAssignment> ConstructorInInitializationList(
Dictionary<string, Expression> constructorParameters) {
Dictionary<string, MemberAssignment> initializer = new();
const BindingFlags flagReflections = BindingFlags.Default | BindingFlags.Instance | BindingFlags.Public;
var allProperties = _type.GetProperties(flagReflections);
var allFields = _type.GetFields(flagReflections);
foreach (var memberName in constructorParameters.Keys) {
var property = allProperties.FirstOrDefault(s => s.Name ==memberName);
var field = allFields.FirstOrDefault(s => s.Name == memberName);
(MemberInfo member, Type memberType) = (property, field) switch {
({ }, _) => (property, property.PropertyType),
(null, { }) => ((MemberInfo)field, field.FieldType),
_ => throw new ArgumentException($"{_type.FullName} does not contain member {memberName}")
};
initializer[memberName] = Expression.Bind(member, constructorParameters[memberName].Convert(memberType));
}
return initializer;
}
/// <summary>
/// Template "Visitor" for traversing the expression tree in order to highlight
/// initialization expression and constructor
/// </summary>
private class ParsingInitializationExpression : ExpressionVisitor {
private Dictionary<string, MemberAssignment>? _initializer;
private Dictionary<string, Expression>? _initializerAnonym;
/// <summary>
/// Parses the expression tree and returns the initializer and constructor parameters
/// </summary>
/// <param name="initializer"><see cref="Expression" /> to parse</param>
/// <returns> tuple of initializer and constructor</returns>
public ParsedInitialization ParseInitialization(Expression initializer) {
_initializer = new Dictionary<string, MemberAssignment>();
_initializerAnonym = new Dictionary<string, Expression>();
Visit(initializer);
return new ParsedInitialization(_initializer, _initializerAnonym);
}
protected override MemberAssignment VisitMemberAssignment(MemberAssignment node) {
_initializer![node.Member.Name] = node;
return base.VisitMemberAssignment(node);
}
protected override Expression VisitNew(NewExpression node) {
foreach (var (member, value) in node.Members?.Zip(node.Arguments) ??
Array.Empty<(MemberInfo First, Expression Second)>())
_initializerAnonym![member.Name] = value;
return base.VisitNew(node);
}
/// <summary>
/// Type to return values from method <see cref="ParseInitialization" />
/// </summary>
/// <param name="Initializer"></param>
/// <param name="ConstructorParameters"></param>
public record struct ParsedInitialization(Dictionary<string, MemberAssignment> Initializer,
Dictionary<string, Expression> ConstructorParameters);
}
}
public static class ValueObjectPlagiarizer{
/// <summary>
/// Creates a copy of the object with a change in its individual properties using an initializer
/// </summary>
/// <param name="alien">Object to be plagiarized</param>
/// <param name="initializer">
/// <see cref="Expression" /> creating an object of type <typeparamref name="T" />
/// with initialization of those fields that need to be changed:
/// <code>ob.Plagiarize(() => new T() {Member1 = "Changed value1", Member2 = "Changed value2"})</code>
/// or <see cref="Expression" /> create an anonymous type with initialization of those fields that need to be changed:
/// <code>ob.Plagiarize(() => new {Member1 = "Changed value1", Member2 = "Changed value2"})</code>
/// </param>
/// <returns>plagiarism of the object</returns>
public static object Plagiarize<T>(this ValueObject alien, Expression<Func<T>> initializer)
where T : class {
var bodyReduced = initializer.Convert<object>();
var initializerReduced = Expression.Lambda<Func<object>>(bodyReduced, initializer.Parameters);
return new Plagiarist(alien).Plagiarize(initializerReduced);
}
}
用法
如果 SomeItem 是 ValueObject 的后代
ValueObject a = new SomeItem("name", "abracadabra");
// via type constructor
var b = (SomeItem)a.Plagiarize(()=>new SomeItem(null){Description="descr"});
// anonymous type
var c = (SomeItem)a.Plagiarize(()=>new{Description="descr"});
b.Description.Should().Be("descr"); //true
c.Description.Should().Be("descr"); //true