Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to implement the IDbSet<T> interface in F#?

I am attempting to make a mock implementation of IDbSet<T>, and I happen to be doing it in F#.

type MockDbSet<'T when 'T : not struct>(items:seq<'T>) =
    let collection = ResizeArray(items)

    new () = MockDbSet(Seq.empty)

    interface IDbSet<'T> with
        member x.Add entity = collection.Add entity; entity
        member x.Attach entity = collection.Add entity; entity
        member x.Remove entity = collection.Remove entity |> ignore; entity
        member x.Create() = Unchecked.defaultof<'T>
        member x.Create<'TD when 'TD : not struct and 'TD :> 'T>() = Unchecked.defaultof<'TD>
        member x.Find([<ParamArray>] values) = raise <| NotImplementedException()
        member x.Local = Collections.ObjectModel.ObservableCollection(collection)

    interface System.Collections.Generic.IEnumerable<'T> with
        member x.GetEnumerator() = 
            collection.GetEnumerator() :> System.Collections.Generic.IEnumerator<_>

    interface System.Collections.IEnumerable with
        member x.GetEnumerator() =
            collection.GetEnumerator() :> System.Collections.IEnumerator

    interface IQueryable<'T> with
        member x.ElementType = typeof<'T>
        member x.Expression =
            collection.AsQueryable().Expression
        member x.Provider =
            collection.AsQueryable().Provider

Everything is fine, except for this line:

member x.Create<'TD when 'TD : not struct and 'TD :> 'T>() = Unchecked.defaultof<'TD>

...which gives me these compiler errors:

error FS0698: Invalid constraint: the type used for the constraint is sealed, which means the constraint could only be satisfied by at most one solution

warning FS0064: This construct causes code to be less generic than indicated by the type annotations. The type variable 'TD has been constrained to be type ''T'.

error FS0663: This type parameter has been used in a way that constrains it to always be ''T when 'T : not struct'

error FS0661: One or more of the explicit class or function type variables for this binding could not be generalized, because they were constrained to other types

This line is attempting to implement this method, which according to that page has the following signature in C#:

TDerivedEntity Create<TDerivedEntity>()
where TDerivedEntity : class, TEntity

And this signature in F#:

abstract Create : unit -> 'TDerivedEntity  when 'TDerivedEntity : not struct and 'TEntity

When I try to use the example F# signature, I get a variety of syntax errors, which doesn't surprise me because that signature doesn't even look like valid F#.

I'm not really sure what to make of these error messages, or how to write my constraints to satisfy both the interface and the F# compiler. I'm starting to wonder if it's even possible to implement this particular Microsoft interface in this particular Microsoft programming language. Any suggestions would be welcomed.

like image 479
Joel Mueller Avatar asked May 14 '14 00:05

Joel Mueller


2 Answers

The method Create needs a subtype constraint between 2 generic type parameters. I'm afraid there is no way to add a subtype constraint to a generic type parameter based on another one in F#. They're always assumed to be equal, see the spec New constraints of the form type :> 'b are solved again as type = 'b.

See this related answer to a similar problem.

We should request to include this feature in the next F# version.

like image 89
Gus Avatar answered Sep 29 '22 16:09

Gus


I was very disappointed by this at first. I still am in some ways, but there is a workaround in EF6. You can inherit DbSet<'TEntity> directly, and use overrides to implement the collection in memory. This will suffice for most cases; you can inherit from this type if you want a concrete implementation of Find.

type FakeDbSet<'TEntity when 'TEntity : not struct>(items: seq<'TEntity>) =
    inherit DbSet<'TEntity>()
    let data = ObservableCollection<'TEntity>(items)
    let query = data.AsQueryable()
    new() = FakeDbSet(Seq.empty)
    override __.Add(item: 'TEntity) = data.Add(item); item
    override __.Remove(item: 'TEntity) = data.Remove(item) |> ignore; item
    override __.Attach(item: 'TEntity) = data.Add(item); item
    override __.Create() = Activator.CreateInstance<'TEntity>()
    override __.Local with get() = data
    interface System.Collections.Generic.IEnumerable<'TEntity> with
        member __.GetEnumerator() = data.GetEnumerator() :> System.Collections.Generic.IEnumerator<_>
    interface System.Collections.IEnumerable with
        member __.GetEnumerator() = data.GetEnumerator() :> System.Collections.IEnumerator
    interface IQueryable<'TEntity> with
        member __.ElementType = typeof<'TEntity>
        member __.Expression = query.Expression
        member __.Provider = query.Provider
like image 28
Rob Lyndon Avatar answered Sep 29 '22 18:09

Rob Lyndon