Search code examples
c#.net-6.0nhibernate-mapping-by-code

Mappings classes into entities using Domain Driven Design with NHibernate as ORM


Hello I want to map class with encapsulated fields. Earlier i used EF Core to map my domain models on entities but this time i want to try something new and i choosed NHibernate as ORM. Below is my domain classes:

public class Order
{
    public virtual EntityId Id { get; }
    public virtual OrderNumber OrderNumber { get; private set; }
    public virtual DateTime Created { get; }
    public virtual string Note { get; private set; } = null;

    public virtual IEnumerable<ProductSale> Products => _products;
    private IList<ProductSale> _products = new List<ProductSale>();

    protected Order() { }

    public Order(EntityId id, OrderNumber orderNumber, DateTime created, string note = null, IEnumerable<ProductSale> products = null)
    {
        Id = id;
        ChangeOrderNumber(orderNumber);
        Created = created;

        if (products != null)
        {
            AddProducts(products);
        }

        Note = note;
    }

    public virtual void ChangeOrderNumber(string orderNumber)
    {
        OrderNumber = orderNumber;
    }

    public virtual void ChangeNote(string note)
    {
        Note = note;
    }

    public virtual void AddProducts(IEnumerable<ProductSale> products)
    {
        // some logic and operations
    }

    public virtual void AddProduct(ProductSale product)
    {
        // some logic and operations
    }

    public virtual void RemoveProduct(ProductSale product)
    {
        // some logic and operations
    }
}

public sealed class ProductSale
{
    public virtual EntityId Id { get; }
    public virtual Order Order { get; private set; } = null;
    public virtual ProductSaleState ProductSaleState { get; private set; } = ProductSaleState.New; // enum

    protected ProductSale() { }

    public ProductSale(EntityId id, ProductSaleState productSaleState, Order order)
    {
        Id = id;
        Order = order;
        ProductSaleState = productSaleState;
    }

    public virtual void AddOrder(Order order)
    {
        // logic
    }

    public virtual void RemoveOrder()
    {
        // logic
    }
}

My value objects:

public class EntityId : IEquatable<EntityId>
{
    public virtual Guid Value { get; protected set; }

    protected EntityId() { }

    public EntityId(Guid value)
    {
        // validation

        Value = value;
    }

    public static EntityId Create() => new(Guid.NewGuid());

    public override bool Equals(object obj)
    {
        return Equals(obj as EntityId);
    }

    public bool Equals(EntityId other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return Value == other.Value;
    }

    public override int GetHashCode()
    {
        return GetEqualityComponents()
                .Select(x => x != null ? x.GetHashCode() : 0)
                .Aggregate((x, y) => x ^ y);
    }

    private IEnumerable<object> GetEqualityComponents()
    {
        yield return Value;
    }
}

public sealed class OrderNumber : IEquatable<OrderNumber>
{
    public virtual string Value { get; protected set; }

    public OrderNumber(string productName)
    {
        ValidProductName(productName);
        Value = productName;
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as OrderNumber);
    }

    public bool Equals(OrderNumber other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return Value == other.Value;
    }

    public override int GetHashCode()
    {
        return GetEqualityComponents()
                .Select(x => x != null ? x.GetHashCode() : 0)
                .Aggregate((x, y) => x ^ y);
    }

    private IEnumerable<object> GetEqualityComponents()
    {
        yield return Value;
    }

    private static void ValidProductName(string productName)
    {
        // validation
    }
}

My mappings:

public sealed class OrderConfiguration : ClassMapping<Order>
{
    public OrderConfiguration ()
    {
        Table("Orders");
        ComponentAsId(a => a.Id, map =>
        {
            map.Access(Accessor.ReadOnly);
            map.Property(id => id.Value, prop =>
            {
                prop.Access(Accessor.ReadOnly);
                prop.Column(nameof(Order.Id));
            });
        });
        Component(a => a.OrderNumber, map =>
        {
            map.Property(ad => ad.Value, prop => 
            {
                prop.Access(Accessor.ReadOnly);
                prop.Column(nameof(Order.OrderNumber));
            });
        });
        Property(a => a.Created, prop=> {
            prop.Access(Accessor.ReadOnly);
            prop.Column(nameof(Order.Created));
        });
        Property(a => a.Note, map => map.Column(nameof(Order.Note)));
        Bag(o => o.ProductSales, map =>
        {
            map.Table("ProductSales");
            map.Key(k => k.Column(col => col.Name("OrderId")));
        }, map => map.OneToMany());
    }
}

public class ProductSaleConfiguration : ClassMapping<ProductSale>
{
    public ProductSaleConfiguration()
    {
        Table("ProductSales");
        ComponentAsId(a => a.Id, map =>
        {
            map.Access(Accessor.ReadOnly);
            map.Property(id => id.Value, prop =>
            {
                prop.Access(Accessor.ReadOnly);
                prop.Column(nameof(ProductSale.Id));
            });
        });
        Property(p => p.ProductSaleState, map => {
            map.Column(nameof(ProductSale.ProductSaleState));
            map.Type<EnumStringType<ProductSaleState>>();
        });
        ManyToOne(p => p.Order, map =>
        {
            map.Column("OrderId");
        });
    }
}

When I inserted object for example order, EntityId was always set as null. I havent used NHibernate as ORM so far, so I couldnt check if mappings were correct. Maybe there is something missing. I used .Net 6, SQLite database.


Solution

  • I solved this problem by implementing own custom IUserType. It is really strange that NHibernate has some problems with mapping wrapped guid as an identitfier. If someone is interested here is implementation

    public sealed class EntityIdConfigurationType : IUserType
    {
        public SqlType[] SqlTypes => new[] { SqlTypeFactory.Guid };
    
        public Type ReturnedType => typeof(EntityId);
    
        public bool IsMutable => false;
    
        public object Assemble(object cached, object owner)
            => DeepCopy(cached);
    
        public object DeepCopy(object value)
            => value;
    
        public object Disassemble(object value)
            => DeepCopy(value);
    
        public new bool Equals(object x, object y)
        {
            if (ReferenceEquals(x, y))
            {
                return true;
            }
    
            if (x == null || y == null)
            {
                return false;
            }
    
            return x.Equals(y);
        }
    
        public int GetHashCode(object x)
            => x.GetHashCode();
    
        public object NullSafeGet(DbDataReader rs, string[] names, ISessionImplementor session, object owner)
        {
            var obj = NHibernateUtil.Guid.NullSafeGet(rs, names[0], session);
            if (obj is null) 
                return null;
            var id = (Guid)obj;
            if (id == Guid.Empty) return null;
            return new EntityId(id);
        }
    
        public void NullSafeSet(DbCommand cmd, object value, int index, ISessionImplementor session)
        {
            if (value is null)
            {
                object nullValue = DBNull.Value;
                NHibernateUtil.Guid.NullSafeSet(cmd, nullValue, index, session);
                return;
            }
    
            var type = value.GetType();
    
            if (type == typeof(Guid))
            {
                NHibernateUtil.Guid.NullSafeSet(cmd, value, index, session);
                return;
            }
    
            EntityId entityId = value as EntityId;
            object valueToSet;
    
            if (entityId != null)
            {
                valueToSet = entityId.Value;
            }
            else
            {
                valueToSet = DBNull.Value;
            }
    
            NHibernateUtil.Guid.NullSafeSet(cmd, valueToSet, index, session);
        }
    
        public object Replace(object original, object target, object owner)
            => original;
    }
    

    and mappings

    public sealed class OrderConfiguration : ClassMapping<Order>
    {
        public AdditionConfiguration()
        {
            Table("Orders");
            Id(p => p.Id, map =>
            {
                map.Column(nameof(Order.Id));
                map.Type<EntityIdConfigurationType>();
            });
            Component(a => a.OrderNumber, map =>
            {
                map.Property(ad => ad.Value, prop => 
                {
                    prop.Access(Accessor.ReadOnly);
                    prop.Column(nameof(Order.OrderNumber));
                });
            });
            Property(a => a.Created, prop=> {
                prop.Access(Accessor.ReadOnly);
                prop.Column(nameof(Order.Created));
            });
            Property(a => a.Note, map => map.Column(nameof(Order.Note)));
            Bag(o => o.ProductSales, map =>
            {
                map.Table("ProductSales");
                map.Key(k => k.Column(col => col.Name("OrderId")));
            }, map => map.OneToMany());
        }
    }
    
    public class ProductSaleConfiguration : ClassMapping<ProductSale>
    {
        public ProductSaleConfiguration()
        {
            Table("ProductSales");
            Id(p => p.Id, map =>
            {
                map.Column(nameof(ProductSale.Id));
                map.Type<EntityIdConfigurationType>();
            });
            Property(p => p.ProductSaleState, map => {
                map.Column(nameof(ProductSale.ProductSaleState));
                map.Type<EnumStringType<ProductSaleState>>();
            });
            ManyToOne(p => p.Order, map =>
            {
                map.Column("OrderId");
            });
        }
    }