2

I have a scenario in which I want to return some facet data associated with an xDB contact via an ApiController. However, when i try to access the Tracker.Current.Contact property it is null.

What is the best solution to ensure access to a contact when performing ApiController calls?

Currently my code looks like this:

public class WidgetsController : ApiController {

[HttpGet]
public IEnumerable<Widget> Get()
{
    if (Sitecore.Analytics.Tracker.Current != null) 
    {
        var contact = Sitecore.Analytics.Tracker.Current.Contact;
        if (contact != null)
        {
            return WidgetsRepository.Get(contact);
        }
    }
}
4

Even though your code is running in an API controller, the browser will still send cookies to the server, including the SC_ANALYTICS_GLOBAL_COOKIE. This cookie contains the current contact's ID in plain text. You can extract the contact ID in your WebAPI controller and get the current contact from the session (if it's there) or from the database. Here's how:

var contactCookie = new Sitecore.Analytics.Web.ContactKeyCookie();

if (!contactCookie.IsNewContact)
{
    Guid contactId = contactCookie.ContactId;

    var contactRepository = (ContactRepositoryBase)Factory.CreateObject("contactRepository", true);
    var sharedSessionManager = (SharedSessionStateManager)Factory.CreateObject("tracking/sharedSessionState/manager", true)

    // Try to load the contact from the Shared Session.
    Contact sessionContact = sharedSessionManager.LockAndLoadContact(contactId);

    if (sessionContact != null)
    {
        try
        {
            // Read facet data from sessionContact
        }
        finally
        {
            sharedSessionManager.SaveAndReleaseContact(sessionContact);
        }
    }
    else
    {
        // The contact is not in an active session on this server.
        // So we'll read it directly from the database:
        Contact databaseContact = contactRepository.LoadContactReadOnly(contactId);

        if (databaseContact != null)
        {
            // Read facet data from databaseContact
        }
        else
        {
            // The contact wasn't found in the Collection database.
            // Handle gracefully.
        }
    }
}
else
{
    // There is no current contact session.
    // Handle gracefully.
}

Note that I am first checking whether or not the contact exists in the Shared Session. If it's not there, I read the contact from the Collection database in a read-only mode.

7
  • 1
    SharedSessionStateManager has been problematic for me in the past. Hit or miss. I'm still trying to understand why it's inconsistent. – Pete Navarra Feb 14 '17 at 15:12
  • @PeteNavarra Could you create a question about that? I will take a look. – Dmytro Shevchenko Feb 14 '17 at 15:20
  • @DmytroShevchenko: Thank you for the thorough response. The only objection I have to your solution is that the API is no longer secure in the sense that if you know the contact id of a contact you can call the service and get the data associated with that contact. By using the session and the build-in tracker/context, I would be sure that only data for the user currently accessing the site is returned. – pwind Feb 15 '17 at 6:26
  • @pwind The Contact ID is a random GUID though. It's impossible to guess. Also, I would argue that if somebody is able to find out your contacts' IDs, that is a bigger security breach in and of itself. – Dmytro Shevchenko Feb 15 '17 at 7:21
  • 1
    @pwind I have updated my answer to apply a better approach. Keep in mind that the user always specifies who they are via the cookie SC_ANALYTICS_GLOBAL_COOKIE where the contact ID is stored. – Dmytro Shevchenko Feb 15 '17 at 8:38
5

For writing contact information, the best practice is to not use API calls and do your work in the shared session within the Page Context on the Content Delivery servers.

For reading contact information, in an API, the Best Practice is passing the contact id through the API and loading a read only contact to gain the information you need.

public class WidgetsController : ApiController
{
    [HttpGet]
    public IEnumerable<Widget> Get(Guid contactId)
    {
        var contactRepository =
            (ContactRepositoryBase) Sitecore.Configuration.Factory.CreateObject("contactRepository", true);
        var contact = contactRepository.LoadContactReadOnly(contactId);

        if (contact != null)
        {
            return WidgetsRepository.Get(contact);
        }
    }
}

If you must use an API for writing information to a contact, you will have to use the FlushToXdb() method to save to Mongo directly or use the SharedSessionStateManager. Be advised though, that the contact record in session on the CD won't be affected if you are trying to perform operations on the CM server.

2
  • Hi Pete. As I wrote to Dmytro in his answer I have the following issues with providing the contact Id: The only objection I have to your solution is that the API is no longer secure in the sense that if you know the contact id of a contact you can call the service and get the data associated with that contact. By using the session and the build-in tracker/context, I would be sure that only data for the user currently accessing the site is returned – pwind Feb 15 '17 at 6:31
  • 1
    Please note that the contact in the Collection database that you'll get via the ContactRepository may not have up-to-date data, as it may have been changed in a currently live session. Which is why you should use the SharedSessionStateManager, as I suggested in my answer. – Dmytro Shevchenko Feb 15 '17 at 7:23
2

Have a look at this solution. You can make an API call session-aware and then use Tracker.Current.Session.**Contact**

 public class MakeWebApiSessionAware
    {
         public void Process(PipelineArgs args)
        {
            var route = RouteTable.Routes[SetupRoutes.ApiRouteSessionAwareKey] as Route;

            if (route != null)
            {
                route.RouteHandler = new SessionRouteHandler();
            }
        }
    }

    public class SessionRouteHandler : IRouteHandler
    {
        IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext)
        {
            return new SessionControllerHandler(requestContext.RouteData);
        }
    }

    public class SessionControllerHandler : HttpControllerHandler, IRequiresSessionState
    {
        public SessionControllerHandler(RouteData routeData)
            : base(routeData)
        {
        }
    }

Pipelines setup:

<sitecore>
    <pipelines>
      <initialize>        
        <processor patch:after="processor[@type='X.Y.Pipelines.SetupRoutes, X.Y.Business']"
              type="X.Y.Business.Pipelines.MakeWebApiSessionAware" />
       </initialize>
    </pipelines>
  </sitecore>

The idea came from here: http://jockstothecore.com/xdb-tracking-the-untrackable-part-1/

0
1

If you don't use a normal controller, then the Context will not be available and thus the tracker wont be available.

To use an API, you would have to pass in the identifier (email address or username) of the user and get the tracker object that way. This will be become an issue because the contact will be on the site, thus the contact record will be locked. You will only be able to read from the object, not write.

public class WidgetsController : Controller {

    [HttpGet]
    public IEnumerable<Widget> GetTrackerInfo()
    {
        if (Sitecore.Analytics.Tracker.Current != null) {
            var contact = Sitecore.Analytics.Tracker.Current.Contact;
            if (contact != null)
            {
                return WidgetsRepository.Get(contact);
            }
        }
    }
}
3
  • 1
    I may be missing something, but isn't your code sample doing exactly the same thing as the code in the OP? – Dmytro Shevchenko Feb 14 '17 at 14:59
  • Its using a Controller, not an API controller. It is very similar, but you can access the context in mine. Use Dmytro's if you want to pass in the contact ID. – Chris Auer Feb 14 '17 at 18:57
  • It's correct, for example code which force xDB session ends: Tracker.Current?.EndTracking(); HttpContext.Current?.Session?.Abandon(); works when called from Controller and doesn't work from ApiController even if session is available from both. – whuu Mar 9 '18 at 8:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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