Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Discriminated unions in NHibernate

I'm wondering if there's any relatively easy way to extend NHibernate to support F#'s discriminated union. Not just a single IUserType or ICompositeUserType, but something generic that I can re-use regardless of the actual contents of the DU.

For example, suppose I have a property called RequestInfo, which is a union defined as:

type RequestInfo =  
    | Id of int
    | Name of string

This compiles into an abstract RequestInfo class, with concrete subclasses Id and Name. I can get all this info out just fine with F# reflection. In this case, I could store it in the database with "RequestInfo_Tag", "RequestInfo_Id", "RequestInfo_Name".

As I'm a NHibernate newbie, what kind of problems am I going to run into trying to follow this approach? Are more complex cases going to be impossible to deal with? For example, what about nested discriminated unions? Is there a way I can "hand off" the reading of the rest of the union to another ICompositeUserType?

More importantly, will this mess up my querying capabilities? Meaning, will I have to know the actual column names in the DB; I won't be able to do Criteria.Eq(SomeDiscUnion) and have it all sorted out?

I'm not looking for a complete "provide code" answer, just some general advice if this is even worth going after (and some pointers on how), or if I should just rethink my model.

Thanks!

P.S. Not to be rude, but if your answer consists of "use C#", it's not very helpful.

like image 369
MichaelGG Avatar asked Oct 26 '09 07:10

MichaelGG


People also ask

What are discriminated union?

A discriminated union is a union data structure that holds various objects, with one of the objects identified directly by a discriminant. The discriminant is the first item to be serialized or deserialized. A discriminated union includes both a discriminant and a component.

What is a discriminated union C#?

Discriminated Unions are a functional programming convenience that indicates that something is one of several different types of objects. For example, a User might be an unauthenticated user, a regular user, or an administrator.

When to use discriminated union?

Discriminated unions are useful for heterogeneous data; data that can have special cases, including valid and error cases; data that varies in type from one instance to another; and as an alternative for small object hierarchies. In addition, recursive discriminated unions are used to represent tree data structures.


1 Answers

I've not been brave enough to try using NHibernate with F#'s type system, but it might help to look from the perspective of what's actually generated by the F# compiler.

If you look at your Discriminated Union in reflector, there are actually three classes generated (and more if you count the private debug proxies).

public abstract class RequestInfo : IStructuralEquatable, IComparable, IStructuralComparable

The first class, RequestInfo, is abstract, and is actually implemented by the other types in the union.

 // Nested Types
    [Serializable, DebuggerTypeProxy(typeof(Program.RequestInfo._Id@DebugTypeProxy)), DebuggerDisplay("{__DebugDisplay()}")]
    public class _Id : Program.RequestInfo
    {
        // Fields
        [DebuggerBrowsable(DebuggerBrowsableState.Never), CompilerGenerated, DebuggerNonUserCode]
        public readonly int id1;

        // Methods
        [CompilerGenerated, DebuggerNonUserCode]
        public _Id(int id1);
    }
    [Serializable, DebuggerTypeProxy(typeof(Program.RequestInfo._Name@DebugTypeProxy)), DebuggerDisplay("{__DebugDisplay()}")]
    public class _Name : Program.RequestInfo
    {
        // Fields
        [DebuggerBrowsable(DebuggerBrowsableState.Never), CompilerGenerated, DebuggerNonUserCode]
        public readonly string name1;

        // Methods
        [CompilerGenerated, DebuggerNonUserCode]
        public _Name(string name1);
    }

so when you do:

 let r=Id(5)
 let s=Name("bob")

r and s are instances of _Id and _Name, respectively.

So the answer to your question is likely the answer to one of the following questions:

  • How do I map to an abstract class in nhibernate?
  • How can I make NHibernate use a factory method?
  • How can I create map Nhibernate to immutable objects?
  • How do I do implement a custom type in NHibernate (presumably with IUserType).

Unfortunately, I'm not savvy enough to give you a coherent answer to any of those, but I'm sure someone else here has done at least one of these three solutions.

I'd like to think that you can use the same methods used for Inheritance Strategies, using, for example, a discriminator column, but I'm afraid the lack of a default constructor makes this problematic. So I'm inclined to think that using a custom type is the solution.

After some fiddling, here's a (possibly buggy and or broken) custom user type:

type RequestInfo =  
    | Id of int
    | Name of string

type RequestInfoUserType() as self =
    interface IUserType with
        member x.IsMutable = false
        member x.ReturnedType = typeof<RequestInfo>
        member x.SqlTypes = [| NHibernate.SqlTypes.SqlType(Data.DbType.String); NHibernate.SqlTypes.SqlType(Data.DbType.Int32); NHibernate.SqlTypes.SqlType(Data.DbType.String) |]
        member x.DeepCopy(obj) = obj //Immutable objects shouldn't need a deep copy
        member x.Replace(original,target,owner) = target // this might be ok
        member x.Assemble(cached, owner) = (x :> IUserType).DeepCopy(cached)
        member x.Disassemble(value) = (x :> IUserType).DeepCopy(value)

        member x.NullSafeGet(rs, names, owner)=
            // we'll use a column as a type discriminator, and assume the first mapped column is an int, and the second is a string.
            let t,id,name = rs.GetString(0),rs.GetInt32(1),rs.GetString(2) 
            match t with
                | "I" -> Id(id) :> System.Object
                | "N" -> Name(name) :> System.Object
                | _ -> null
        member x.NullSafeSet(cmd, value, index)=
            match value with
                | :? RequestInfo ->
                    let record = value :?> RequestInfo
                    match record with
                        | Id(i) ->
                            cmd.Parameters.Item(0) <- "I"
                            cmd.Parameters.Item(1) <- i
                        | Name(n) ->
                            cmd.Parameters.Item(0) <- "N"
                            cmd.Parameters.Item(2) <- n
                | _ -> raise (new  ArgumentException("Unexpected type"))

        member x.GetHashCode(obj) = obj.GetHashCode()
        member x.Equals(a,b) = 
            if (Object.ReferenceEquals(a,b)) then
                true
            else
                if (a=null && b=null) then
                    false
                else
                    a.Equals(b)
    end

This code could surely be made more generic, and should probably not be in your actual domain layer, but I thought it would be useful to take a stab at a F# implementation of IUserType.

Your mapping file would then do something like:

<property name="IdOrName" type="MyNamespace.RequestInfoUserType, MyAssembly"  >
  <column name="Type"/>
  <column name="Id"/>
  <column name="Name"/>
</property>

You probably can get away without a column for "Type" with a slight tweak to the custom UserType code.

I don't know how these custom user types work with queries/ICriteria, as I haven't really worked with custom user types much before.

like image 77
JasonTrue Avatar answered Oct 19 '22 19:10

JasonTrue