MarimerLLC / cslaforum

Discussion forum for CSLA .NET
https://cslanet.com
Other
31 stars 6 forks source link

TargetParameterCountException in dataportal child fetch after upgrading to 5.0.1 #843

Open mtavares628 opened 4 years ago

mtavares628 commented 4 years ago

I'm in the process of trying to upgrade from 4.8.1 to 5.0.1 and I've run into an issue with the dataportal when trying to perform a child fetch passing a DataRow array as my criteria. I've basically got a read-only list that contains a child read-only list. I'm using old Ado.NET to call a stored procedure in the list dataportal and using datatables and relations to pull all the data back at once. So the dataportal tree looks something like this:

AppointmentInfocollection:

Protected Overloads Sub DataPortal_Fetch(ByVal crit As CriteriaGet)
            Using ctx = ConnectionManager(Of SqlConnection).GetManager(Database.NDRNConnection, False)
                Using cmd As New SqlCommand("procDesktop_AppointmentByEmployeeIDDateRange_Select", ctx.Connection)
                    cmd.CommandType = CommandType.StoredProcedure
                    cmd.Parameters.AddWithValue("@StartDate", crit.StartDate.DBValue).DbType = DbType.DateTime
                    cmd.Parameters.AddWithValue("@EndDate", crit.EndDate.DBValue).DbType = DbType.DateTime
                    cmd.Parameters.AddWithValue("@EmployeeIDs", crit.EmployeeIDs.NullAsEmpty()).DbType = DbType.String
                    cmd.Parameters.AddWithValue("@AdvocacyGroupID", crit.AdvocacyGroupID).DbType = DbType.Int32
                    cmd.Parameters.AddWithValue("@TimeZone", crit.TimeZone).DbType = DbType.Int16
                    Dim args As New DataPortalHookArgs(cmd, crit)
                    LoadCollection(cmd)
                End Using
            End Using
        End Sub

Private Sub LoadCollection(ByVal cmd As SqlCommand)
            Dim ds As New DataSet()
            Using da As New SqlDataAdapter(cmd)
                da.Fill(ds)
            End Using
            CreateRelations(ds)
            Fetch(ds.Tables(0).Rows)
        End Sub

        Private Sub CreateRelations(ByVal ds As DataSet)
            ds.Tables(0).TableName = "AppointmentInfo"
            ds.Tables(1).TableName = "AppointmentEmployeeInfo"
            ds.Relations.Add("AppointmentInfoAppointmentEmployeeInfo", New DataColumn() {ds.Tables("AppointmentInfo").Columns("AppointmentID")}, New DataColumn() {ds.Tables("AppointmentEmployeeInfo").Columns("AppointmentID")}, False)

        End Sub

        ''' <summary>
        ''' Loads all <see cref="AppointmentInfoCollection"/> collection items from given DataTable.
        ''' </summary>
        Private Sub Fetch(ByVal rows As DataRowCollection)
            IsReadOnly = False
            Dim rlce As Boolean = RaiseListChangedEvents
            RaiseListChangedEvents = False
            For Each row As DataRow In rows
                Dim appt As AppointmentInfo = AppointmentInfo.GetAppointmentInfo(row)
                MyBase.Add(appt)
            Next
            RaiseListChangedEvents = rlce
            IsReadOnly = True
        End Sub

AppointmentInfo:

        Friend Shared Function GetAppointmentInfo(ByVal dr As DataRow) As AppointmentInfo
                        Return DataPortal.FetchChild(Of AppointmentInfo)(dr)
        End Function

         Private Sub Child_Fetch(ByVal dr As DataRow)
            If Not dr.IsNull("AppointmentID") Then
                LoadProperty(AppointmentIDProperty, dr("AppointmentID"))
            End If
              FetchChildren(dr)
        End Sub

        Private Sub FetchChildren(Byval dr As DataRow)
            Dim childRows As DataRow()
            childRows = dr.GetChildRows("AppointmentInfoAppointmentEmployeeInfo")
            LoadProperty(EmployeesProperty, AppointmentEmployeeInfoCollection.GetAppointmentEmployeeInfoCollection(childRows))  
        End Sub

AppointmentEmployeeInfoCollection:

       Friend Shared Function GetAppointmentEmployeeInfoCollection(ByVal dr As DataRow()) As AppointmentEmployeeInfoCollection
            Return DataPortal.FetchChild(Of AppointmentEmployeeInfoCollection)(dr)
        End Function

        Private Sub Child_Fetch(ByVal rows As DataRow())
            IsReadOnly = False
            Dim rlce As Boolean = RaiseListChangedEvents
            RaiseListChangedEvents = False
            For Each row As DataRow In rows
                MyBase.Add(AppointmentEmployeeInfo.GetAppointmentEmployeeInfo(row))
            Next
            RaiseListChangedEvents = rlce
            IsReadOnly = True
        End Sub

Something is going on when it gets to the child fetch of the child list. The issue appears to happen in the ServiceProviderMethodCaller.cs file in the FindDataPortalMethod method. It's looking for a criteria object array, and the code is trying to find my Child_Fetch method using the following code:


      // scan candidate methods for matching criteria parameters
      int criteriaLength = 0;
      if (criteria != null)
        criteriaLength = criteria.GetLength(0);
      var matches = new List<ScoredMethodInfo>();
      if (criteriaLength > 0)
      {
        foreach (var item in candidates)
        {
          int score = 0;
          var methodParams = GetCriteriaParameters(item);
          if (methodParams.Length == criteriaLength)
          {
            var index = 0;
            foreach (var c in criteria)
            {
              if (c == null) 
              {
                if (methodParams[index].ParameterType.IsPrimitive)
                  break;
                else if (methodParams[index].ParameterType == typeof(object))
                  score++;
              }
              else
              {
                if (c.GetType() == methodParams[index].ParameterType)
                  score += 2;
                else if (methodParams[index].ParameterType.IsAssignableFrom(c.GetType()))
                  score++;
                else
                  break;
              }
              index++;
            }
            if (index == criteriaLength)
              matches.Add(new ScoredMethodInfo { MethodInfo = item, Score = score });
          }
        }
      }
      else
      {
        foreach (var item in candidates)
        {
          if (GetCriteriaParameters(item).Length == 0)
            matches.Add(new ScoredMethodInfo { MethodInfo = item });
        }
      }
      if (matches.Count == 0)
      {
        // look for params array
        foreach (var item in candidates)
        {
          var lastParam = item.GetParameters().LastOrDefault();
          if (lastParam != null && lastParam.ParameterType.Equals(typeof(object[])) && 
            lastParam.GetCustomAttributes<ParamArrayAttribute>().Any())
          {
            matches.Add(new ScoredMethodInfo { MethodInfo = item, Score = 1 });
          }
        }
      }
      if (matches.Count == 0)
      {
        if (throwOnError)
          throw new TargetParameterCountException($"{targetType.FullName}.[{typeOfT.Name.Replace("Attribute", "")}]{GetCriteriaTypeNames(criteria)}");
        else
          return null;
      }

The above code is looking at the criteria array, and cycling through and trying to match up the parameter list with the parameter types of the method that it found in my code. However, instead of the criteria object being an array of DataRow array objects, it appears to be the DataRow array itself. Meanwhile the MethodParams object has the DataRow array object, so one of two things happens when I step through the code depending upon the underlying data.

  1. If there is only one record returned then the DataRow array in the criteria object has the same length as the MethodParams object, but when cycling through the criteria object it is trying to check the DataRow in the Datarow array against the MethodParams DataRow Array itself, and it fails.

  2. If there is more than one record returned then the length of the criteria DataRow Array ends up being greater than the length of the MethodParams object and it also fails.

To me it looks like the criteria is coming in as a DataRow array, when the code assumes that it should be an object array of DataRow arrays.

Is this a bug, or am I doing something wrong? This was never an issue before I tried upgrading, so I'm guessing it's an issue with the new methodology of not requiring hard coded dataportal names. And I know that my code is using older methodologies, but based on the change log I think it should still work in the new version. Any assistance will be greatly appreciated.

rockfordlhotka commented 4 years ago

One of the drawbacks of using a param array as the last parameter for a method is that it becomes difficult if not impossible to pass an actual array as a parameter.

I'm certainly open to the idea that there's a solution to this, but I'm not confident that an answer exists (though a workaround does - see below).

There are unit tests for FindDataPortalMethod that test numerous combinations of parameter types, arrays, etc. If you'd like, you can try and establish a pattern that accepts your array scenario without breaking the other scenarios. If there is a solution I'm very open to implementing it.

As a workaround, you can pass your array within a "message object" - very possibly a List<DataRow> which is easily created from an array, and easily converted back into an array.

  var list = new List<DataArray>(myarray);
  var array = list.ToArray();
kcabral817 commented 4 years ago

Hi,

I too have just upgraded to the latest release and find that all my Child_Fetch methods fail. I use these to create a new instance of a phone number and address object. I'm not sure what to do, can someone provide some insight?

         protected void Child_Fetch()
         {
            var result = DataPortal.Create<BankAddressInfoPersist>();
            string[] args = { "OriginalValues" };
            using (BypassPropertyChecks)
            {
                DataMapper.Map(result, this, args);
                MarkAsChild();
                MarkNew();
            }
        }

Then I get this:

Csla.DataPortalException
  HResult=0x80131500
  Message=ChildDataPortal.Fetch failed on the server
  Source=Csla
  StackTrace:
   at Csla.Reflection.ServiceProviderMethodCaller.FindDataPortalMethod[T](Type targetType, Object[] criteria, Boolean throwOnError)
   at Csla.Reflection.ServiceProviderMethodCaller.FindDataPortalMethod[T](Object target, Object[] criteria)
   at Csla.Reflection.LateBoundObject.<CallMethodTryAsyncDI>d__12`1.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Csla.Server.DataPortalTarget.<FetchChildAsync>d__18.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Csla.Server.ChildDataPortal.<Fetch>d__9.MoveNext()
   at Csla.Server.ChildDataPortal.Fetch(Type objectType)
   at Csla.DataPortal.FetchChild[T]()
   at AmicusBusinessConsole.Program.Main(String[] args) in C:\Users\kcabr\Source\Repos\AmicusBusiness\AmicusBusinessConsole\Program.cs:line 133

Inner Exception 1:
TargetParameterCountException: SI.Amicus.Business.ExternalAgentLibrary.BO.BankAddressInfoPersist.[FetchChild]()

Inner Exception 2:
TargetParameterCountException: SI.Amicus.Business.CommonLibrary.BO.InjectableBusinessBase`1[[SI.Amicus.Business.ExternalAgentLibrary.BO.BankAddressInfoPersist, SI.Amicus.Business.ExternalAgentLibrary, Version=1.0.7456.42805, Culture=neutral, PublicKeyToken=null]].[FetchChild]()

Inner Exception 3:
TargetParameterCountException: Csla.BusinessBase`1[[SI.Amicus.Business.ExternalAgentLibrary.BO.BankAddressInfoPersist, SI.Amicus.Business.ExternalAgentLibrary, Version=1.0.7456.42805, Culture=neutral, PublicKeyToken=null]].[FetchChild]()

Inner Exception 4:
TargetParameterCountException: Csla.Core.BusinessBase.[FetchChild]()

Inner Exception 5:
TargetParameterCountException: Csla.Core.UndoableBase.[FetchChild]()

Inner Exception 6:
TargetParameterCountException: Csla.Core.BindableBase.[FetchChild]()

Inner Exception 7:
TargetParameterCountException: Csla.Core.MobileObject.[FetchChild]()

Inner Exception 8:
TargetParameterCountException: System.Object.[FetchChild]()
brinawebb commented 4 years ago

Have you tried flagging your method with the [FetchChild] attribute? It should work without it, so this may be a workaround to a bug.

[FetchChild] protected void Child_Fetch()

kcabral817 commented 4 years ago

Hi Brinawebb, gave it try, still nothing but the error.

rockfordlhotka commented 4 years ago

First, I want to point out that this forum is now obsolete - please move to the new discussion forum at https://github.com/marimerllc/csla/discussions

Second, it looks like CSLA is trying to find a FetchChild method on the type BankAddressInfoPersist. Is that the type where you've implemented the method?