Implementing IUserType stack overflows

19 views
Skip to first unread message

Michael W Powell

unread,
Jul 25, 2025, 10:58:19 PMJul 25
to Fluent NHibernate
Hello,

I am at a point now implementing some user types, things are loading from the database correctly, I can work with the models, make adjustments via services and WPF UI views, view models, all that is working beautifully.

Now I am doing the last leg of the game plan: persisting back to the database, which as I can gather from the traces, StackOverflowExceptions, etc, revolves around negotiating Equals: A LOT, and a lot of navigation to and/or from assembed and/or dissassembled form factors. Which depending upon the user type implementation, can be tricky.

Which is part partial my question. What is the general expection, i.e. persistence 'protocol' from an NHibernate perspective, navigating the persistence conversation.

Our approach is also generally to implement a P or R based generic user type at base (i.e. primitive versus returned types), especially when we want to do things such as negotiate NodaTime constructs, sometimes also JSON based Newtonsoft.Json.Linq constructs; whch as I mentioned, works beautifully querying and loading from the database.

From what I can also determine, user types sometimes also cached, although from the lack of documentation, we are not hundred percent clear in which form, either P or R.

I've implemented the assembly and disassembly generally to use switch expressions with strategically placed pattern matching in order to isolate P from R in a broad range of use cases. But still finding a StackOverflow slip through the cracks here and there.

Overall, we are familiar with ORM in general, usually involving comparison between two datasets, so I'd guess minimally at a primitive level, but what is cached, the assembled version, and to make the apporpriate comparison that does not blow up the stack.

So I am here to ask the question: what sort of protocol, assy, disassy, caching, can we expect, negotiating the persistence sequence?

Thanks!

Best,

Michael W. Powell

Michael W Powell

unread,
Jul 25, 2025, 11:25:43 PMJul 25
to Fluent NHibernate
Perhaps making it harder than it needs to be. The interfaces both indicating literally equality and hash codes _of the persistent state_ that is of the P primitive types, I gather. If so no biggie, that's a simple correction to make.

Michael W Powell

unread,
Jul 25, 2025, 11:46:28 PMJul 25
to Fluent NHibernate
The docs as do the comments claim Equals compares the _persistent state_ which I take to mean the PRIMITIVE (P) type.

Take for instance, we have a DECIMAL database mapping, to a CLR UINT64 in the apporpriate scale and precision.

Equals is not seeing DECIMAL, which I would expect, but rather, UINT64. Which is part and partial my confusion over this issue.

In the following base implementation, assuming NH docs, comments, etc, are accurate, then this should never throw. But it is throwing, because object value is UINT64.

public virtual new bool Equals(object x, object y)
{
    static object Normalize(object value) => value switch
    {
        DBNull or null => DBNull.Value,
        P p => p,
        _ => throw new InvalidOperationException($"Unexpected value type: '{value.GetType().FullName}'.")
    };

    var (xp, yp) = (Normalize(x), Normalize(y));

    return (xp is null && yp is null)
        || (xp is DBNull && yp is DBNull)
        || ReferenceEquals(x, y)
        || Equals(xp, yp);
}

So my guess is, we want to be comparing values of type P, but can also see values of type R (returned types), so need to disassemble them accordingly, presumably from cached.

Anyone with any insight into this, please chime in? Thanks...

Michael W Powell

unread,
Jul 26, 2025, 12:56:26 AMJul 26
to Fluent NHibernate
From what I can tell, this is so far doing the right thing from an Equals perspective. We prefer to see the primitive P type by default, but apparently can also see the returned R type, so normalize accordingly.

protected virtual bool Equals(P x, P y) => x is IEquatable<P> xe && y is IEquatable<P> ye && xe.Equals(ye);


public virtual new bool Equals(object x, object y)
{
    object Normalize(object value) => value switch
    {
        DBNull or null => DBNull.Value,
        P p => p,
        R r => Disassemble(r),

        _ => throw new InvalidOperationException($"Unexpected value type: '{value.GetType().FullName}'.")
    };

    var (xnorm, ynorm) = (Normalize(x), Normalize(y));

    return (xnorm is null && ynorm is null)
        || (xnorm is DBNull && ynorm is DBNull)
        || ReferenceEquals(x, y)
        || (xnorm is P xp && ynorm is P yp && Equals(xp, yp));
}

Another concern is getting the hash code; ostensibly we think this may suffer from the same P versus R behavior patterns as Equals exhibits. Initially taking the naive approach that we are expecting the primitive P type and that's it. But wondering if we might also see the returned R type there as well.

protected virtual int GetHashCode(P p) => p?.GetHashCode() ?? default;

public virtual int GetHashCode(object x) => x is P p ? GetHashCode(p) : throw new InvalidOperationException(
    $"Value {(x is null ? "null" : $"{x}")} of the incorrect type: '{(x is null ? "null" : typeof(P).FullName)}'"
);

Taking a step back from equality and hash codes, could that perhaps be an indication that one of the cache directions may be assembling or disassembling a value somehow incorrectly, perhaps? Which is entirely possible; although overall we went to some lengths to ensure that the diretion was properly, from database assemble, and to database disassemble.

Thoughts? Anyone?

Oskar Berggren

unread,
Jul 26, 2025, 5:52:52 AMJul 26
to Fluent NHibernate
I am not 100% sure I understand what you consider "primitive" versus "returned". By returned, do you mean what NullSafeGet() returns? I.e. objects of the class as seen in the domain model?

For the stack overflow, in your first code sample it looks like infinite recursion:

public virtual new bool Equals(object x, object y)  // hides the static object.Equals(x,y). May consider using explicit implementation ... IUserType.Equals(x, y) to avoid
{
[...]
        || Equals(xp, yp);   // xp, yp are typed as object
}

But you later example seems to have fixed that.


As for what types to handle. In my own implementation I always handle the mapped type (only!) in Equals() and GetHashCode(). "mapped type" is the class used in the domain model. They must handle null, but I've never accounted for DBNull in those. This has worked fine for many years.

Another relevant aspect to remember is that NullSafeSet() can set *multiple* parameters, NullSafeGet() can read multiple columns, and this is also why the SqlTypes property returns an array. It would not really make sense for NHibernate to pack multiple values into some opaque instance of type object and pass that back to the IUserType implementation.

So in summary, what I always do is:

NullSafeGet() => important point is to be prepared to get DBNull from the database and in such case return regular .NET null, or whatever is appropriate. I never let the DBNull leak into the returned value.

NullSafeSet() => if the value from the domain model is or contains null, maybe need to write DBNull to the parameter.

Equals(x,y), GetHashCode(x) => deals in the mapped type, not the SQL types. Should handle null, but should never see DBNull.

Assemble()/Disassemble() => I never had a need to implement them so I have not special comments there.

/Oskar
Reply all
Reply to author
Forward
0 new messages