4

I have a block of code that I want to write in F#, but the examples I have are in C#. I would like some help to write this in the F# language, and help understanding how it works.

Here is the c# code I have to mimic:

builder.HasMany(r => r.Options).WithOne(o => o.Root).HasForeignKey(o => o.RootId).OnDelete(DeleteBehavior.Cascade);

In F#, I am trying to do this:

builder
    .HasOne(fun i -> i.ProductionReport) 
    .WithMany(fun pr -> pr.CostItems)
    .HasForeignKey(fun pr -> pr.ProductionReportId).OnDelete(DeleteBehavior.Cascade) |> ignore

And the issue, per visual studio, is that pr is of type obj. How do I make sure f# knows that pr is of type ProductionReport, according to the return type of builder.HasOne.

Here is the complete sample requested:

BackendDemoDbContext

namespace BackendDemo.BackendDemoContext

open Microsoft.EntityFrameworkCore

type BackendDemoContext(options: DbContextOptions<BackendDemoContext>) =
    inherit DbContext(options)


    override __.OnModelCreating modelbuilder =         
        //Todo:
        //modelbuilder.ApplyConfiguration(new CostItemEntityTypeConfiguration());        
        //modelbuilder.ApplyConfiguration(new ProductionReportEntityTypeConfiguration());

CostItem

namespace BackendDemo.Data.Models

type CostItem() = 
    member val CostItemId = null with get, set
    member val Paper1 = null with get, set    
    member val Paper2 = null with get, set
    member val Cases = null with get, set
    member val Boxes = null with get, set
    member val Paste = null with get, set
    member val Bundling = null with get, set
    member val Ink = null with get, set
    member val Cardboard = null with get, set
    member val Wrapping = null with get, set
    member val Labour = null with get, set
    member val Fringe = null with get, set
    member val Pallet = null with get, set

    member val ProductionReportId =null with get,set
    member val ProductionReport = null with get, set

ProductionReport

namespace BackendDemo.Data.Models

open System.Collections
open BackendDemo.Data.Models

type ProductionReport() = 
    //val keyword necessary for AutoProperties
    member val ProductionReportId : int = 2
    //Todo:
    //abstract member CostItems : ICollection<CostItem> with get, set

CostItemEntityTypeConfiguration

namespace BackendDemo.Data.EntityConfigurations

open Microsoft.EntityFrameworkCore
open Microsoft.EntityFrameworkCore.Metadata.Builders
open BackendDemo.Data.Models

type CostItemEntityTypeConfiguration =
    interface IEntityTypeConfiguration<CostItem> with

        override this.Configure(builder: EntityTypeBuilder<CostItem>) =
            builder.ToTable("CostItem") |> ignore
            builder.HasKey(fun i -> i.CostItemId) |> ignore
            builder.Property(fun i -> i.Paper1).IsRequired() |> ignore
            builder.Property(fun i -> i.Paper2).IsRequired() |> ignore
            builder.Property(fun i -> i.Cases).IsRequired() |> ignore
            builder.Property(fun i -> i.Boxes).IsRequired() |> ignore
            builder.Property(fun i -> i.Paste).IsRequired() |> ignore
            builder.Property(fun i -> i.Bundling).IsRequired() |> ignore
            builder.Property(fun i -> i.Ink).IsRequired() |> ignore
            builder.Property(fun i -> i.Cardboard).IsRequired() |> ignore
            builder.Property(fun i -> i.Wrapping).IsRequired() |> ignore
            builder.Property(fun i -> i.Labour).IsRequired() |> ignore
            builder.Property(fun i -> i.Fringe).IsRequired() |> ignore
            builder.Property(fun i -> i.Pallet).IsRequired() |> ignore

            builder
                .HasOne(fun i -> i.ProductionReport) 
                .WithMany(fun pr -> pr.CostItems)
                .HasForeignKey(fun pr -> pr.ProductionReportId).OnDelete(DeleteBehavior.Cascade) |> ignore

ProductionReportEntityTypeConfiguration

namespace BackendDemo.Data.EntityConfigurations

open Microsoft.EntityFrameworkCore
open Microsoft.EntityFrameworkCore.Metadata.Builders
open BackendDemo.Data.Models

type ProductionReportEntityTypeConfiguration =
    interface IEntityTypeConfiguration<ProductionReport> with

        override this.Configure(builder: EntityTypeBuilder<ProductionReport>) =
            builder.ToTable("ProductionReport") |> ignore
            //Todo
            ///builder.HasKey(fun r -> r.ProductionReportId) |> ignore

Here are the results of the suggestions below (thanks by the way!):

  • 1 Try forcing an argument type
builder
    .HasOne(fun i -> i.ProductionReport) 
    .WithMany(fun (pr: ProductionReport) -> pr.CostItems)

Result

  • 2 Use the alternative Function syntax
builder
    .HasOne(<@ fun i -> i.ProductionReport @>) 
    .WithMany(<@ fun pr -> pr.CostItems @>)

Result

  • 3 Use the <@ notation with specific type
builder
    .HasOne(<@ Func<ProductionReport,_> fun i -> i.ProductionReport @>) 
    .WithMany(<@ Func<CostItem,_> fun pr -> pr.CostItems @>)

Result

  • 4 Factorize the Expression solution from Nathan
static member toExpr (f:'a -> 'b) = 
    <@ Func<_,_> (f) @> 
    |> LeafExpressionConverter.QuotationToExpression 
    |> unbox<Expression<Func<'a, 'b>>>

Factorization class

Result

  • 5 Factorize the Expression with type notation suggested by Nathan
    static member toExpr<'a, 'b> (f:'a -> 'b) = 
        <@ Func<_,_> (f) @> 
        |> LeafExpressionConverter.QuotationToExpression 
        |> unbox<Expression<Func<'a, 'b>>>

Result

  • Can you provide a more complete sample so we can see the types of the builder and the DbContext / entities involved here? – Aaron M. Eshbach Mar 28 '19 at 18:02
  • I added the full code, there is a lot I have to fix. If you have also other improvements to suggestion I am open. – Philippe Adib Mar 28 '19 at 18:34
  • 1
    You can put a type annotation on the value inside the lambda expression: `.WithMany(fun (pr : ProductionReport) -> pr.CostItems)`. I find it a little odd that it should be necessary to do so, but I have next to no experience with EF, so who knows.... – TeaDrivenDev Mar 28 '19 at 22:02
  • 1
    I'm assuming you're using the overloads for `HasOne` and `WithMany` that take in an `Expression` as input. In C#, you use the "fat arrow" (`=>`) syntax for _both_ Expressions and Func/Action. In F#, Expressions are treated separately with a different syntax. I've not worked with these EF methods, but you might try the F# expression syntax: `.HasOne(<@ fun i -> i.ProductionReport @>).WithMany(<@ fun pr -> pr.CostItems @>)` and see if that works. – Nathan Wilson Mar 28 '19 at 22:11
  • @NathanWilson and TeaDrivenDev and I tried your solutions and I updated my question at the bottom (last 2 points). – Philippe Adib Mar 28 '19 at 23:06
  • You can keep all classes that deal with Fluent API in C# as rewriting them in F# is always ugly in C# and call them from F#. – abatishchev Mar 28 '19 at 23:11
  • 1
    Ah, okay, I think we might be getting somewhere. It says that `Expr<'b -> 'c>` is not compatible with `Expression>`. In many cases, F# functions (`'b -> 'c`) are automatically converted to C# functions (`Func<'b, 'c>`) but not always. It looks like in this case you might have to do the conversion yourself. I would try `.HasOne(<@ Func(fun i -> i.ProductionReport) @>).WithMany(<@ Func(fun pr -> pr.CostItems) @>)` Not sure if that's it, but that should at least make the func types compatible. – Nathan Wilson Mar 28 '19 at 23:19
  • 1
    @abatishchev might be right in this case that either keeping the EF stuff in C# and referencing it from an F# project, or making some kind of wrapper around this EF code could be nice, because some of the F# syntax for working with these expressions is getting a little tedious... – Nathan Wilson Mar 28 '19 at 23:22
  • @NathanWilson and abatishchev, I prefer to go with a pure F# solution for now because I want to avoid relying on C# as much as possible. I am also doing this as an exercise to learn how the F# syntax works, as ugly as it gets, because I want to use these cases as a stepping stone to learn the language in general. I updated my question to show the result of your fix. I also added back F# to the question title because I want this to be about F#, not C#. – Philippe Adib Mar 29 '19 at 14:42

1 Answers1

1

I think I got it, but it took some digging to figure out how to work with the expressions. I referenced this post's history to see how to build a System.Linq.Expressions.Expression. Here's what I have:

open System.Linq.Expressions
open Microsoft.FSharp.Linq.RuntimeHelpers

...

let toProdRptExpr : Expression<Func<CostItem, ProductionReport>> =
  <@ Func<_, _> (fun (i:CostItem) -> i.ProductionReport) @>
  |> LeafExpressionConverter.QuotationToExpression 
  |> unbox<Expression<Func<CostItem, ProductionReport>>>

let toCostItemsExpr : Expression<Func<ProductionReport, seq<CostItem>>> = 
  <@ Func<_,_> (fun (pr:ProductionReport) -> pr.CostItems) @>
  |> LeafExpressionConverter.QuotationToExpression 
  |> unbox<Expression<Func<ProductionReport, seq<CostItem>>>>

let a = builder.HasOne(toProdRptExpr)
let b = a.WithMany(toCostItemsExpr)

that's a lot more verbose than it needs to be, but it helped me figure out how the types fit together.

EDIT

For brevity, you can create a function like

let toExpr (f:'a -> 'b) = 
  <@ Func<_,_> (f) @>
  |> LeafExpressionConverter.QuotationToExpression 
  |> unbox<Expression<Func<'a, 'b>>>

and then use it like

builder
  .HasOne(toExpr(fun (i:CostItem) -> i.ProductionReport))
  .WithMany(toExpr(fun (pr:ProductionReport) -> pr.CostItems))

But you have to be careful because it looks like CostItem and ProductionReport are mutually referential (see the discussion in comments below). That means they need to be defined in the same file and use the and keyword (see this example)

Nathan Wilson
  • 629
  • 6
  • 11
  • I was able to make it work. My code is big now, and I wonder: is there any way to wrap this into a function and takes the types and the fun as arguments? For Example: toExpr(CostItem, ProductionReport, (fun (i:CostItem) -> i.ProductionReport)) – Philippe Adib Mar 30 '19 at 08:41
  • 1
    I bet you could do something like: ``` let toExpr (f:’a -> ‘b) = <@ Func<_,_> (f) @> |> LeafExpressionConverter.QuotationToExpression |> unbox>> ``` – Nathan Wilson Mar 30 '19 at 17:55
  • But you should be able to call it with any regular F# function – Nathan Wilson Mar 30 '19 at 17:57
  • I placed the code in my library and tried it. It worked if I use Has One only, but when I follow-up with WithMany it fails. I updated my question with the result. – Philippe Adib Apr 01 '19 at 00:36
  • Weird, it looks like `toExpr(fun (i:CostItem) -> i.ProductionReport)` is returning an `Expression>` instead of an `Expression>`. Now you could probably try throwing additional type annotations in by putting `static member toExpr<'a,'b> (f:'a -> 'b) = ...` and `builder.HasOne(E.toExpr(fun (i:CostItem) -> i.ProductionReport))` and hopefully that works, or at least gives us more info. – Nathan Wilson Apr 01 '19 at 19:41
  • Alright, so I added the type annotations, and it seems to bring me back to the obj type problem for i.ProductReport. I updated the question with the error. – Philippe Adib Apr 02 '19 at 14:04
  • wow, we might have uncovered the deeper issue underlying some of this. It seems like something is wrong with the definition of `CostItem` that its `.ProductionReport` field is just an `obj` and not a `ProductionReport`. I mean we can fix the specific problem here by making a version of `toExpr` that takes in `(f: 'a -> obj)` instead of `(f: 'a -> 'b)`, but I think we need to figure out the underlying problem of why `CostItem.ProductionReport` is just `obj`... – Nathan Wilson Apr 02 '19 at 15:41
  • Found it! It is in my CostItem class but I don't know how to fix it because Costitem type (or class) contains a ProductionReport and ProductionReport type (or class) contains a list of CostItem, and I can't have both orders in the solution explorer (move up, move down). So in the CostItem class, I am not able to force the ProductionReport member to a ProductionReport type with ```member val ProductionReport:ProductionReport = new ProductionReport() with get, set```. In my solution explorer, I put CostItem.fs above ProductionReport.fs. – Philippe Adib Apr 02 '19 at 15:49
  • Well, it looks like I'm at a roadblock here https://fsharpforfunandprofit.com/posts/removing-cyclic-dependencies/ . I think that this kind of object to object two-way dependency is not possible in F#. – Philippe Adib Apr 02 '19 at 15:57
  • 1
    I understand. You _can_ do cyclic dependencies in F#, but the two have to be in the same file. The second type gets declared with the `and` keyword _in place of_ the `type` keyword, and then the two can reference each other. You're right though that cyclic dependencies between different files is disallowed – Nathan Wilson Apr 02 '19 at 16:06
  • The article does not recommend it but I was able to get it to work by putting both types in one file using the ```and``` keyword. ```namespace BackendDemo.Data.Models ... type CostItem() = ... member val ProductionReport:ProductionReport = new ProductionReport() with get, set ... and ProductionReport() = ... member val CostItems : List = [] with get, set``` – Philippe Adib Apr 02 '19 at 16:06
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/191110/discussion-between-philippe-adib-and-nathan-wilson). – Philippe Adib Apr 02 '19 at 16:18