4
votes

In a new application we are creating now, we have a lot of SOAP requests (easily over 50 different ones for now). In order to abstract the creation of the requests as much as possible, we added an abstract class named TRequestMessageParser to delegate construction of the soap requests.

This abstract class receives a parameter list and has a SetParameterValues method to fill the corresponding SOAP request by using the new RTTI. It creates and fills object parameters, array parameters and other complex structures of a given request. We then create derived classes tied to a specific request type generated by the WSDL importer. These derived classes only do two things:

  1. instantiate the request.
  2. call SetParameterValues.

Now, this works fine (or seems to). The request is created and if you debug it you can see all properties specified in the parameters are set, regardless of if they are ordinal types, instances or dynamic arrays.

The problem comes when the request is parsed to XML text. When that happens, the dynamic array properties are never set. We could confirm this by using the OnBeforeExecute event handler of the THTTPRIO we assigned to the service wrapper. No error or exception is thrown. The dynamic array properties are simply ignored.

If we create the request manually, i.e., creating and setting specifically each object, array and property, then the request (which seems the same as the RTTI one) gets correctly parsed to XML text.

So clearly we must be doing something wrong when we create the request by using RTTI, although despite debugging and googling for conversion errors we could not find what it is.

Following you will find the relevant code for the TRequestMessageParser class:

TRequestMessageParser<REQ: TRemotable> = class
protected
  FRequest : REQ;
<snip rest of declaration>

procedure TRequestMessageParser<REQ>.SetParameterValues(Parameters: TObjectList<TRequestParameter>);
begin
  SetParameterValues(FRequest, Parameters);
end;

procedure TRequestMessageParser<REQ>.SetParameterValues(parentObject: TObject; ParameterList : TObjectList<TRequestParameter>);
var
  parameter : TRequestParameter;
  requestPropertyRttiType : TRttiType;
  requestProperty : TRttiProperty;
  booleanValue : boolean;
begin
  //context is initialized in constructor
  for parameter in ParameterList do
  begin

    if parameter.IsComplexType then //true if it has > 1 subparameter (object or array)
    begin
      requestProperty := context.GetType(parentObject.ClassType).GetProperty(parameter.Code);
      requestPropertyRttiType := requestProperty.PropertyType;

      case requestPropertyRttiType.TypeKind of
        tkClass: ManageObjectProperty(parentObject, requestPropertyRttiType, parameter);
        tkDynArray: ManageDynamicArrayProperty(parentObject, parameter);
      else
        raise Exception.Create('Unsupported type for requests.');
      end;    
    end
    else
    //ordinal types
    begin
      requestProperty := context.GetType(parentObject.ClassType).GetProperty(parameter.Code);

      if requestProperty.PropertyType.TypeKind = tkEnumeration then
      begin
        if (requestProperty.PropertyType as TRttiEnumerationType).UnderlyingType.Handle = System.TypeInfo(Boolean) then
        begin
          booleanValue := parameter.Value;
          requestProperty.SetValue(parentObject, TValue.From(booleanValue));
        end
        //TODO: probably not necessary as SOAP request have no enumerations so far
        else
          requestProperty.SetValue(parentObject, TValue.FromVariant(parameter.Value));
      end
      else
        requestProperty.SetValue(parentObject, TValue.FromVariant(parameter.Value));
    end;
  end;
end;


procedure TRequestMessageParser<REQ>.ManageObjectProperty(parentObject: TObject; requestPropertyRttiType : TRttiType; parameter : TRequestParameter);
var
  requestPropertyInstance : TObject;
  requestProperty : TRttiProperty;
begin
  requestPropertyInstance := requestPropertyRttiType.AsInstance.MetaclassType.Create;

  //we add the instance to the parent object
  requestProperty := context.GetType(parentObject.ClassType).GetProperty(parameter.Code);
  requestProperty.SetValue(parentObject, requestPropertyInstance);

  //we assign the parameters corresponding to the instance
  SetParameterValues(requestPropertyInstance, parameter.Subparameters);
end;


procedure TRequestMessageParser<REQ>.ManageDynamicArrayProperty(parentObject: TObject; parameter : TRequestParameter);
var
  parentType : trttiType;
  objectProperty : TRttiProperty;
  DynArrayType: TRttiDynamicArrayType;
  DynArrElementType: TRttiType;
  newArrayValue : TValue;
  parentObjectArrayValue : TValue;
  ArrayLength : LongInt;
  i : integer;
begin
  //we retrive rtti information for the property
  parentType := context.GetType(parentObject.ClassInfo);
  objectProperty := parentType.GetProperty(parameter.Code);
  DynArrayType := (objectProperty.PropertyType as TRttiDynamicArrayType);

  //we retrieve a reference to the property as TValue
  newArrayValue := objectProperty.GetValue(parentObject);

  //we get and set the dynamic array length
  arrayLength := parameter.Subparameters.Count;
  DynArraySetLength(PPointer(newArrayValue.GetReferenceToRawData)^, newArrayValue.TypeInfo, 1, @arrayLength);

  //we retrieve the array element type
  DynArrElementType := DynArrayType.ElementType;

  //if it is an object we create the corresponding instances
  if DynArrElementType.IsInstance then
  begin
    for i := 0 to ArrayLength - 1 do
      AddObjectElementToDynamicArray(newArrayValue, i, DynArrElementType, parameter.Subparameters[i]);
  end
  //if it is an ordinal element we assign the value
  else if DynArrElementType.IsOrdinal then
  begin
    for i := 0 to ArrayLength - 1 do
      newArrayValue.SetArrayElement(i, TValue.FromVariant(parameter.Subparameters[i].Value));
  end
  else
    raise Exception.Create('Unsupported');


  //until now we have a copy of the dynamic array so we reassign it to the property
  TValue.MakeWithoutCopy(newArrayValue.GetReferenceToRawData, DynArrayType.Handle, parentObjectArrayValue);
  objectProperty.SetValue(parentObject, parentObjectArrayValue);
end;


procedure TRequestMessageParser<REQ>.AddObjectElementToDynamicArray(DynamicArray : TValue; position: integer; DynamicArrayElementType: TRttiType; objectElementParameter: TRequestParameter);
var
  ElementValue : TValue;
  objectSubparameter : TRequestParameter;
begin
  ElementValue := DynamicArrayElementType.GetMethod('Create').Invoke(DynamicArrayElementType.AsInstance.MetaclassType, []);

  SetParameterValues(ElementValue.AsObject, objectElementParameter.Subparameters);
  DynamicArray.SetArrayElement(position, ElementValue);
end;

The TRequestParameter is a simple class that holds a Code, a Value and a list of subparameters ( a generic TObjectList), all accessible through read properties. Id you need to see it I can add its code as well.

We are using Delphi XE5 to create the app. If anyone can give us at least a lead on what we are doing wrong it would be great!

1
With all of this indirection, are you sure your dynamic arrays are not just falling out of scope and getting cleaned up? GetReferenceToRawData's documentation warns : The obtained pointer is not always safe to use, because the reference counting may have actually disposed of the value. It seems a likely scenario without seeing the rest of your code. docwiki.embarcadero.com/Libraries/XE2/en/…J...
Hmm do you mean after creating the whole request but before passing the request to the service wrapper? At that point we can see the dynamic arrays if we debug the request. Not sure what happens later when the service wrapper parses the object to SOAP text. Could it be that at that point the arrays are getting cleaned up? Regarding GetReferenceToRawData, we would love to get rid of it, but we would need an alternative way to set the array's length and to assign the array copy to the property, which we haven't found so far. :-(Guillem Vicens
@J..., you were completely right. Somehow the dynamic arrays were falling out of scope when sending the request. We have managed to overcome the problem by assigning to each dynamic array property of the request a copy of the same dynamic array after creating the request but before sending it to the service. Not the best solution but it saves us from the problem. Please put your comment as an answer so I can accept it. Thanks a lot! :-)Guillem Vicens
I wouldn't really call my comment an answer - it was just a hypothesis, really. If you post your own solution to the problem, I'll give it an upvote! Another possible solution may be to manually increment the reference count when you know you are assigning a dynamic array like this. I'm not sure where that falls on the list of good practices, but it seems sensible to fill in for the compiler when you know what's going on and it doesn't. So long as the array will have its reference count decremented in the future when the new parent is freed it should be ok and would save making a copy.J...
@J..., done. I hope this answer can help other people when using RTTI with dynamic arrays.Guillem Vicens

1 Answers

1
votes

As @J... suggested the problem was that (somehow) the dynamic arrays were falling out of scope when sending the request.

To solve the problem we are assigning to each dynamic array property of the request a copy of the same dynamic array after creating the request but before sending it to the service. The copying is done just before sending the request as following:

Foo := TFooRequestMessageParser.getRequest;
//for each dynamic array property get a copy. This applies also to subproperties
Foo.DynArrayProperty := Copy(Foo.DynArrayProperty);
fooService.SendRequest(foo);

Another possibility would be doing as @J... also suggested, i.e., manually increment the reference count to avoid the dynamic array being freed. This is probably a more sensible and faster way to accomplish it, but for now we will stick to our solution.