MarimerLLC / csla

A home for your business logic in any .NET application.
https://cslanet.com
MIT License
1.23k stars 394 forks source link

Using await in CreateChild causes "Cannot wait on monitors on this runtime" #4007

Closed clodewyks closed 1 month ago

clodewyks commented 1 month ago

Describe the bug Our application makes use of a lot of Lookup objects, simply key value pairs that are read from the database and maintained on the server and client for selection components. We would like each object to be in charge of loading its own lookups, and caching them in a generic store once done. The below code works fine in a unit test, and is fine if child objects are not setup, but as soon as the Blazor WASM client attempts to setup a child object which loads it's own lookups asynchronously we are met with "Cannot wait on monitors on this runtime".

Version and Platform CSLA version: 6.2.2 OS: Windows Platform: Blazor WASM

Code that Fails The below code fails when attempting to add an item to the NumberContactList

image

Although CreateClient makes an async call of its own:

image

When the CreateChild method is hit for NumberContactList and tries to fetch its lookups:

image

We get the error described. I found this issue which describes improving the message, which makes me believe the exception I am receiving is not expected.

Apologies for screenshots, Github refused to format code when opening the issue.

Stack Trace or Exception Detail

Message: 'DataPortal.Create failed (Cannot wait on monitors on this runtime.)' Trace: at System.Threading.Monitor.ObjWait(Int32 millisecondsTimeout, Object obj) at System.Threading.Monitor.Wait(Object obj, Int32 millisecondsTimeout) at System.Threading.ManualResetEventSlim.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken) at System.Threading.Tasks.Task.SpinThenBlockingWait(Int32 millisecondsTimeout, CancellationToken cancellationToken) at System.Threading.Tasks.Task.InternalWaitCore(Int32 millisecondsTimeout, CancellationToken cancellationToken) at System.Threading.Tasks.Task.InternalWait(Int32 millisecondsTimeout, CancellationToken cancellationToken) at System.Threading.Tasks.Task1[[System.Object, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].GetResultCore(Boolean waitCompletionNotification) at System.Threading.Tasks.Task1[[System.Object, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].get_Result() at Csla.Server.ChildDataPortal.Create(Type objectType) at Csla.DataPortal1[[Kronos.Business.Client.ContactDetails.NumberContactDetail, Kronos.Business.Client, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].CreateChild() at Csla.BusinessBindingListBase2[[Kronos.Business.Client.ContactDetails.NumberContactDetailList, Kronos.Business.Client, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[Kronos.Business.Client.ContactDetails.NumberContactDetail, Kronos.Business.Client, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].AddNewCore() at System.ComponentModel.BindingList1[[Kronos.Business.Client.ContactDetails.NumberContactDetail, Kronos.Business.Client, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].System.ComponentModel.IBindingList.AddNew() at System.ComponentModel.BindingList1[[Kronos.Business.Client.ContactDetails.NumberContactDetail, Kronos.Business.Client, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].AddNew() at Kronos.Business.Client.Client.CreateClient(ClientInitializer initializer, ClientFactory factory) in E:\Repos\Kronos Framework\Kronos.Business.Client\Client.cs:line 879 at Csla.Reflection.ServiceProviderMethodCaller.CallMethodTryAsync(Object obj, ServiceProviderMethodInfo method, Object[] parameters) at Csla.Reflection.ServiceProviderMethodCaller.CallMethodTryAsync(Object obj, ServiceProviderMethodInfo method, Object[] parameters) at Csla.Reflection.LateBoundObject.d22`1[[Csla.CreateAttribute, Csla, Version=6.2.2.0, Culture=neutral, PublicKeyToken=93be5fdc093e4c30]].MoveNext() at Csla.Server.DataPortalTarget.d141[[Csla.CreateAttribute, Csla, Version=6.2.2.0, Culture=neutral, PublicKeyToken=93be5fdc093e4c30]].MoveNext() at Csla.Server.DataPortalTarget.CreateAsync(Object criteria, Boolean isSync) at Csla.Server.SimpleDataPortal.Create(Type objectType, Object criteria, DataPortalContext context, Boolean isSync) at Csla.DataPortal1[[Kronos.Business.Client.Client, Kronos.Business.Client, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].HandleDataPortalException(String operation, DataPortalException ex, Boolean isSync, IDataPortalProxy proxy) at Csla.DataPortal1[[Kronos.Business.Client.Client, Kronos.Business.Client, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].HandleCreateDataPortalException(DataPortalException ex, Boolean isSync, IDataPortalProxy proxy) at Csla.DataPortal1.d14[[Kronos.Business.Client.Client, Kronos.Business.Client, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].MoveNext() at Csla.DataPortal`1.d18[[Kronos.Business.Client.Client, Kronos.Business.Client, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].MoveNext() at Kronos.Business.Client.Factories.ClientFactory.NewClient(ClientInitializer initializer) in E:\Repos\Kronos Framework\Kronos.Business.Client\Factories\ClientFactory.cs:line 79 at Kronos.EOSOnline.Components.ClientCaptureCtl.MainMemberNext() in E:\Repos\Kronos Framework\Kronos.EOSOnline\Components\ClientCaptureCtl.razor:line 214

clodewyks commented 1 month ago

This appears to happen in any [ CreateChild ], regardless of whether it is being called from within another [ Create ] or directly from the client.

clodewyks commented 1 month ago

In its simplest form:

image

Are we incorrect in expected that we should be able to call a Command from within a CreateChild?

StefanOssendorf commented 1 month ago

Hi. Can you share the code of the Lookups.Load*() methods?

Regarding the command. I've only used csla 7+ but I think that should be possible.

clodewyks commented 1 month ago

Hi @StefanOssendorf,

I reduced the Lookups.Load*() to it's simplest form and the section of code that is causing the error.

Removing all of the caching etc, essentially Lookups just calls a CommandBase which returns a Dictionary.

I get the error on the CreateAsync within a CreateChild

        IDataPortal<TextListCommand> textListCommandPortal = ApplicationContext.GetRequiredService<IDataPortalFactory>().GetPortal<TextListCommand>();
        var command = await textListCommandPortal.CreateAsync(TextListCriteria.DependantTypesCriteria());
        var lookup = await textListCommandPortal.ExecuteAsync(command);
StefanOssendorf commented 1 month ago

Ah now I see - I think. In your first screenshot. You can't use AddNew() from the list. It's internally sync over async. Please try instead an explicit create child call and .Add() on the list with the new item.

clodewyks commented 1 month ago

Thanks @StefanOssendorf , is the below what you meant?

var number = await numberPortal.CreateChildAsync();

number.Number = initializer.CellNumber;

number.NumberType = "Cell";

NumberContactList.Add(number);
StefanOssendorf commented 1 month ago

Yes. Did that fix the exception?

rockfordlhotka commented 1 month ago

@clodewyks you actually want the create to occur on the app server or the client?

Your latest code will do the create on the client, and allow the child's create operation method to invoke root data portal operations that do call the server.

fwiw, this should all improve with changes coming in CSLA 9, where there'll be async add methods on collections.