Skip to content

Instantly share code, notes, and snippets.

@btshft
Last active March 7, 2020 21:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save btshft/136e8cabaefa6d5f0bf5ceaa7c9ecba1 to your computer and use it in GitHub Desktop.
Save btshft/136e8cabaefa6d5f0bf5ceaa7c9ecba1 to your computer and use it in GitHub Desktop.
Modification / EntityPatch
/// <summary>
/// Контейнер содержащий модификацию для объекта.
/// </summary>
/// <typeparam name="T">Тип объекта.</typeparam>
public class Modification<T> where T : class
{
/// <summary>
/// Модификации объекта.
/// </summary>
public ICollection<Change> Changes { get; set; } = new List<Change>();
/// <summary>
/// Создает сущность патча, где присутствуют только измененные свойства.
/// </summary>
public dynamic CreatePatch()
{
var patch = new ExpandoObject();
if (Changes == null)
return patch;
foreach (var change in Changes)
{
var propertyChain = change.Path.Split('.', StringSplitOptions.RemoveEmptyEntries).ToArray();
var container = (IDictionary<string, object>) patch;
for (var i = 0; i < propertyChain.Length; i++)
{
var isLastLeaf = i == propertyChain.Length - 1;
var propertyName = propertyChain[i];
if (isLastLeaf)
{
container.Add(propertyName, change.Value);
}
else
{
var nestedProperty = new ExpandoObject();
container.Add(propertyName, nestedProperty);
container = nestedProperty;
}
}
}
return patch;
}
/// <summary>
/// Применяет модификацию к объекту.
/// </summary>
public T ApplyTo(T source)
{
static (PropertyInfo property, object container) ConstructProperty(string propertyPath, T propertyContainer, object valueToSet)
{
if (string.IsNullOrEmpty(propertyPath))
throw new ArgumentNullException(nameof(propertyPath));
var property = default(PropertyInfo);
var reflectedType = typeof(T);
var container = (object) propertyContainer;
var propertyChain = propertyPath.Split('.', StringSplitOptions.RemoveEmptyEntries).ToArray();
for (var i = 0; i < propertyChain.Length; i++)
{
var isLastLeaf = i == propertyChain.Length - 1;
var propertyName = propertyChain[i];
property = reflectedType.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public);
if (property == null)
throw new InvalidOperationException($"Не найдено свойство '{propertyName}' в типе '{reflectedType}' при обработке модификации по пути '{propertyPath}'");
if (isLastLeaf)
{
if (!property.CanWrite || property.GetSetMethod(nonPublic: false) == null)
throw new InvalidOperationException($"Свойство '{propertyName}' в типе '{reflectedType}' не поддерживает запись. Путь: '{propertyPath}'");
}
else
{
var propertyValue = property.GetValue(container);
// Не инициализируем свойство если значение для установки - null.
if (propertyValue == null && valueToSet != null)
{
if (property.PropertyType.IsAbstract)
throw new InvalidOperationException($"Свойство '{propertyName}' в типе '{reflectedType}' не инициализировано и не может быть изменено. Путь: '{propertyPath}'");
try
{
var emptyContainer = FormatterServices.GetUninitializedObject(property.PropertyType);
property.SetValue(container, emptyContainer);
propertyValue = property.GetValue(container);
} catch(Exception e)
{
throw new InvalidOperationException($"Свойство '{propertyName}' в типе '{reflectedType}' не инициализировано и не может быть изменено. Путь: '{propertyPath}'", e);
}
}
container = propertyValue;
reflectedType = property.PropertyType;
}
}
return (property, container);
}
if (source == null)
throw new ArgumentNullException(nameof(source));
if (Changes == null)
return source;
foreach (var change in Changes)
{
if (string.IsNullOrEmpty(change.Path))
throw new InvalidOperationException("Не задан путь к свойству");
var (property, container) = ConstructProperty(change.Path, source, change.Value);
if (change.Value == null)
{
var defaultValue = property.PropertyType.IsValueType
? Activator.CreateInstance(property.PropertyType)
: null;
property.SetValue(container, defaultValue);
}
else
{
var changeType = change.Value.GetType();
if (property.PropertyType != changeType && !property.PropertyType.IsInstanceOfType(changeType))
throw new InvalidOperationException($"Свойство '{change.Path}' в типе '{typeof(T).Name}' не поддерживает установку значения типа '{changeType}'");
var value = changeType == property.PropertyType
? change.Value
: Convert.ChangeType(change.Value, property.PropertyType);
property.SetValue(container, value);
}
}
return source;
}
/// <summary>
/// Создает экземпляр билдера.
/// </summary>
public static Builder CreateBuilder()
{
return new Builder();
}
/// <summary>
/// Изменение свойства.
/// </summary>
public class Change
{
/// <summary>
/// Путь к свойству через точку.
/// Например: Customer.Name или Customer.Address.Street
/// </summary>
public string Path { get; set; }
/// <summary>
/// Значение для установки в свойство.
/// </summary>
public object Value { get; set; }
}
/// <summary>
/// Билдер модификации.
/// </summary>
public class Builder
{
private readonly List<Change> _changes;
/// <summary>
/// Инициализирует экземпляр <see cref="Builder"/>.
/// </summary>
internal Builder()
{
_changes = new List<Change>();
}
/// <summary>
/// Добавляет модификацию для свойства.
/// </summary>
/// <typeparam name="TProperty">Тип свойства.</typeparam>
/// <param name="propertySelector">Селектор свойства.</param>
/// <param name="value">Новое значение.</param>
/// <returns>Билдер.</returns>
public Builder Assign<TProperty>(Expression<Func<T, TProperty>> propertySelector, TProperty value)
{
_changes.Add(new Change
{
Path = string.Join('.', GetPropertyChain(propertySelector)),
Value = value
});
return this;
}
/// <summary>
/// Создает модификацию объекта.
/// </summary>
public Modification<T> Build() => new Modification<T> { Changes = _changes };
internal static IReadOnlyCollection<string> GetPropertyChain<TProperty>(
Expression<Func<T, TProperty>> expression)
{
static MemberExpression ExtractMember(Expression source)
{
if (source is UnaryExpression unary)
return unary.Operand as MemberExpression;
return source as MemberExpression;
}
var chain = new Stack<string>();
var member = ExtractMember(expression.Body);
while (member != null)
{
chain.Push(member.Member.Name);
member = ExtractMember(member.Expression);
}
return chain;
}
}
}
/// <summary>
/// Тесты для модификации <see cref="Modification{T}"/>.
/// </summary>
public class ModificationTests
{
[Fact]
public void FlatPath_MultipleProperties_Should_Change_Object()
{
// Arrange
var customerId = Guid.NewGuid();
var customer = new Customer
{
Id = customerId,
Name = "John",
LastName = "King"
};
var modification = Modification<Customer>.CreateBuilder()
.Assign(p => p.LastName, "Doe")
.Assign(p => p.Name, "Jake")
.Build();
// Act
var modifiedObject = modification.ApplyTo(customer);
// Assert
modifiedObject.LastName.ShouldBe("Doe");
modifiedObject.Name.ShouldBe("Jake");
modifiedObject.Id.ShouldBe(customerId);
modifiedObject.ShouldBe(customer);
}
[Fact]
public void NestedPath_InitializedNestedObject_Should_Change_Object()
{
// Arrange
var customer = new Customer
{
Address = new Customer.AddressInfo
{
Index = 123,
Street = "Default"
}
};
var modification = Modification<Customer>.CreateBuilder()
.Assign(p => p.Address.Street, "Custom")
.Assign(p => p.Address.Index, 555)
.Build();
// Act
var modifiedObject = modification.ApplyTo(customer);
// Assert
modifiedObject.Address.Index.ShouldBe(555);
modifiedObject.Address.Street.ShouldBe("Custom");
}
[Fact]
public void NestedPath_UninitializedNestedObject_Should_Change_Object()
{
// Arrange
var customer = new Customer { };
var modification = Modification<Customer>.CreateBuilder()
.Assign(p => p.Address.Index, 555)
.Build();
// Act
var modifiedObject = modification.ApplyTo(customer);
// Assert
modifiedObject.Address.Index.ShouldBe(555);
modifiedObject.Address.Street.ShouldBe(null);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment