RepoDB
RepoDB copied to clipboard
Question: Question on how to properly configure type mapping for FSharp Option types
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.
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?
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.
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
- https://github.com/AngelMunoz/Migrondi/blob/master/src/Migrondi/Queries.fs#L80
- https://github.com/AngelMunoz/Migrondi/blob/master/src/Migrondi/Queries.fs#L95
- https://github.com/AngelMunoz/Migrondi/blob/master/src/Migrondi/Queries.fs#L101
But I'd just use the Execute'Query family
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.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
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<T>().Table("TableName") method?
@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.
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
@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)?
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}
The fix for this has been checked in to the main branch. #499. Will be a part of the next beta release.
Does this mean that Option or any types are mappable using FluentMapper?
@Swoorup - Apology for overlook. The options are not a part of our latest beta released. Please keep this ticket open.
@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. 👍
@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
Can you provide the error stack trace if you are using that attribute?
@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
It would be nice if you can make all 3 property handlers work out of the box:
- FSharpOption<T>
- FsharpValueOption<T>
- 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