4

I am working on display facet data on the Experience profile tab. I am following the below article for the same.

http://zhenyuan.azurewebsites.net/post/displaying-custom-facets-in-contact-profile

But when I am hitting my custom tab, debugger comes to defined routes (Controller and Action) which I registered and successfully returns the Custom Facts data, but unfortunately, it's throwing me below different error's on the console:

(1) ExceptionMessage: "The 'ObjectContent'1' type failed to serialize the

response body for content type 'application/json; charset=utf-8'."

 

(2) Exception Newtonsoft.Json.JsonSerializationException: Self referencing loop detected with type 'Sitecore.XConnect.Schema.XdbProperty'. Path 'XObject[0].Key.DeclaringType.DeclaredProperties'.

URL which executes on custom tab click is:

http://helixbase.sc/sitecore/api/ao/v1/contacts/a2c0c034-c6f9-0000-0000-058b1d973da1/customformfields/

If I execute this URL in the seperate tab then it returns me value as below:  

{"k__BackingField":"Prince Sharma","k__BackingField":"[email protected]","k__BackingField":"9654477778","k__BackingField":"Sharma","k__BackingField":"PAS34567NUM890","k__BackingField":"Hindi","k__BackingField":"10b430b2-a9c1-463b-be1e-315af66fdbde","k__BackingField":"2019-04-25T11:05:16.7443003Z","k__BackingField":true}

JS File

define(["sitecore", "/-/speak/v1/experienceprofile/DataProviderHelper.js",
"/-/speak/v1/experienceprofile/CintelUtl.js"],
function (sc, providerHelper, cintelUtil) {
    var app = sc.Definitions.App.extend({
        initialized: function () {
            var localUrl = "/customformfields/";
            providerHelper.setupHeaders([
                { urlKey: localUrl }
            ]);
            //providerHelper.addDefaultTransformerKey();
            var url = sc.Contact.baseUrl + localUrl;
            var $that = this;
            providerHelper.initProvider(this.customDataProvider, "", url, this.customTabMessageBar);
            providerHelper.getData(this.customDataProvider,
                $.proxy(function (jsondata) {
                    cintelUtil.setText($that.SurNameValue, jsondata.SurName, true);
                    cintelUtil.setText($that.PassportValue, jsondata.PassportNumber, true);
                }));
        }
    });
    return app;
});

Rest of the Controller, Action, InitializeRoutes code I implemented the same as an above-defined article. Thanks.

3 Answers 3

4

I have faced the same issue during the implementation of Display custom facet on the Experience profile tab. @x3mxray helped me as well to implement the same.

As per the article http://zhenyuan.azurewebsites.net/post/displaying-custom-facets-in-contact-profile you followed, the controller will return your facets values, but it's not serialized so you will get this type of error.

I have serialized Facets Values using Newtonsoft.Json and after that, it works fine for me. Here Newtonsoft.Json will serialize and give you a string object so before return, it to the JSON data you need to parse it as below: (taken code sample from your refered article)

try
{
    var contactManager = GetContactManager();
    var contact = contactManager.LoadContactReadOnly(contactId);
    if (contact == null)
    {
        throw new ContactNotFoundException();
    }
    var subscriptionsFacet = contact.GetFacet<ISubscriptions>("Subscriptions");
    var serializerSettings = new JsonSerializerSettings
    {
        ContractResolver = new XdbJsonContractResolver(client.Model, serializeFacets: true, serializeContactInteractions: true),
        DateTimeZoneHandling = DateTimeZoneHandling.Utc,
        DefaultValueHandling = DefaultValueHandling.Ignore
    };
    return Newtonsoft.Json.Linq.JObject.Parse(JsonConvert.SerializeObject(subscriptionsFacet, serializerSettings));
}
catch (ContactNotFoundException ex)
{
    return Request.CreateResponse(HttpStatusCode.NotFound, ex.Message);
}

You can find serialization setting more details from here.

1
  • Thanks for the support. It works fine for me. May 17, 2019 at 6:53
3

Your issue is that each facet has an XObject object on it that contains a lot of circular references. The XObject will never serialize, you just need to tell NewtonSoft to ignore it when serializing. You can do this with a DefaultContractResolver.

You can see this working in my repo. https://github.com/buildabonfire/Bonfire.Analytics.Dto Your facets will look like this:

{
   "Key": "Emails",
      "Value": {
      "Others": {},
      "PreferredKey": "Primary Email",
      "PreferredEmail": {
      "Validated": false,
      "SmtpAddress": "[email protected]",
      "BounceCount": 0
   },
   "ConcurrencyToken": "9685b921-6c5a-4cde-becd-74e9b3f6d74c",
   "LastModified": "2019-05-14T02:31:56.2523352Z",
   "ClrTypePresent": true
   }
},
{
   "Key": "Personal",
   "Value": {
      "Birthdate": "1971-10-12T00:00:00Z",
      "FirstName": "Chris",
      "MiddleName": null,
      "LastName": "Auer",
      "Gender": "M",
      "JobTitle": null,
      "Nickname": null,
      "Suffix": null,
      "Title": null,
      "PreferredLanguage": null,
      "ConcurrencyToken": "e73f4cad-0264-4a8d-bba4-c51b1293b6d8",
      "LastModified": "2019-04-28T16:30:43.7406558Z",
      "ClrTypePresent": true
   }
},

I have a custom controller action that returns Json Text. https://github.com/buildabonfire/Bonfire.Analytics.Dto/blob/master/Bonfire.Analytics.Dto/Serialization/JsonNet.cs

PropertyRenameAndIgnoreSerializerContractResolver

This is the code that will tell NewtonSoft to ignore a property.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Web;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace Bonfire.Analytics.Dto.Extensions
{
    public class PropertyRenameAndIgnoreSerializerContractResolver : DefaultContractResolver
    {
        private readonly Dictionary<Type, HashSet<string>> _ignores;
        private readonly Dictionary<Type, Dictionary<string, string>> _renames;

        public PropertyRenameAndIgnoreSerializerContractResolver()
        {
            _ignores = new Dictionary<Type, HashSet<string>>();
            _renames = new Dictionary<Type, Dictionary<string, string>>();
        }

        public void IgnoreProperty(Type type, params string[] jsonPropertyNames)
        {
            if (!_ignores.ContainsKey(type))
                _ignores[type] = new HashSet<string>();

            foreach (var prop in jsonPropertyNames)
                _ignores[type].Add(prop);
        }

        public void RenameProperty(Type type, string propertyName, string newJsonPropertyName)
        {
            if (!_renames.ContainsKey(type))
                _renames[type] = new Dictionary<string, string>();

            _renames[type][propertyName] = newJsonPropertyName;
        }

        protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
        {
            var property = base.CreateProperty(member, memberSerialization);

            if (IsIgnored(property.DeclaringType, property.PropertyName))
            {
                property.ShouldSerialize = i => false;
                property.Ignored = true;
            }

            if (IsRenamed(property.DeclaringType, property.PropertyName, out var newJsonPropertyName))
                property.PropertyName = newJsonPropertyName;

            return property;
        }

        private bool IsIgnored(Type type, string jsonPropertyName)
        {
            if (!_ignores.ContainsKey(type))
                return false;

            return _ignores[type].Contains(jsonPropertyName);
        }

        private bool IsRenamed(Type type, string jsonPropertyName, out string newJsonPropertyName)
        {
            Dictionary<string, string> renames;

            if (!_renames.TryGetValue(type, out renames) || !renames.TryGetValue(jsonPropertyName, out newJsonPropertyName))
            {
                newJsonPropertyName = null;
                return false;
            }

            return true;
        }
    }
}

Executing the serialization

{
    var jsonResolver = new PropertyRenameAndIgnoreSerializerContractResolver();

    // THIS IS WHERE WE TELL IT TO IGNORE XObject
    jsonResolver.IgnoreProperty(typeof(XdbExtensible), "XObject");

    var serializerSettings = new JsonSerializerSettings();
    serializerSettings.ContractResolver = jsonResolver;

    var formatting = Formatting.None;
    var sb = new StringBuilder();
    var sw = new StringWriter(sb);

    var writer = new JsonTextWriter(sw) { Formatting = formatting };
    var serializer = JsonSerializer.Create(serializerSettings);
    serializer.Serialize(writer, this.Data);

    // your JSON is in sb.ToString()
}
1

It seems that you try to return Sitecore.XConnect.Facet in server response to browser. Facet is a complex model with generic self-referenced properties.

Try to create a simple class for view model only with fields that you need on client side, populate them from facet and use it for server response instead of Facet model.

I have also faced this issue when I implemented custom tab with all contact facets for Experience profile. You should skip 'native' properties of Sitecore.XConnect.Facet type for output to resolve this issue.

Example of my custom report processor:

 public  class PopulateWithFacetData: ReportProcessorBase
 {
        private static readonly Type BaseType = typeof(Sitecore.XConnect.Facet);
        readonly List<string> _skippedProperties = BaseType.GetProperties().Select(x => x.Name).ToList();
        private const int maxDepth = 3;
        private bool IsDefaultProperty(string propertyName)
        {
                return _skippedProperties.Contains(propertyName);
        }

        public override void Process(ReportProcessorArgs args)
        {
            var xConnectService = new XconnectService();
            var facets = Task.Run(() => xConnectService.GetContactFacets(args.ReportParameters.ContactId)).Result;

            var table = CreateDataTable(facets); 
            args.ResultSet.Data.Dataset[args.ReportParameters.ViewName] = table;
        }

        public DataTable CreateDataTable(IReadOnlyDictionary<string,Facet> list)
        {
            DataTable dataTable = new DataTable();

            dataTable.Columns.Add(new DataColumn("FacetName", "".GetType()));
            dataTable.Columns.Add(new DataColumn("FieldName", "".GetType()));
            dataTable.Columns.Add(new DataColumn("FieldValue", "".GetType()));
            dataTable.Columns.Add(new DataColumn("DepthLevel", "".GetType()));
            dataTable.Columns.Add(new DataColumn("Type", "".GetType()));
            foreach (var entity in list)
            {
                if(entity.Key!="Avatar")
                    AddChildren(ref dataTable, entity.Key, entity.Value, 0);
            }

            return dataTable;
        }

        private void AddChildren(ref DataTable dataTable,string contactFacetName, dynamic obj, int depth, string typePath="")
        {
            if (depth > maxDepth) return; 
            Type type = obj.GetType();
            var properties = type.GetProperties();
            foreach (var p in properties)
            {
                object[] values = new object[5];

                values[0] = contactFacetName;
                values[1] = p.Name;
                var value = p.GetValue(obj) ?? "";
                values[2] = value;
                values[3] = depth;


                Type valueType = value.GetType();
                if (valueType.FullName != null)
                {
                    if (!valueType.FullName.StartsWith("System.Collections"))
                        if (!IsDefaultProperty(p.Name))
                        {
                            var path = typePath+ "$" + values[1];
                            values[4] = typePath;
                            if (valueType.Namespace.StartsWith("System"))
                            {
                                dataTable.Rows.Add(values);
                            }
                            else
                            {
                                values[2] = "$object$";
                                dataTable.Rows.Add(values);
                                AddChildren(ref dataTable, contactFacetName, value, depth+1, path);
                            }
                        }
                }
            }
        }

}

I used reflection to make solution generic. You can easy adapt it for your concrete facet. We implemented it for previous year Hackathon (sitecore 9.0), I hope it can be useful for your custom tab implementation. https://github.com/Sitecore-Hackathon/2018-Sitecore-Friends/blob/master/documentation/README.md

4
  • This is very because Sitecore just sends back objects at run time. The real facet can be of any type. You will have to use a lot of reflector to find the type and new up a new one. I went this route and it turned into a mess.
    – Chris Auer
    May 16, 2019 at 19:49
  • Yes, this is because of cycled reference in XObject field. I faced the same issue when I implemented generic model for all facets read/write operations for my custom experience profile tab. And I solved it with skipping 'native' facet properties for output model. That is what I mean. Custom output model can solve it without diving in serializer. github.com/Sitecore-Hackathon/2018-Sitecore-Friends/blob/master/…
    – x3mxray
    May 16, 2019 at 20:16
  • Nice. You should post some of that code so it is available for the next person.
    – Chris Auer
    May 16, 2019 at 20:20
  • 1
    Thank you. I add clarification in my answer.
    – x3mxray
    May 16, 2019 at 20:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.