onebeyond/onebeyond-studio-core

View on GitHub
src/OneBeyond.Studio.Domain.SharedKernel/Entities/ValueObject.cs

Summary

Maintainability
A
1 hr
Test Coverage
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Logging;
using OneBeyond.Studio.Crosscuts.Logging;

namespace OneBeyond.Studio.Domain.SharedKernel.Entities;

/// <summary>
/// source: https://github.com/jhewlett/ValueObject
/// </summary>
public abstract class ValueObject : IEquatable<ValueObject>
{
    private static readonly ILogger Logger = LogManager.CreateLogger<ValueObject>();

    private readonly Lazy<IReadOnlyList<PropertyInfo>> _properties;
    private readonly Lazy<IReadOnlyList<FieldInfo>> _fields;

    /// <summary>
    /// </summary>
    protected ValueObject()
    {
        _properties = new Lazy<IReadOnlyList<PropertyInfo>>(
            () =>
            {
                return GetType()
                    .GetProperties(BindingFlags.Instance | BindingFlags.Public)
                    .Where(p => p.GetCustomAttribute(typeof(IgnoreMemberAttribute)) == null)
                    .ToList();
            });
        _fields = new Lazy<IReadOnlyList<FieldInfo>>(
            () =>
            {
                return GetType()
                    .GetFields(BindingFlags.Instance | BindingFlags.Public)
                    .Where(p => p.GetCustomAttribute(typeof(IgnoreMemberAttribute)) == null)
                    .ToList();
            });
    }

    /// <summary>
    /// Checks whether 2 value objects are the same based on their types and values
    /// of public properties and fields which are not marked by <see cref="IgnoreMemberAttribute"/>
    /// as well as on their types.
    /// Note: If a value object of one type compared with a value object of derived type,
    /// it gives a false result even though properties have the same values in part of base class.
    /// </summary>
    /// <param name="obj1"></param>
    /// <param name="obj2"></param>
    /// <returns></returns>
    public static bool operator ==(ValueObject? obj1, ValueObject? obj2)
        => Equals(obj1, null)
            ? Equals(obj2, null)
            : obj1.Equals(obj2);

    /// <summary>
    /// Checks whether 2 value objects are different based on their types and values
    /// of public properties and fields which are not marked by <see cref="IgnoreMemberAttribute"/>
    /// as well as on their types.
    /// Note: If a value object of one type compared with a value object of derived type,
    /// it gives a false result even though properties have the same values in part of base class.
    /// </summary>
    /// <param name="obj1"></param>
    /// <param name="obj2"></param>
    /// <returns></returns>
    public static bool operator !=(ValueObject? obj1, ValueObject? obj2)
        => !(obj1 == obj2);

    /// <summary>
    /// Checks whether 2 value objects are the same based on their types and values
    /// of public properties and fields which are not marked by <see cref="IgnoreMemberAttribute"/>
    /// as well as on their types.
    /// Note: If a value object of one type compared with a value object of derived type,
    /// it gives a false result even though properties have the same values in part of base class.
    /// <param name="obj"></param>
    /// <returns></returns>
    /// </summary>
    public bool Equals(ValueObject? obj)
        => Equals(obj as object);

    /// <summary>
    /// Checks whether 2 value objects are different based on their types and values
    /// of public properties and fields which are not marked by <see cref="IgnoreMemberAttribute"/>
    /// as well as on their types.
    /// Note: If a value object of one type compared with a value object of derived type,
    /// it gives a false result even though properties have the same values in part of base class.
    /// </summary>
    /// <param name="obj"></param>
    /// <returns></returns>
    public override bool Equals(object? obj)
    {
        if (obj is null)
        {
            return false;
        }

        var thisType = GetType();
        var objType = obj.GetType();

        if (thisType != objType)
        {
            if (thisType.IsAssignableFrom(objType)
                || objType.IsAssignableFrom(thisType))
            {
                Logger.LogWarning($"Attempt to compare value object of a derived type detected: {thisType.FullName} vs {objType.FullName}");
            }
            return false;
        }

        return _properties.Value.All(p => PropertiesAreEqual(obj, p))
            && _fields.Value.All(f => FieldsAreEqual(obj, f));
    }

    /// <summary>
    /// </summary>
    /// <returns></returns>
    public override int GetHashCode()
    {
        unchecked   //allow overflow
        {
            var hash = 17;

            hash = _properties.Value
                .Aggregate(hash, (h, pi) => HashValue(h, pi.GetValue(this, null)!));

            hash = _fields.Value
                .Aggregate(hash, (h, fi) => HashValue(h, fi.GetValue(this)!));

            return hash;
        }
    }

    private bool PropertiesAreEqual(object obj, PropertyInfo p)
        => Equals(p.GetValue(this, null), p.GetValue(obj, null));

    private bool FieldsAreEqual(object obj, FieldInfo f)
        => Equals(f.GetValue(this), f.GetValue(obj));

    private static int HashValue(int seed, object value)
    {
        var currentHash = value != null
            ? value.GetHashCode()
            : 0;
        return seed * 23 + currentHash;
    }
}