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
On Saturday, July 26, 2025 at 6:56:26 AM UTC+2 Michael W Powell wrote:
> 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?
> On Friday, July 25, 2025 at 11:46:28 PM UTC-4 Michael W Powell wrote:
>
>> 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...
>>
>> On Friday, July 25, 2025 at 11:25:43 PM UTC-4 Michael W Powell wrote:
>>
>>> 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.
>>>
>>> On Friday, July 25, 2025 at 10:58:19 PM UTC-4 Michael W Powell wrote:
>>>
>>>> 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
>>>>
>>>>
--
You received this message because you are subscribed to the Google Groups
"Fluent NHibernate" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion visit
https://groups.google.com/d/msgid/fluent-nhibernate/e7d99987-fa69-4028-bf1a-e000f49cbc2fn%40googlegroups.com.