2

Is there a simple way to determine what pipelines run for an API call? I am trying to extend the search in experience profile (Add columns) but can't find the pipelines so that I can add fields to the data table. I have already done the core DB work, but I need data in the API.

http://site/sitecore/api/ao/v1/contacts/search?&pageNumber=1&sort=visitCount%20desc&match=level

Update 1

The initialize routes for this API call is in Sitecore.Cintel.Endpoint.Plumbing.

routes.MapHttpRoute("cintel_contact_search", "sitecore/api/ao/v1/contacts/search", (object) new
{
     controller = "Contact",
     action = "Search"
});

enter image description here

5
  • A majority of the api calls do not actually have an associated pipeline. I have had to completely decompile the api used in SearchDataSource components and build my own to extend functionality.
    – Pete Navarra
    Feb 18, 2017 at 17:03
  • That being said, if an API call does indeed have a pipeline, a simple decompile should reveal which pipeline fairly quickly.
    – Pete Navarra
    Feb 18, 2017 at 17:05
  • I did find the pipeline for the Experience profile "<latest-visitors>" and extended that. But the search is being trouble.
    – Chris Auer
    Feb 18, 2017 at 17:05
  • this is just a normal controller action, you can override that by inheriting / using services from that controller Feb 18, 2017 at 18:50
  • @AndreyBobrov if I create another initialize pipeline that create that same MapHttpRoute route with my controller details, will mine override the one that was created before me? I'll make sure my config is last in the list.
    – Chris Auer
    Feb 18, 2017 at 18:53

1 Answer 1

4

Immediate Answer

You asked: "Is there a simple way to determine what pipelines run for an API call?"

In a nutshell, the only way to know for sure what an API controller is doing is to decompile the controller and take a peek. So, if that's not considered "simple", then the answer to your question is probably no.

That being said, I'd like to offer up some information specifically on the Contact Search functionality that you are asking about, and provide you with a path for adding to the results that you are wanting to extend.

I'll start with an explanation of how Contact Search works. I will also add that a lot of the API controllers have similar methodologies.

Understanding how Contact Search Works

The Contact API Controller, when Search() is executed, performs a Find(parameters) method of the configured ContactSearchProvider.

This is defined in the Sitecore.ExperienceProfile.config as such:

  <providers>
    
    <!-- Provides contact search functionality. -->
    <contactSearchProvider type="Sitecore.Cintel.ContactSearchProvider, Sitecore.Cintel" singleInstance="true" />
     
  </providers>

The ContactSearchProvider provides the brains behind how the search is performed and returns a ResultSet list of results based off of the interface IContactSearchResult.

Once the Search is complete, the Contact Controller then passes the results off of to a Result Transformer, that takes the results and converts them into a JSON response that is then passed through the API controller and back to the client browser.

The Result Transformer that is used in the Experience Profile is called ContactSearchResultTransformer and is defined in the config file Sitecore.ExperienceProfile.Client.config

      <contactSearchResultTransformer>
        <add key="default" type="Sitecore.Cintel.Client.Transformers.Contact.ContactSearchResultTransformer, Sitecore.Cintel.Client" />
      </contactSearchResultTransformer>

A look at the out of the box result transformer reveals an example on how to extend the JSON results.

public object Transform(ResultSet<List<IContactSearchResult>> resultSet)
{
  Assert.ArgumentNotNull((object) resultSet, "resultSet");
  if (resultSet.Data.Dataset.Count == 0)
    return (object) resultSet;
  List<IContactSearchResult> source = resultSet.Data.Dataset.FirstOrDefault<KeyValuePair<string, List<IContactSearchResult>>>().Value;
  if (source == null)
    return (object) resultSet;
  DateTime nowTime = DateTime.UtcNow;
  List<ContactSearchResultEx> list = source.Select<IContactSearchResult, ContactSearchResultEx>((Func<IContactSearchResult, ContactSearchResultEx>) (r => this.ExtendResult(r, nowTime))).ToList<ContactSearchResultEx>();
  source.Clear();
  source.AddRange((IEnumerable<IContactSearchResult>) list);
  return (object) resultSet;
}

private ContactSearchResultEx ExtendResult(IContactSearchResult result, DateTime nowTime)
{
  ContactSearchResultEx contactSearchResultEx = new ContactSearchResultEx(result);
  contactSearchResultEx.FormattedLatestVisitStartDateTime = this.timeConverter.FormatDateTime(result.LatestVisitStartDateTime);
  contactSearchResultEx.Recency = this.timeConverter.GetRecency(result.LatestVisitStartDateTime, nowTime);
  contactSearchResultEx.LatestVisitLocationDisplayName = this.textConverter.GetLocation(result.LatestVisitLocationCityDisplayName, result.LatestVisitLocationRegionDisplayName, result.LatestVisitLocationCountryDisplayName);
  contactSearchResultEx.EmailAddressExt = this.textConverter.GetEmail((object) result.PreferredEmail);
  string duration = this.timeConverter.GetDuration(result.LatestVisitStartDateTime, result.LatestVisitEndDateTime);
  contactSearchResultEx.LatestDuration = duration;
  string[] strArray = new string[3]
  {
    result.FirstName,
    result.MiddleName,
    result.Surname
  };
  string fullName = this.textConverter.GetFullName((ContactIdentificationLevel) result.IdentificationLevel, strArray);
  contactSearchResultEx.FullName = fullName;
  return contactSearchResultEx;
}

The magic here is that Transform() returns an object and not a concrete class.

Notice the method ExtendResult() returns a ContactSearchResultEx object. A look at this class shows that it is adding 6 different, additional JSON values to the search result.

[JsonProperty("formattedLatestVisitStartDateTime")]
public string FormattedLatestVisitStartDateTime { get; set; }

[JsonProperty("recency")]
public string Recency { get; set; }

[JsonProperty("latestVisitLocationDisplayName")]
public string LatestVisitLocationDisplayName { get; set; }

[JsonProperty("emailAddressExt")]
public string EmailAddressExt { get; set; }

[JsonProperty("fullName")]
public string FullName { get; set; }

[JsonProperty("latestDuration")]
public string LatestDuration { get; set; }

Extending the Contact Search Results Further

With a basic understanding of how the Contact Search works currently, to add additional fields, I would do the following:

  1. Start with creating a new ContactSearchResultExtended class that inherits from ContactSearchResultEx, adding the appropriate JSON properties to represent the new data that you want to return.

  2. Create a custom ContactSearchResultTransformer class that mimics the same functionality as the out of the box class, but adds in whatever logic is needed in order to add the additional data to the result set in your own ExtendResult method. Keep in mind that you pass in the result which contains Contact ID so that if you need to perform operations on the Contact ID in this method, you can. I would also preserve whatever logic is needed to satisfy the original extended results as well.

  3. Create the appropriate patch configs to apply the config changes needed to utilize the new custom <contactSearchResultTransformer>.

A Pipeline Approach (Theoretically)

Sadly, as shown here, there's really not a pipeline utilized to adjust, extend, or affect the result list. It's all in the configured Result Transformer. However, I love looking at unpipelined logic and seeing where I might be able to apply a pipeline.

In this case, the Transform() method of the Result Transformer seems like a potential candidate for adding a pipline. Specifically, I can see a pipeline process that allows for the addition of additional JSON values as part of the embedded function:

List<ContactSearchResultEx> list = source.Select<IContactSearchResult, ContactSearchResultEx>((Func<IContactSearchResult, ContactSearchResultEx>) (r => this.ExtendResult(r, nowTime))).ToList<ContactSearchResultEx>();

Then each processor would be responsible for adding values to the result. Obviously, I haven't actually tried this, nor do I think it's actually needed since you can easily just create your own transform results.

However... it's a possible thought.

2
  • 1
    As always Pete very well thought out answer. Let me try some of this out. I might need to make a new ContactSerchProvider. I need to add a new custom field to search on. So I'll need a new IndexedContact and a new QueryIndex to add my fields. IQueryable<IndexedContact> source = queryable.Where<IndexedContact>((Expression<Func<IndexedContact, bool>>) (q => q.FullName.MatchWildcard<string>(wildcard) || q.Emails.MatchWildcard<List<string>>(wildcard)));
    – Chris Auer
    Feb 18, 2017 at 19:39
  • 1
    Yeah, if you need to affect the way the search is actually performed, then it is reasonable to think you'll want your own search provider. That's also why I included this as part of the answer, so that you can see the provider is actually configurable and customizable. Happy Coding!
    – Pete Navarra
    Feb 18, 2017 at 19:43

Your Answer

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

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