unit XData.Json.Converter;

interface

uses
  Generics.Collections, System.TypInfo, System.SysUtils, System.Rtti, System.Classes,
  Aurelius.Mapping.Explorer,
  Aurelius.Mapping.Optimization,
  Bcl.Collections,
  Bcl.Json.Converters,
  Bcl.Json.BaseObjectConverter,
  Bcl.Json.ObjectConverter,
  Bcl.Json.Reader,
  Bcl.Json.Writer,
  Bcl.Rtti.ObjectFactory,
  XData.Aurelius.Model,
  XData.Json.Solver,
  XData.Model.Classes;

type
  TXDataJsonConverterFactory = class;

  TXDataJsonObjectConverter = class(TJsonBaseObjectConverter)
  strict private
    function IsTransient(AObject: TObject): boolean;
    function IsEmptyList(const AListObject: TObject): boolean;
  strict private
    procedure WriteSimpleProperties(AType: TXDataEntityType; AObject: TObject; Writer: TJsonWriter);
    procedure WriteNavigationProperties(AType: TXDataEntityType; AObject: TObject; Writer: TJsonWriter);
    procedure WriteBlobProperty(AObject: TObject; Writer: TJsonWriter; const APropName: string;
      ARttiInfo: TRttiOptimization);
    function WriteReferenceProperty(AObject: TObject; Writer: TJsonWriter;
      const PropName: string; RttiInfo: TRttiOptimization): Boolean;
    function BuildBlobReference(AObject: TObject; const APropName: string; RttiInfo: TRttiOptimization): string;
    function BuildReference(AObject: TObject; const APropName: string; RttiInfo: TRttiOptimization; ReferencedObject: TObject): string;
  strict private
    function ReadSimpleProperty(const PropId: string; AObject: TObject;
      Reader: TJsonReader): Boolean;
    function ReadNavigationProperty(const PropId: string; AObject: TObject;
      Reader: TJsonReader): Boolean;
    function ReadReferenceProperty(AObject: TObject; Reader: TJsonReader;
      RttiInfo: TRttiOptimization; const Annotation: string): Boolean;
    procedure ReadJsonObjectReference(AObject: TObject; Reader: TJsonReader;
      RttiInfo: TRttiOptimization; AsProxy: Boolean);
    procedure ReadNavigationPropertyInline(JReader: TJsonReader; Target: TObject;
      RttiInfo: TRttiOptimization);
    procedure ReadBlobProxyProperty(AObject: TObject; Reader: TJsonReader; RttiInfo: TRttiOptimization);
    procedure SetDummyProxy(AObject: TObject; RttiInfo: TRttiOptimization);
  protected
    function XFactory: TXDataJsonConverterFactory;
    function Explorer: TMappingExplorer;
    function XModel: TXDataAureliusModel;
  strict protected
    function ReadProperty(const PropName: string; AObject: TObject;
      AReader: TJsonReader): Boolean; override;
    procedure WriteProperties(AObject: TObject; Writer: TJsonWriter); override;
    function QualifiedName(AClass: TClass): string; override;
    function FindType(const AQualifiedName: string): TClass; override;
  public
    constructor Create(const AClass: TClass; AConverters: TJsonConverters; AFactory: TXDataJsonConverterFactory);
  end;

  TXDataJsonConverterFactory = class(TJsonObjectConverterFactory, IJsonConverterFactory)
  private
    FSerializeBlobReferences: boolean;
    FExpandLevel: integer;
    FForceProxyLoading: boolean;
    FProxyControllerOnReferences: boolean;
    FSolver: IJsonReferenceSolver;
    FIgnoreProxies: boolean;
    FIgnoreBlobProxies: boolean;
    FModel: TXDataAureliusModel;
    FExpander: IJsonExpander;
  public
    function CreateConverter(Converters: TJsonConverters; const ATypeToken: TTypeToken): IJsonTypeConverter; override;
    constructor Create(AModel: TXDataAureliusModel);
    procedure AfterConstruction; override;
  public
    property SerializeBlobReferences: boolean read FSerializeBlobReferences write FSerializeBlobReferences;
    property ExpandLevel: integer read FExpandLevel write FExpandLevel;
    property ForceProxyLoading: boolean read FForceProxyLoading write FForceProxyLoading;
    property ProxyControllerOnReferences: boolean read FProxyControllerOnReferences write FProxyControllerOnReferences;
    property IgnoreProxies: boolean read FIgnoreProxies write FIgnoreProxies;
    property Solver: IJsonReferenceSolver read FSolver write FSolver;
    property Expander: IJsonExpander read FExpander write FExpander;
    property IgnoreBlobProxies: boolean read FIgnoreBlobProxies write FIgnoreBlobProxies;
    property Model: TXDataAureliusModel read FModel;
  end;

implementation

uses
  Bcl.Rtti.Utils,
  Aurelius.Types.Blob,
  Aurelius.Types.Proxy,
  Sparkle.Utils,
  XData.Consts;

function AnnotatedName(const AName, AAnnotation: string): string;
begin
  Result := AName + '@' + AAnnotation;
end;

{ TJsonXDataConverterFactory }

procedure TXDataJsonConverterFactory.AfterConstruction;
begin
  inherited;
  InstanceTypeProp := JsonXDataType;
  InstanceReferenceProp := JsonDefaultInstanceReferenceProp;
  InstanceIdProp := JsonDefaultInstanceIdProp;
end;

constructor TXDataJsonConverterFactory.Create(AModel: TXDataAureliusModel);
begin
  FModel := AModel;
  inherited Create;
  InstanceRefSerialization := TInstanceRefSerialization.Always;
  InstanceTypeSerialization := TInstanceTypeSerialization.Always;
  InstanceLoopHandling := TInstanceLoopHandling.Reference;
  UnknownMemberHandling := TUnknownMemberHandling.Error;
end;

function TXDataJsonConverterFactory.CreateConverter(Converters: TJsonConverters;
  const ATypeToken: TTypeToken): IJsonTypeConverter;
var
  Clazz: TClass;
begin
  Result := nil;
  if ATypeToken.IsClass then
  begin
    Clazz := ATypeToken.GetClass;

    { note: in test below, the best way would be to call FindEntityType, so
      that only XData entities will be processed by this converters. However,
      this will break backward compatibility because the Model Builder can import
      service operations (actions) that use Aurelius classes without those classes
      being present in the XData model. So we need to process any Aurelius entity
      in the explorer, not only those present in the XData model }
//    if XModel.XModel.FindEntityType(Clazz) <> nil then
    if FModel.Explorer.Hierarchy.Contains(Clazz) then
      Result := TXDataJsonObjectConverter.Create(Clazz, Converters, Self)
    else
      Result := inherited CreateConverter(Converters, ATypeToken);
  end;
end;

{ TXDataJsonObjectConverter }

function TXDataJsonObjectConverter.BuildBlobReference(AObject: TObject;
  const APropName: string; RttiInfo: TRttiOptimization): string;
begin
  Result := TSparkleUtils.PercentEncode(XFactory.Model.CanonicalUrl(AObject, APropName));
end;

function TXDataJsonObjectConverter.BuildReference(AObject: TObject;
  const APropName: string; RttiInfo: TRttiOptimization;
  ReferencedObject: TObject): string;
begin
  if ReferencedObject <> nil then
    Result := TSparkleUtils.PercentEncode(XModel.CanonicalUrl(ReferencedObject))
  else
    Result := TSparkleUtils.PercentEncode(XModel.CanonicalUrl(AObject, APropName));
end;

constructor TXDataJsonObjectConverter.Create(const AClass: TClass;
  AConverters: TJsonConverters; AFactory: TXDataJsonConverterFactory);
begin
  inherited Create(AClass, AConverters, AFactory);
end;

function TXDataJsonObjectConverter.Explorer: TMappingExplorer;
begin
  Result := XModel.Explorer;
end;

function TXDataJsonObjectConverter.FindType(
  const AQualifiedName: string): TClass;
var
  EntityType: TXDataEntityType;
begin
  Result := nil;
  EntityType := XModel.FindEntityType(AQualifiedName);
  if EntityType <> nil then
    Result := XModel.GetEntityTypeClass(EntityType);
end;

function TXDataJsonObjectConverter.IsEmptyList(
  const AListObject: TObject): boolean;
var
  List: IObjectList;
begin
  Result := true;
  if (AListObject <> nil) then
  begin
    List := Explorer.AsObjectList(AListObject);
    if (List <> nil) and (List.Count > 0) then
      Result := false;
  end;
end;

function TXDataJsonObjectConverter.IsTransient(AObject: TObject): boolean;
begin
  Result := not Explorer.HasIdValue(AObject);
end;

function TXDataJsonObjectConverter.QualifiedName(AClass: TClass): string;
begin
  Result := XModel.GetEntityTypeByClass(AClass).QualifiedName;
end;

procedure TXDataJsonObjectConverter.ReadBlobProxyProperty(AObject: TObject;
  Reader: TJsonReader; RttiInfo: TRttiOptimization);
var
  Blob: TBlob;
  Value: TValue;
  RefSolver: IJsonReferenceSolver;
begin
  // if code reaches here, then the blob in json is represented as proxy. We have the following options:
  // a) Ignore the value
  // b) Set a proxy controller to load the blob as lazy
  // c) load the blob solving the reference and setting the blob value now
  //
  // If IgnoreBlobProxies is true, then we do (a) and leave. If we don't have a solver, we should also
  // leave because we won't be able to load the blob anyway
  // Otherwise, if the blob is set as lazy, we do (b), otherwise we must force load and do (c)
  RefSolver := XFactory.Solver;
  if XFactory.IgnoreBlobProxies or (RefSolver = nil) then
    Reader.SkipValue
  else
  begin
    if Explorer.IsLazy(RttiInfo) then
    begin
      // set proxy controller
      Explorer.SetBlobController(Blob,
        TDeserializerBlobController.Create(Reader.ReadString, RttiInfo, RefSolver));
    end
    else
    begin
      // otherwise find the reference directly (load the object right away)
      Blob.AsBytes := RefSolver.SolveBlob(Reader.ReadString, RttiInfo);
    end;
  end;
  Value := TValue.From<TBlob>(Blob);
  Explorer.SetMemberValue(AObject, RttiInfo, Value);
end;

procedure TXDataJsonObjectConverter.ReadJsonObjectReference(AObject: TObject;
  Reader: TJsonReader; RttiInfo: TRttiOptimization; AsProxy: Boolean);
var
  Value: TObject;
  OldValue: TObject;
  ProxyType: TProxyType;
  TargetClass: TClass;
  Solver: IJsonReferenceSolver;
begin
  Solver := XFactory.Solver;
  TargetClass := RttiInfo.RealType.AsInstance.MetaclassType;
  if RttiInfo.IsProxy and (Solver <> nil) and (XFactory.ProxyControllerOnReferences or AsProxy) then
  begin
    { we enter here if:
      1. Mapping is proxy and there is Solver - which means we can "proxify" the content
      and either:
         a) ProxyControllerOnReferences (meaning @xdata.ref will behave as proxy, mostly
                                         for Delphi client-side usage)
         b) AsProxy is true (meaning json has @xdata.proxy thus we should always consider
                             it as a proxy no matter what)
    }

    if Explorer.IsObjectList(TargetClass) then
      ProxyType := TProxyType.List
    else
      ProxyType := TProxyType.Single;

    Explorer.SetProxyController(AObject, RttiInfo,
      TDeserializerProxyController.Create(Reader.ReadString, RttiInfo,
        ProxyType, Solver, Depth));
  end
  else
  begin
    // otherwise find the reference directly (load the object right away)
    if (Solver <> nil) then
      Value := Solver.SolveReference(Reader.ReadString, RttiInfo, Depth)
    else
    begin
      Reader.SkipValue;
      Value := nil;
    end;

    if Explorer.IsObjectList(TargetClass) then
    begin
      if Value <> nil then
      begin
        OldValue := Explorer.GetMemberValue(AObject, RttiInfo).AsObject;
        try
          Explorer.SetMemberValue(AObject, RttiInfo, Value);
        finally
          OldValue.Free;
        end;
      end;
    end
    else
      Explorer.SetMemberValue(AObject, RttiInfo, Value);
  end;
end;

function TXDataJsonObjectConverter.ReadNavigationProperty(const PropId: string;
  AObject: TObject; Reader: TJsonReader): Boolean;
var
  NavProp: TXDataNavigationProperty;
  PropName, Annotation: string;
  EntityType: TXDataEntityType;
  RttiInfo: TRttiOptimization;
begin
  EntityType := XModel.GetEntityTypeByClass(AObject.ClassType);
  TSparkleUtils.GetNameAndAnnotation(PropId, PropName, Annotation);
  NavProp := EntityType.FindNavigationProperty(PropName);
  if NavProp <> nil then
  begin
    RttiInfo := XModel.GetNavPropInfo(NavProp);
    if not ReadReferenceProperty(AObject, Reader, RttiInfo, Annotation) then
      ReadNavigationPropertyInline(Reader, AObject, RttiInfo);
    Result := True;
  end
  else
    Result := False;
end;

procedure TXDataJsonObjectConverter.ReadNavigationPropertyInline(
  JReader: TJsonReader; Target: TObject; RttiInfo: TRttiOptimization);
var
  Value: TValue;
  TargetClass: TClass;
//  ReplaceValue: Boolean;
//  OldObject: TObject;
begin
  TargetClass := RttiInfo.RealType.AsInstance.MetaclassType;
  if not Explorer.IsObjectList(TargetClass) then
  begin
    { // Uncomment this when we decide to PATCH associated objects
    Value := Explorer.GetMemberValue(Target, RttiInfo);
    OldObject := Value.AsObject;
    Converters.Get(TTypeToken.FromTypeInfo(RttiInfo.RealType.Handle)).ReadJson(JReader, Value);
    if OldObject <> Value.AsObject then
      Explorer.SetMemberValue(Target, RttiInfo, Value);
    }
    Value := TValue.Empty;
    Converters.Get(RttiInfo.RealType.Handle).ReadJson(JReader, Value);
    Explorer.SetMemberValue(Target, RttiInfo, Value);
  end
  else
  begin
    Value := Explorer.GetMemberValue(Target, RttiInfo);
    Converters.Get(RttiInfo.RealType.Handle).ReadJson(JReader, Value);
  end;
end;

function TXDataJsonObjectConverter.ReadProperty(const PropName: string;
  AObject: TObject; AReader: TJsonReader): Boolean;
begin
  Result := ReadSimpleProperty(PropName, AObject, AReader)
    or ReadNavigationProperty(PropName, AObject, AReader);
end;

function TXDataJsonObjectConverter.ReadReferenceProperty(AObject: TObject;
  Reader: TJsonReader; RttiInfo: TRttiOptimization; const Annotation: string): Boolean;
begin
  if Annotation = '' then
    Exit(false);

  Result := true;
  if Annotation = JsonProxyAnnotation then
  begin
    if not XFactory.IgnoreProxies then
      ReadJsonObjectReference(AObject, Reader, RttiInfo, True)
    else
    begin
      // Even though we are ignoring proxies here, set a dummy proxy controller
      // to indicate that the object (mainly list) is proxied so the client didn't send
      // an empty list/object, but instead, it's "not changed". This helps on PUT/MERGE
      // operations where for example if the user sent a proxy, we should not clear the list
      // but instead leave it as-is
      SetDummyProxy(AObject, RttiInfo);
      Reader.SkipValue;
    end;
  end
  else
  if Annotation = JsonReferenceAnnotation then
    ReadJsonObjectReference(AObject, Reader, RttiInfo, False)
  else
    Reader.SkipValue;
end;

function TXDataJsonObjectConverter.ReadSimpleProperty(const PropId: string;
  AObject: TObject; Reader: TJsonReader): Boolean;
var
  Prop: TXDataSimpleProperty;
  PropName, Annotation: string;
  EntityType: TXDataEntityType;
  RttiInfo: TRttiOptimization;
  Value: TValue;
begin
  EntityType := XModel.GetEntityTypeByClass(AObject.ClassType);
  TSparkleUtils.GetNameAndAnnotation(PropId, PropName, Annotation);
  Prop := EntityType.FindProperty(PropName);
  if Prop <> nil then
  begin
    RttiInfo := XModel.GetPropertyInfo(Prop).RttiInfo;
    if XModel.Explorer.IsBlob(RttiInfo.RealType.Handle) and (Annotation = JsonProxyAnnotation) then
      ReadBlobProxyProperty(AObject, Reader, RttiInfo)
    else
    begin
      Value := TValue.Empty;
      Converters.Get(RttiInfo.RealType.Handle).ReadJson(Reader, Value);
      XModel.Explorer.SetMemberValue(AObject, RttiInfo, Value);
    end;
    Result := True;
  end
  else
    Result := False;
end;

procedure TXDataJsonObjectConverter.SetDummyProxy(AObject: TObject;
  RttiInfo: TRttiOptimization);
var
  ProxyType: TProxyType;
  TargetClass: TClass;
begin
  TargetClass := RttiInfo.RealType.AsInstance.MetaclassType;
  if RttiInfo.IsProxy then
  begin
    if Explorer.IsObjectList(TargetClass) then
      ProxyType := TProxyType.List
    else
      ProxyType := TProxyType.Single;

    Explorer.SetProxyController(AObject, RttiInfo,
      TDummyProxyController.Create(RttiInfo, ProxyType, Explorer.ObjectFactory));
  end;
end;

procedure TXDataJsonObjectConverter.WriteBlobProperty(AObject: TObject;
  Writer: TJsonWriter; const APropName: string; ARttiInfo: TRttiOptimization);
var
  Value: TValue;
  StreamValue: TValue;
  InlineBlob: boolean;
  Blob: TBlob;
begin
  StreamValue := Explorer.GetMemberValue(AObject, ARttiInfo);

  if XFactory.SerializeBlobReferences then // and StreamValue.IsType<TBlob> then
  begin
    // If type is TBlob, it will always be provided as proxy (lazy).
    // Unless the blob is empty (null) or object is transient (so there is no url for the blob)
    Blob := StreamValue.AsType<TBlob>;
    InlineBlob := (Blob.Available and (Blob.Size = 0)) or IsTransient(AObject);
  end
  else
    InlineBlob := true;

  if InlineBlob then
  begin
    Writer.WriteName(APropName);
    Value := Explorer.GetMemberValue(AObject, ARttiInfo);
    Converters.Get(ARttiInfo.RealType.Handle).WriteJson(Writer, Value);
  end
  else
  begin
    // if blob is not inline, serialize value using format  "BlobProperty@xdata.proxy": "url"
    Writer
      .WriteName(AnnotatedName(APropName, JsonProxyAnnotation))
      .WriteString(BuildBlobReference(AObject, APropName, ARttiInfo));
  end;
end;

procedure TXDataJsonObjectConverter.WriteNavigationProperties(
  AType: TXDataEntityType; AObject: TObject; Writer: TJsonWriter);
var
  NavProp: TXDataNavigationProperty;
  Value: TValue;
  RttiInfo: TRttiOptimization;
  MustExpand: Boolean;
begin
  if AType.BaseType <> nil then
    WriteNavigationProperties(AType.BaseType, AObject, Writer);

  for NavProp in AType.NavigationProperties do
  begin
    RttiInfo := XModel.GetNavPropInfo(NavProp);
    MustExpand := (XFactory.Expander <> nil) and XFactory.Expander.MustExpand(NavProp);
    if MustExpand or not WriteReferenceProperty(AObject, Writer, NavProp.Name, RttiInfo) then
    begin
      if MustExpand then
        Explorer.ForceProxyLoad(AObject, RttiInfo);
      Value := XModel.Explorer.GetMemberValue(AObject, RttiInfo);
      NotifyPropertyWrite(RttiInfo.MemberRef, Value);
      Writer.WriteName(NavProp.Name);
      Converters.Get(RttiInfo.RealType.Handle).WriteJson(Writer, Value);
    end;
  end;
end;

procedure TXDataJsonObjectConverter.WriteProperties(AObject: TObject;
  Writer: TJsonWriter);
var
  EntityType: TXDataEntityType;
begin
  EntityType := XModel.GetEntityTypeByClass(AObject.ClassType);
  WriteSimpleProperties(EntityType, AObject, Writer);
  WriteNavigationProperties(EntityType, AObject, Writer);
end;

type
  TIntExplorer = class(TMappingExplorer)
  end;

function TXDataJsonObjectConverter.WriteReferenceProperty(AObject: TObject;
  Writer: TJsonWriter; const PropName: string; RttiInfo: TRttiOptimization): Boolean;
type
  TJsonEntityMode = (Ref, InlineObj, Proxy);
var
  AssociatedObject: TObject;
  ProxyInfo: IProxyInfo;
  IntExplorer: TIntExplorer;
  ProxyValue: TValue;
  Mode: TJsonEntityMode;
  UseReferences: boolean;
begin
  Result := true;
  UseReferences := (Depth > XFactory.ExpandLevel);

  // Object can be represented in three different modes:
  //
  // 1. Ref
  //   This is annotated property in format "PropName@xdata.ref": "Type(id)"
  //   It must be used when current level is higher than expand level, so we can't add inline objects anymore.
  //   If that's the case, still there are conditions
  //   Conditions are:
  //      a) object is available in property (proxy is loaded, for example)
  //      b) Must have an id (not transient)
  //
  // 2. InlineObj
  //    This must be used if depth level did not reach maximum ExpandLevel.
  //    Or if object is available and does not have an id (meaning it's a transient object)
  //    It's mostly used when serializing   //   objects from client side, to perform deep inserts,
  //    but can also be used by the server if client requires an ExpandLevel higher than 0 so some levels are to be put inline
  //
  // 3. Proxy
  //    Used when object is a proxy and the proxy is not loaded. We could load the proxy
  //    and use option 1.Ref but this would slow down the process. Same notation as
  //    ref, but using "xdata.proxy" annotation
  //

  AssociatedObject := nil;
  ProxyInfo := nil;

  // First, check if property is proxy. If it is, let's do some more advanced handling for it
  if RttiInfo.IsProxy then
  begin
    IntExplorer := TIntExplorer(Self.Explorer);
    ProxyValue := IntExplorer.GetValue(RttiInfo.MemberRef, AObject);
    ProxyInfo := IntExplorer.GetProxyInfo(RttiInfo, ProxyValue);

    if (ProxyInfo <> nil) and not IntExplorer.IsProxyLoaded(AObject, RttiInfo) then
    begin
      // we should load the proxy if we are forced to (propery ForceProxyLoading is True)
      // and don't use references.
      // or unless we don't have a Builder property (which means we just can't serialize as proxy)
      if XFactory.ForceProxyLoading and not UseReferences then
      begin
        IntExplorer.ForceProxyLoad(AObject, RttiInfo);
        Mode := TJsonEntityMode.InlineObj;
      end
      else
        Mode := TJsonEntityMode.Proxy;
    end else
    begin
      // proxy info = nil or proxy is loaded means the object is available (or nil), thus we
      // can get the object associated with it, it's already available
      // let's set the mode as reference for now (code will later check if reference is transient or not)
      Mode := TJsonEntityMode.Ref;
      AssociatedObject := Explorer.GetMemberValue(AObject, RttiInfo).AsObject;
    end;
  end
  else
  begin
    Mode := TJsonEntityMode.Ref;
    AssociatedObject := Explorer.GetMemberValue(AObject, RttiInfo).AsObject;
  end;

  // different code for single-value association and multi-valued association
  if not Explorer.IsObjectList(RttiInfo.RealType.AsInstance.MetaclassType) then
  begin
    // we must check if Ref mode can be serialized. If the object is transient, then we can't do that
    // and must serialize it inline. Also to optimize, if the associatedobject is nil,
    // we don't need to serialize it as ref, we can serialize it as null and avoid roundtrips from the client
    // finally, if UseReferences is false, we can't use it and must serialize object inline
    if (Mode = TJsonEntityMode.Ref) then
      // if AssociatedObject is nil or is transient, then we must change the serialization mode
      // from ref to inline. We can't serialize it as ref because there is no id.
      // Also, if there is no reference builder, we should also serialize as inline
      if (AssociatedObject = nil) or IsTransient(AssociatedObject) or not UseReferences then
        Mode := TJsonEntityMode.InlineObj;

    // now serialize properly
    case Mode of
      TJsonEntityMode.Ref:
        begin
          NotifyPropertyWrite(RttiInfo.MemberRef, AssociatedObject);
          Writer.WriteName(AnnotatedName(PropName, JsonReferenceAnnotation));
          Writer.WriteString(BuildReference(AObject, PropName, RttiInfo, AssociatedObject));
        end;
      TJsonEntityMode.InlineObj:
          Result := false;
      TJsonEntityMode.Proxy:
        begin
          // TODO: use a better-performance URL for proxy
          Writer.WriteName(AnnotatedName(PropName, JsonProxyAnnotation));
          Writer.WriteString(BuildReference(AObject, PropName, RttiInfo, nil));
        end;
    end;
  end
  else
  begin
    // we must check if Ref mode can be serialized for lists. If the master object (object holding the list) is transient,
    // then we can't do that and must serialize list inline. Also to optimize, if the list is empty
    // we don't need to serialize it as ref, we can serialize it as empty array and avoid roundtrips from the client.
    // Finally, if UseReferences is false, we can't use it and must serialize list inline
    if (Mode = TJsonEntityMode.Ref) then
      // if AssociatedObject is nil or is master transient or list is empty, then we must change the serialization mode
      // from ref to inline. We can't serialize it as ref because there is no master id.
      if (AssociatedObject = nil) or IsTransient(AObject) or not UseReferences or IsEmptyList(AssociatedObject) then
        Mode := TJsonEntityMode.InlineObj;

    case Mode of
      TJsonEntityMode.Ref, TJsonEntityMode.Proxy:
        begin
          // For now, many-valued associations are always serialized as proxy, using canonical url
          // TODO: use a better-performance URL for proxy
          Writer.WriteName(AnnotatedName(PropName, JsonProxyAnnotation));
          Writer.WriteString(BuildReference(AObject, PropName, RttiInfo, nil));
        end;
      TJsonEntityMode.InlineObj:
          Result := false;
    end;
  end;
end;

procedure TXDataJsonObjectConverter.WriteSimpleProperties(
  AType: TXDataEntityType; AObject: TObject; Writer: TJsonWriter);
var
  RttiInfo: TRttiOptimization;
  Prop: TXDataSimpleProperty;
  Value: TValue;
begin
  if AType.BaseType <> nil then
    WriteSimpleProperties(AType.BaseType, AObject, Writer);
  for Prop in AType.Properties do
  begin
    RttiInfo := XModel.GetPropertyInfo(Prop).RttiInfo;
    if XModel.Explorer.IsBlob(RttiInfo.RealType.Handle) then
      WriteBlobProperty(AObject, Writer, Prop.Name, RttiInfo)
    else
    begin
      Writer.WriteName(Prop.Name);
      Value := XModel.Explorer.GetMemberValue(AObject, RttiInfo);
      Converters.Get(RttiInfo.RealType.Handle).WriteJson(Writer, Value);
    end;
  end;
end;

function TXDataJsonObjectConverter.XFactory: TXDataJsonConverterFactory;
begin
  Result := TXDataJsonConverterFactory(Factory);
end;

function TXDataJsonObjectConverter.XModel: TXDataAureliusModel;
begin
  Result := XFactory.Model;
end;

end.
