More diagnostics, I have INPC wired in my domain model, which communicates 
to our WPF view model, view, bindings, and so forth. Some debugging there. 
Can see the Id being set properly. Changed here, changing snipped for 
brevity, but you can see the progression, things appear to be properly set.

Although it is curious the IQueryable failure lands during the 
InternalPositions (ObservableCollection<Location>) INPC EH. No discernable 
way why that should be the case. And without explanation why the TimeSpan 
Duration user type is failing.

Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: 
WhalleyBotEnhanced.Tracks.TrackMeetLane; property: Id
NullSafeGet: fields: 18, names: [lowvalueduetime7_10_0_], owner class: 
'WhalleyBotEnhanced.Tracks.TrackMeetLane'.
NullSafeGet: fields: 18, names: [lowvalueperiod8_10_0_], owner class: 
'WhalleyBotEnhanced.Tracks.TrackMeetLane'.
NullSafeGet: fields: 18, names: [lowvalueduration9_10_0_], owner class: 
'WhalleyBotEnhanced.Tracks.TrackMeetLane'.
NullSafeGet: fields: 18, names: [highvalueduetime11_10_0_], owner class: 
'WhalleyBotEnhanced.Tracks.TrackMeetLane'.
NullSafeGet: fields: 18, names: [highvalueperiod12_10_0_], owner class: 
'WhalleyBotEnhanced.Tracks.TrackMeetLane'.
NullSafeGet: fields: 18, names: [highvalueduration13_10_0_], owner class: 
'WhalleyBotEnhanced.Tracks.TrackMeetLane'.
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: 
WhalleyBotEnhanced.Tracks.TrackMeetLane; property: Enabled
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: 
WhalleyBotEnhanced.Tracks.TrackMeetLane; property: IsReady
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: 
WhalleyBotEnhanced.Tracks.TrackMeetLane; property: TrackMeetEvent
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: 
WhalleyBotEnhanced.Tracks.TrackMeetLane; property: Name
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: 
WhalleyBotEnhanced.Tracks.TrackMeetLane; property: Color
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: 
WhalleyBotEnhanced.Tracks.TrackMeetLane; property: LowValue
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: 
WhalleyBotEnhanced.Tracks.TrackMeetLane; property: HighValue
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: 
WhalleyBotEnhanced.Tracks.TrackMeetLane; property: PackCategories
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: 
WhalleyBotEnhanced.Tracks.TrackMeetLane; property: InternalPackCategories
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: 
WhalleyBotEnhanced.Tracks.TrackMeetLane; property: PackIds
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: 
WhalleyBotEnhanced.Tracks.TrackMeetLane; property: InternalPackIds
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: 
WhalleyBotEnhanced.Tracks.TrackMeetLane; property: Positions
Property changed: WhalleyBotEnhanced.Tracks.TrackMeetLane; root: 
WhalleyBotEnhanced.Tracks.TrackMeetLane; property: InternalPositions
Exception thrown: 'NHibernate.Exceptions.GenericADOException' in 
NHibernate.dll
An exception of type 'NHibernate.Exceptions.GenericADOException' occurred 
in NHibernate.dll but was not handled in user code
could not initialize a collection: 
[WhalleyBotEnhanced.Tracks.TrackMeetEvent.Lanes#c0832da0-d2f9-412c-8364-059f7ed205b5][SQL:
 
SELECT lanes0_.TrackMeetEventId as trackmeeteventid3_10_1_, lanes0_.Id as 
id1_10_1_, lanes0_.Id as id1_10_0_, lanes0_.Enabled as enabled2_10_0_, 
lanes0_.TrackMeetEventId as trackmeeteventid3_10_0_, lanes0_.Name as 
name4_10_0_, lanes0_.Color as color5_10_0_, lanes0_.lowValuePlayerCount as 
lowvalueplayercount6_10_0_, lanes0_.lowValueDueTime as 
lowvalueduetime7_10_0_, lanes0_.lowValuePeriod as lowvalueperiod8_10_0_, 
lanes0_.lowValueDuration as lowvalueduration9_10_0_, 
lanes0_.highValuePlayerCount as highvalueplayercount10_10_0_, 
lanes0_.highValueDueTime as highvalueduetime11_10_0_, 
lanes0_.highValuePeriod as highvalueperiod12_10_0_, 
lanes0_.highValueDuration as highvalueduration13_10_0_, 
lanes0_.packCategoriesJson as packcategoriesjson14_10_0_, 
lanes0_.packIdsJson as packidsjson15_10_0_, lanes0_.positionsJson as 
positionsjson16_10_0_ FROM public.efcore_wbe_trackmeetlane lanes0_ WHERE 
lanes0_.TrackMeetEventId=?]

The core of the user type for us is TryAssemble and TryDisassemble, 
basically from (database) and to (database) directions. Our approach thus 
far is to handle the edge cases, null, intercepting the TPrimitive (P) and 
TUser (U), and failing those, throw an exception. Then in the derived 
class, override the two Try methods only focused on the specific types we 
want to convert ONLY. Which for the most part seems to be working well. 
Wondering however if there are some issues with Fluent and NH components 
not using this for whatever reason, which would be muy no bueno.

protected override bool TryAssemble(object cached, object owner, out object 
assembled)
{
    assembled = null;

    switch (cached)
    {
        case not null when cached is Duration duration:
            assembled = duration;
            break;

        case not null when cached is TimeSpan timeSpan:
            assembled = Duration.FromTicks(timeSpan.Ticks);
            break;
    }

    return assembled is not null;
}

protected override bool TryDisassemble(object value, out object 
disassembled)
{
    disassembled = null;

    switch (value)
    {
        case not null when value is Duration duration:
            // TODO: for now assuming TimeSpan is the intermediate record 
set type
            disassembled = duration.ToTimeSpan();
            break;

        case not null when value is TimeSpan timeSpan:
            disassembled = timeSpan;
            break;

        case not null:
            // TODO: just in the event we may need a DateTimeOffset use case
            throw new InvalidOperationException($"Unable to disassemble 
value type '{value.GetType()}'.");
    }

    return disassembled is not null;
}

And from the base class perspective. Snipped for brevity... effectively you 
can say the Try methods are both core to the Assemble and Disassemble 
methods, which are in turn central to the user type. And as long as we have 
primitive P and returned R types correctly aligned, it works out pretty 
well. P being with respect to the ADO recordset, and R being the returned 
or sometimes user (U) type. In this specific circumstance, TimeSpan to 
NodaTime.Duration.

public virtual object NullSafeGet(DbDataReader rs, string[] names, 
ISessionImplementor session, object owner)
{
    // after lifting the index ordinal named field...
    return value switch
    {
        DBNull or null => null,
        R r => Assemble(Disassemble(r), owner),
        P p => Assemble(p, owner),
        _ => throw new InvalidOperationException($"Unable to null safe get 
value type '{value.GetType()}', names [{string.Join(", ", names)}].")
    };
}

public virtual void NullSafeSet(DbCommand cmd, object value, int index, 
ISessionImplementor session)
{
    // after finding cmd, arg (parameter) is the correct pattern matched 
type and so on...
    arg.Value = value switch
    {
        DBNull or null => DBNull.Value,
        P p => p,
        R r => Disassemble(r),
        _ => throw new InvalidOperationException($"Unable to null safe set 
value type '{value.GetType()}', index {index}.")
    };
    // ...
}

I put breakpoints in the Try methods, and I do not see anything that jumps 
out as being unusual or errant there. But for whatever reason obviously a 
convertion is being missed.

Our component part handler is this:

protected static void 
OnMapSchedulablePlayerComponentData(ComponentPart<SchedulablePlayerComponentData>
 
componentPart)
{
    componentPart.Map(x => x.PlayerCount).Not.Nullable();
    componentPart.Map(x => 
x.DueTime).CustomType<NpgsqlNodaTimeDurationUserType>().Not.Nullable();
    componentPart.Map(x => 
x.Period).CustomType<NpgsqlNodaTimeDurationUserType>().Not.Nullable();
    componentPart.Map(x => 
x.Duration).CustomType<NpgsqlNodaTimeDurationUserType>().Not.Nullable();
}

And in the lane map. CamelCaseColumnPrefix is something I added to camel 
case the prefix. Technically perhaps a nice to have.

Component(x => x.LowValue, 
OnMapSchedulablePlayerComponentData).CamelCaseColumnPrefix(nameof(TrackMeetLane.LowValue));
Component(x => x.HighValue, 
OnMapSchedulablePlayerComponentData).CamelCaseColumnPrefix(nameof(TrackMeetLane.HighValue));

As far as the map itself and the queries, verified the queries are good and 
returning the data they should, including in the specific ID use case.

Totaly mystery at this point why the custom types are not being handled 
correctly. And it would be a massive issue if we had to somehow break open 
the component, that would be bad.

Failing fluent NH components,  we could perhaps rethink whether component 
is the right answer, and do an alternate JSON based approach for the 
column(s) itself. We are doing that in other places and it seems  to be 
working correctly.

Open to other suggestions.

Best and thank you!

Michael W. Powell
On Friday, June 27, 2025 at 12:27:11 PM UTC-4 Michael W Powell wrote:

> With a strategically placed Debug write, the component low and high values 
> are properly being set, but the failure is happening after the owner 
> component instance has been set, as far as I can determine.
>
> NullSafeGet: fields: 18, names: [lowvalueduetime7_10_0_], owner class: 
> 'WhalleyBotEnhanced.Tracks.TrackMeetLane'.
> NullSafeGet: fields: 18, names: [lowvalueperiod8_10_0_], owner class: 
> 'WhalleyBotEnhanced.Tracks.TrackMeetLane'.
> NullSafeGet: fields: 18, names: [lowvalueduration9_10_0_], owner class: 
> 'WhalleyBotEnhanced.Tracks.TrackMeetLane'.
> NullSafeGet: fields: 18, names: [highvalueduetime11_10_0_], owner class: 
> 'WhalleyBotEnhanced.Tracks.TrackMeetLane'.
> NullSafeGet: fields: 18, names: [highvalueperiod12_10_0_], owner class: 
> 'WhalleyBotEnhanced.Tracks.TrackMeetLane'.
> NullSafeGet: fields: 18, names: [highvalueduration13_10_0_], owner class: 
> 'WhalleyBotEnhanced.Tracks.TrackMeetLane'.
>
> Which there is some property setter logic going on there, but I wonder if 
> that should not be a simple receiver of the NH negotiated component 
> instance. One example:
>
> public virtual SchedulablePlayerComponentData HighValue
> {
>     get => _highValue;
>     set
>     {
>         var highValue = _highValue;
>
>         if (SetProperty(ref highValue, value ?? new()) && 
> !ReferenceEquals(highValue, _highValue))
>         {
>             _highValue.AcceptValue(highValue);
>         }
>     }
> }
>
> So instead we just do: set => SetProperty(ref _highValue, value), and if 
> we need to connect any business logic after that, we do that, but let NH 
> handle the component property itself.
>
> Not dissimilar in approach from IList<T> collections, adding model or view 
> model observability, ObservableCollection<T> around those source 
> collections, for proper parent-child referencing.
>
> Thoughts?
> On Friday, June 27, 2025 at 12:04:25 PM UTC-4 Michael W Powell wrote:
>
>> Hello,
>>
>> I have the dialect arranging a map between TimeSpan and 
>> NodaTime.Duration. I step through several such user type conversions from 
>> and to, assembled and disassembled, so I know that at least it should be 
>> able to handle the use case.
>>
>> However, still receiving an exception, not sure quite as to why, maybe 
>> something TimeSpan? Nullable is appearing in the ADO recordset somehow, I'm 
>> not sure yet.
>>
>> And sorry for the mess here, the excptions, messages, and the query... 
>> The data is all there as expected, and have traced up the callstack, up to 
>> and including the repository pattern IQueryable query provider, everything 
>> else being equal, the values SHOULD be landing in the mapped property(ies) 
>> correctly.
>>
>> Trouble is in an exception like this, I do not know per se "which" 
>> properties are failing. I'll look at possibly doing some Debug writes at 
>> strategic IUserType moments when those details, alias, etc, are better 
>> known. Although I'm not sure exactly that will tell me property names, more 
>> like the query alias names, which is also not especially helpful.
>>
>> BTW, if it matters, the columns in question, are properly mapped in the 
>> context of a fluent Component. This is intentionally properly the case. 
>> I'll also have to review the component code, because I'm not hundred 
>> percent certain those properties are not internally or privately set, with 
>> their values either calculated or ctor provided.
>>
>> Thanks!
>>
>> NHibernate.Exceptions.GenericADOException: 'could not initialize a 
>> collection: 
>> [WhalleyBotEnhanced.Tracks.TrackMeetEvent.Lanes#c0832da0-d2f9-412c-8364-059f7ed205b5][SQL:
>>  
>> SELECT lanes0_.TrackMeetEventId as trackmeeteventid3_10_1_, lanes0_.Id as 
>> id1_10_1_, lanes0_.Id as id1_10_0_, lanes0_.Enabled as enabled2_10_0_, 
>> lanes0_.TrackMeetEventId as trackmeeteventid3_10_0_, lanes0_.Name as 
>> name4_10_0_, lanes0_.Color as color5_10_0_, lanes0_.lowValuePlayerCount as 
>> lowvalueplayercount6_10_0_, lanes0_.lowValueDueTime as 
>> lowvalueduetime7_10_0_, lanes0_.lowValuePeriod as lowvalueperiod8_10_0_, 
>> lanes0_.lowValueDuration as lowvalueduration9_10_0_, 
>> lanes0_.highValuePlayerCount as highvalueplayercount10_10_0_, 
>> lanes0_.highValueDueTime as highvalueduetime11_10_0_, 
>> lanes0_.highValuePeriod as highvalueperiod12_10_0_, 
>> lanes0_.highValueDuration as highvalueduration13_10_0_, 
>> lanes0_.packCategoriesJson as packcategoriesjson14_10_0_, 
>> lanes0_.packIdsJson as packidsjson15_10_0_, lanes0_.positionsJson as 
>> positionsjson16_10_0_ FROM public.efcore_wbe_trackmeetlane lanes0_ WHERE 
>> lanes0_.TrackMeetEventId=?]'
>>
>> - $exception {"could not initialize a collection: 
>> [WhalleyBotEnhanced.Tracks.TrackMeetEvent.Lanes#c0832da0-d2f9-412c-8364-059f7ed205b5][SQL:
>>  
>> SELECT lanes0_.TrackMeetEventId as trackmeeteventid3_10_1_, lanes0_.Id as 
>> id1_10_1_, lanes0_.Id as id1_10_0_, lanes0_.Enabled as enabled2_10_0_, 
>> lanes0_.TrackMeetEventId as trackmeeteventid3_10_0_, lanes0_.Name as 
>> name4_10_0_, lanes0_.Color as color5_10_0_, lanes0_.lowValuePlayerCount as 
>> lowvalueplayercount6_10_0_, lanes0_.lowValueDueTime as 
>> lowvalueduetime7_10_0_, lanes0_.lowValuePeriod as lowvalueperiod8_10_0_, 
>> lanes0_.lowValueDuration as lowvalueduration9_10_0_, 
>> lanes0_.highValuePlayerCount as highvalueplayercount10_10_0_, 
>> lanes0_.highValueDueTime as highvalueduetime11_10_0_, 
>> lanes0_.highValuePeriod as highvalueperiod12_10_0_, 
>> lanes0_.highValueDuration as highvalueduration13_10_0_, 
>> lanes0_.packCategoriesJson as packcategoriesjson14_10_0_, 
>> lanes0_.packIdsJson as packidsjson15_10_0_, lanes0_.positionsJson as 
>> positionsjson16_10_0_ FROM public.efcore_wbe_trackmeetlane lanes0_ WHERE 
>> lane..."} NHibernate.Exceptions.GenericADOException
>>
>> + InnerException {"Unable to cast object of type 'System.TimeSpan' to 
>> type 'NodaTime.Duration'."} System.Exception 
>> {System.InvalidCastException}
>>
>>

-- 
You received this message because you are subscribed to the Google Groups 
"Fluent NHibernate" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/fluent-nhibernate/95eec468-52a7-4034-82de-9eb465f0fd11n%40googlegroups.com.

Reply via email to