mikependon / RepoDB

A hybrid ORM library for .NET.
Apache License 2.0
1.69k stars 124 forks source link

Question: Question on how to properly configure type mapping for FSharp Option types #483

Open lambdakris opened 4 years ago

lambdakris commented 4 years ago

I am trying to configure mapping for F# option types but have run into an exception that I could not diagnose. Attached is my attempt.

ConsoleApp1.zip

mikependon commented 4 years ago

Would you be able to check this project? I am not the best resource for F#, maybe I can ask the community. @AngelMunoz would you be a help here?

mikependon commented 4 years ago

Or if it is just a technical thing, you can visit the Class Mapping documentation for attributes-based setup, or Implicit Mapping if you wish to setup a mapping without any attributes.

AngelMunoz commented 4 years ago

Hey there, I just took a brief look at the sample

I'm not much experienced in that area either but since RepoDB uses Reflection there might be an issue with F#'s explicit interface usage

I stumbled across that recently (https://github.com/worldbeater/Live.Avalonia/blob/main/Live.Avalonia/LiveControlLoader.cs#L40) if you use a class from F# and want to use it's interface via reflection you must be sure to get the method from the interface type not the concrete class, I'm not sure how RepoDB does that but that would be my best guess and I'm not sure that could be the real problem

that being said... F# types are sometimes complex to interop with C# libraries (and json serialization) I tend to avoid them when writing to the database/serializing to json 😞 and use either DTO's + anonymous records with the shape of the database table with raw queries, you can check these lines to give you an idea

But I'd just use the Execute'Query family

AngelMunoz commented 4 years ago

Just to add to this

Exception has occurred: CLR/System.NullReferenceException An unhandled exception of type 'System.NullReferenceException' occurred in RepoDb.dll: 'Object reference not set to an instance of an object.' 
stacktrace Exception has occurred: CLR/System.NullReferenceException An unhandled exception of type 'System.NullReferenceException' occurred in RepoDb.dll: 'Object reference not set to an instance of an object.' at RepoDb.Reflection.FunctionFactory.<>c__DisplayClass7_0`1.b__0(Int32 entityIndex, Expression instance, ParameterExpression property, DbField dbField, ClassProperty classProperty, Boolean skipValueAssignment, ParameterDirection direction) at RepoDb.Reflection.FunctionFactory.GetDataEntitiesDbCommandParameterSetterFunction[TEntity](IEnumerable`1 inputFields, IEnumerable`1 outputFields, Int32 batchSize, IDbSetting dbSetting) at RepoDb.FunctionCache.DataEntitiesDbCommandParameterSetterFunctionCache`1.Get(String cacheKey, IEnumerable`1 inputFields, IEnumerable`1 outputFields, Int32 batchSize, IDbSetting dbSetting) at RepoDb.FunctionCache.GetDataEntitiesDbCommandParameterSetterFunction[TEntity](String cacheKey, IEnumerable`1 inputFields, IEnumerable`1 outputFields, Int32 batchSize, IDbSetting dbSetting) at RepoDb.DbConnectionExtension.<>c__DisplayClass252_0`1.b__0(Int32 batchSizeValue) at RepoDb.Contexts.Execution.InsertAllExecutionContextCache`1.Get(String tableName, IEnumerable`1 fields, Int32 batchSize, Func`2 callback) at RepoDb.DbConnectionExtension.InsertAllInternalBase[TEntity](IDbConnection connection, String tableName, IEnumerable`1 entities, Int32 batchSize, IEnumerable`1 fields, String hints, Nullable`1 commandTimeout, IDbTransaction transaction, ITrace trace, IStatementBuilder statementBuilder, Boolean skipIdentityCheck) at RepoDb.DbConnectionExtension.InsertAllInternal[TEntity](IDbConnection connection, IEnumerable`1 entities, Int32 batchSize, String hints, Nullable`1 commandTimeout, IDbTransaction transaction, ITrace trace, IStatementBuilder statementBuilder) at RepoDb.DbConnectionExtension.InsertAll[TEntity](IDbConnection connection, IEnumerable`1 entities, Int32 batchSize, String hints, Nullable`1 commandTimeout, IDbTransaction transaction, ITrace trace, IStatementBuilder statementBuilder) at Program.main(String[] argv) in C:\Users\scyth\Downloads\ConsoleApp1\ConsoleApp1\Program.fs:line 61

which leads me think this might be the method with the issue it might as well happen with the Getter https://github.com/mikependon/RepoDb/blob/b1e0b5a9a0d9ec938a33688cefe20268d517d7e1/RepoDb/RepoDb/Reflection/FunctionFactory.cs#L984-L989

mikependon commented 4 years ago

Thanks @AngelMunoz for the reply.

F# and want to use it's interface via reflection you must be sure to get the method from the interface type not the concrete class, I'm not sure how RepoDB does that but that would be my best guess and I'm not sure that could be the real problem

This is handled internally, the type references of the underlying interface used by the class is always passed before the compilation. If that reference is null, then I used the DeclaringType property in the compilation. So that would fix the problem there.

The only problem that I know in relation to F# is that, F# classes are immutable and that RepoDb would not work there until you specific the CLIMutable attribute.

Maybe you can guide the guy how to call the FluentMapper.Entity().Table("TableName") method?

mikependon commented 4 years ago

@lambdakris, is the issue above mentioned by @AngelMunoz the same as what you are encountering? If that is the case, then I need to look at it. It seems the RepoDb is failing in reflecting the type in F#. Have to research it.

lambdakris commented 4 years ago

Hey @mikependon and @AngelMunoz, thanks for looking at this. I have a feeling that @AngelMunoz might be on to something because the problem seems to occur before the PropertyHandler methods have a chance to execute. I tried to recreate the example in the docs which simply serializes and deserializes a json string to an object, and I still get the same exception. I haven't tried it in C#, but assuming that it works as expected there, then it really might be an issue with how interfaces are implemented in F#. Below is the example:

the table...

create table dbo.Customer (
    Id int identity(1,1) not null,
    Name nvarchar(100) not null,
    Address nvarchar(500) not null
);

the code...

open System
open System.Data.SqlClient
open RepoDb
open RepoDb.Attributes
open RepoDb.Interfaces
open Newtonsoft.Json

[<CLIMutable>]
type Address = {
    City: string
    State: string }

type AddressPropertyHandler() =
    interface IPropertyHandler<string, Address> with
        member this.Get (input, property) =
            JsonConvert.DeserializeObject<Address>(input)
        member this.Set (input, property) =
            JsonConvert.SerializeObject(input)

[<CLIMutable>]
type Customer = {
    Id: int
    Name: string
    [<PropertyHandler(typeof<AddressPropertyHandler>)>] 
    Address: Address 
}

[<EntryPoint>]
let main argv =
    RepoDb.SqlServerBootstrap.Initialize()

    let customer1 = { Id = 0; Name = "customer 1"; Address = { City = "San Juan"; State = "Puerto Rico" } }

    use connection = new SqlConnection("server=localhost;database=repodb;trusted_connection=true")

    connection.Insert<Customer>(customer1) |> ignore

    0 // exit code
mikependon commented 4 years ago

@lambdakris - now that I have your model and table schema, can you also paste your exception here? Is it identical to what @AngelMunoz has encountered (i.e.: NullException)?

lambdakris commented 4 years ago

Sure, here is the serialized exception...

System.NullReferenceException
  HResult=0x80004003
  Message=Object reference not set to an instance of an object.
  Source=RepoDb
  StackTrace:
   at RepoDb.Reflection.FunctionFactory.<>c__DisplayClass6_0`1.<GetDataEntityDbCommandParameterSetterFunction>b__0(Expression instance, ParameterExpression property, DbField dbField, ClassProperty classProperty, Boolean skipValueAssignment, ParameterDirection direction)
   at RepoDb.Reflection.FunctionFactory.GetDataEntityDbCommandParameterSetterFunction[TEntity](IEnumerable`1 inputFields, IEnumerable`1 outputFields, IDbSetting dbSetting)
   at RepoDb.FunctionCache.GetDataEntityDbCommandParameterSetterFunctionCache`1.Get(String cacheKey, IEnumerable`1 inputFields, IEnumerable`1 outputFields, IDbSetting dbSetting)
   at RepoDb.FunctionCache.GetDataEntityDbCommandParameterSetterFunction[TEntity](String cacheKey, IEnumerable`1 inputFields, IEnumerable`1 outputFields, IDbSetting dbSetting)
   at RepoDb.DbConnectionExtension.<>c__DisplayClass242_0`2.<InsertInternalBase>b__0()
   at RepoDb.Contexts.Execution.InsertExecutionContextCache`1.Get(String tableName, IEnumerable`1 fields, Func`1 callback)
   at RepoDb.DbConnectionExtension.InsertInternalBase[TEntity,TResult](IDbConnection connection, String tableName, TEntity entity, IEnumerable`1 fields, String hints, Nullable`1 commandTimeout, IDbTransaction transaction, ITrace trace, IStatementBuilder statementBuilder, Boolean skipIdentityCheck)
   at RepoDb.DbConnectionExtension.InsertInternal[TEntity,TResult](IDbConnection connection, TEntity entity, String hints, Nullable`1 commandTimeout, IDbTransaction transaction, ITrace trace, IStatementBuilder statementBuilder)
   at RepoDb.DbConnectionExtension.Insert[TEntity](IDbConnection connection, TEntity entity, String hints, Nullable`1 commandTimeout, IDbTransaction transaction, ITrace trace, IStatementBuilder statementBuilder)
   at Program.main(String[] argv) in C:\Users\cax9124\source\repos\ConsoleApp1\ConsoleApp1\Program.fs:line 36

And the content of the {exception}.TargetSite property just in case...

{System.Linq.Expressions.Expression <GetDataEntityDbCommandParameterSetterFunction>b__0(System.Linq.Expressions.Expression, System.Linq.Expressions.ParameterExpression, RepoDb.DbField, RepoDb.ClassProperty, Boolean, System.Data.ParameterDirection)} | System.Reflection.MethodBase {System.Reflection.RuntimeMethodInfo}
mikependon commented 4 years ago

The fix for this has been checked in to the main branch. #499. Will be a part of the next beta release.

Swoorup commented 4 years ago

Does this mean that Option or any types are mappable using FluentMapper?

mikependon commented 4 years ago

@Swoorup - Apology for overlook. The options are not a part of our latest beta released. Please keep this ticket open.

Swoorup commented 4 years ago

@mikependon Any timeline for the release?

Starting the evaluate this library to replace current TypeProvider based library on a F# codebase. It hits a sweet spot before EF and using TP libraries directly writing queries. 👍

Swoorup commented 3 years ago

@mikependon @lambdakris @AngelMunoz Can confirm that I can directly use Option types in POCO model with the latest release.

EDIT: Spoke too soon. Didn't ran the code I thought I did. Doesnt appear to work for
attributes like [<NpgsqlTypeMap(NpgsqlDbType.Jsonb)>]. Best to use

Option.ofObj
Nullable
mikependon commented 3 years ago

Can you provide the error stack trace if you are using that attribute?

xperiandri commented 1 year ago

@mikependon this is the sample you can test

open System
open System.ComponentModel.DataAnnotations.Schema
open Npgsql
open RepoDb
open RepoDb.Interfaces

[<Struct>]
type TenantId = TenantId of Guid

type TenantIdPropertyHandler () =
    interface IPropertyHandler<Guid, TenantId> with
        member _.Get (input, _) = TenantId input
        member _.Set (TenantId input, _) = input

type ValueOptionPropertyHandler<'t> () =
    interface IPropertyHandler<'t, 't voption> with
        member _.Get (input, _) =
            match input |> box with
            | null -> ValueNone
            | _ -> ValueSome input
        member _.Set (input, _) =
            match input with
            | ValueNone -> null |> box :?> 't
            | ValueSome x -> x

type OptionNullablePropertyHandler<'t when 't:> ValueType and 't: struct and 't: (new: unit -> 't )> () =
    interface IPropertyHandler<'t Nullable, 't option> with
        member _.Get (input, _) = input |> Option.ofNullable<'t>
        member _.Set (input, _) = input |> Option.toNullable<'t>

type ValueOptionNullablePropertyHandler<'t when 't:> ValueType and 't: struct and 't: (new: unit -> 't )> () =
    interface IPropertyHandler<'t Nullable, 't voption> with
        member _.Get (input, _) = input |> ValueOption.ofNullable<'t>
        member _.Set (input, _) = input |> ValueOption.toNullable<'t>

type DriverRiskAssessmentSession = {
    [<Column "tenant_id">]
    TenantId: TenantId
    [<Column "started_at">]
    StartedAt: DateTimeOffset voption
}

let [<Literal>] TableName = "test.assessment_sessions"

let assessment1 = {
    TenantId = Guid.NewGuid() |> TenantId
    StartedAt = ValueNone
}
let assessment2 = {
    TenantId = Guid.NewGuid() |> TenantId
    StartedAt = DateTimeOffset.UtcNow |> ValueSome
}

PostgreSqlBootstrap.Initialize();
PropertyHandlerMapper.Add<TenantId, TenantIdPropertyHandler>()
//PropertyHandlerMapper.Add<option<_>, OptionNullablePropertyHandler<_>>()
PropertyHandlerMapper.Add<voption<DateTimeOffset>, ValueOptionNullablePropertyHandler<DateTimeOffset>>()
//let connection = new NpgsqlConnection("Host=localhost;Port=5432;Database=risk;Username=postgres;Enlist=false;Integrated Security=true;")
let connection = new NpgsqlConnection("Host=localhost;Port=12605;Database=risk;Username=postgres;Enlist=false;Integrated Security=true;")

connection.Open()
connection.CreateCommand(
"""CREATE SCHEMA IF NOT EXISTS test;
    CREATE TABLE IF NOT EXISTS test.assessment_sessions (
        --session_id uuid PRIMARY KEY,
        tenant_id uuid NOT NULL,
        started_at TIMESTAMP WITH TIME ZONE NULL
    );
    """)
    .ExecuteNonQuery() |> printfn "Created: %A"

connection.Insert<DriverRiskAssessmentSession>(TableName, assessment1) |> printfn "Inserted: %A"
connection.Insert<DriverRiskAssessmentSession>(TableName, assessment2) |> printfn "Inserted: %A"

connection.QueryAll<DriverRiskAssessmentSession>(TableName) |> Seq.iter (printfn "Found: %A")

Console.ReadKey() |> ignore
xperiandri commented 1 year ago

It would be nice if you can make all 3 property handlers work out of the box:

  1. FSharpOption
  2. FsharpValueOption
  3. Single case discriminated union which at the end is a value object that has a single property Item like
    record SingleCaseDU<T>(T Item);

    @Swoorup correct my if I'm wrong