Implementing pg Npgsql IUserType

7 views
Skip to first unread message

Michael W Powell

unread,
Jun 10, 2025, 1:13:17 PMJun 10
to Fluent NHibernate
Hello,

Double checking my work implementing IUserType. Implementing to facilitate mapping types to postgres Npgsql. Specifically in one case to JSON/JSONB column types, facilitated by either string, then to Newtonsoft.Json.Linq JObject or JArray, both JContainer, depending on the use case.

Best I can figure, Assemble and Disassemble are somewhat core and central of such an implementation. Around the NullSafeGet and Set, for instance. Almost to a point where it might be worth providing a generic serialization implementation, interface, etc, but starting from here:

public virtual object NullSafeGet(DbDataReader rs, string[] names, ISessionImplementor session, object owner)
{
    this.VerifyNullSafeNames(names: names);

    var name = names[0];
    var ordinal = rs.GetOrdinal(name);
    var value = rs[ordinal];
    return value switch
    {
        null => null,
        P p => Assemble(p, owner),
        _ => throw new InvalidOperationException($"Unable to get null safe value, names: [{string.Join(", ", names)}].")
    };
}

public void NullSafeSet(DbCommand cmd, object value, int index, ISessionImplementor session)
{
    // We expect there to be an parameter of this type.
    if (cmd.Parameters.TryGetValue<NpgsqlParameter>(index, out var arg))
    {
        // Parameter Value may be either null, of types R or P, otherwise throw.
        arg.Value = value switch
        {
            null => null,
            R r => Disassemble(r),
            P p => p,
            _ => throw new InvalidOperationException($"Unable to set null safe value type '{value.GetType()}' index {index}.")
        };

        // Indeed return here since we do not want to throw the default parameter ex.
        return;
    }

    throw new InvalidOperationException($"Unable to set null safe parameter value index {index}.");
}

Here I verify names apart from the implementation. Also I look up the postgres Npgsql parameter in this instance.

But the core of the approach, I think, are Assemble and Disassemble, from what I can gather. Everything revolves around that, including DeepCopy.

Mostly everything else is pretty boilerplate, in my estimation, so any specialization can focus on the A/D overrides. Sometimes perhaps also the Equals.

public abstract class NpgsqlJsonCustomTypeBase<P, R> : IUserType
{
    // ...
}

Have verified through mappings and to a test project, seems to satisfy things.

Posting here in case I am missing something, perhaps there are gaps I am unaware of.

Appreciate the feedback.

Best regards,

Michael W. Powell

Michael W Powell

unread,
Jun 10, 2025, 2:53:08 PMJun 10
to Fluent NHibernate
Extending that thought a bit, from what I could determine Equals is a bit nuanced as well. First argument x appears to be cached or disassembled, whereas it would seem second argument y appears to be assembled version of the same.

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

public new virtual bool Equals(object x, object y)
{
    object Normalize(object value) => value switch

    {
        null => null,
        R r => Disassemble(r),
        P p => p,
        _ => throw new InvalidOperationException($"Unsupported equality value type '{value.GetType()}'.")
    };

    x = Normalize(x);
    y = Normalize(y);

    return (x is null && y is null)
        || ReferenceEquals(x, y)
        || (x is P xp && y is P yp && Equals(xp, yp))
        //|| (x is R xr && y is R yr && Equals(xr, yr))
        ;
}

It's unlikely probably would see an R clause (returned class), as contrasted with the P clause (primitive class).

The Normalize local function is also geared at any rate along the same lines to compare vis-a-vis the primitive type.

Then Equals(P, P), after having ruled out null and reference equality, base implementation whether P is IEquatable<P> can just do it then.

Allowing for authors to extend as needed.

Michael W Powell

unread,
Jun 10, 2025, 3:02:48 PMJun 10
to Fluent NHibernate
Last but not least is the enigmatic DeepCopy... reading some of the drudged up old blogs, different folks interpretations over IUserType, take aways are all over the map. Best I can figure, and from debugging, and since you are not given 'object owner', either...

One has to disassembly the value. Whereas assembling a value you are given the owner. So I again take away the reasonable conclusion that Assembly and Disassembly are central.

public virtual object DeepCopy(object value) => value switch

{
    null => null,
    R r => Disassemble(r),
    _ => throw new InvalidOperationException($"Unable to deep copy value type '{value.GetType()}'.")
};

Reply all
Reply to author
Forward
0 new messages