7

I have a Sitecore 8.1-3 instance. I am trying to write some code that lets admin users edit a user's xDB contact information. I have written a general-purpose method in my code that gets a contact object using the user's email address. The idea behind this method is that it could be used within a regular page request (where we are trying to get the current logged in user's contact) or it might be used on an admin page (where we are trying to get the contact that goes with some other user). If the code can't find a contact with the given identifier then it should create a new contact and return it. What I am noticing however is that when my code tries to save the new contact (in the case where it couldn't find a contact using that email address) it throws an error that says "another contact with the same identifier already exists". I can't figure this out. When I use robomongo to look for a contact with that identifier it returns no results.

private Contact GetXdbContact(string identifier)
{
    if (Tracker.Current != null && Tracker.Current.Contact != null && Tracker.Current.Contact.Identifiers.Identifier == identifier)
    {
        //This is the current user
        return Tracker.Current.Contact;
    }

    //There is no Tracker.Current.Contact, so this must be running in some non-user context
    var manager = Factory.CreateObject("tracking/contactManager", true) as ContactManager;

    var contact = manager.LoadContactReadOnly(identifier);
    if (contact == null)
    {
        //No contact was found so create one
        var repository = Factory.CreateObject("tracking/contactRepository", true) as ContactRepository;
        var leaseOwner = new LeaseOwner("ADMIN", LeaseOwnerType.OutOfRequestWorker);
        var newContact = repository.CreateContact(Guid.NewGuid());
        newContact.Identifiers.Identifier = identifier;
        newContact.Identifiers.IdentificationLevel = ContactIdentificationLevel.Known;
        var options = new ContactSaveOptions(true, leaseOwner);
        repository.SaveContact(newContact, options); //EXCEPTION IS THROWN HERE
        return newContact;
    }
    return contact;
}

In our system, the email address is the identifier. So if I call this with jdoe@company.com as the identifier, the manager does not find any contact, so my code attempts to create a new one. However, when it goes to save the contact it throws the exception below. I can use robomongo to verify that indeed there are not contacts where Identifiers.Identifier == 'jdoe@company.com', so I'm not really sure what is going on here.

Another contact with the same identifier already exists.

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.InvalidOperationException: Another contact with the same identifier already exists.

Source Error:

An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below.

Stack Trace:


[InvalidOperationException: Another contact with the same identifier already exists.]    Sitecore.Analytics.Data.DataAccess.MongoDb.MongoDbDataAdapterProvider.SaveContactWithIdentifier(IContact contact, ContactSaveOptions saveOptions) +1256    Sitecore.Analytics.Data.DataAccess.MongoDb.c__DisplayClass9.b__7()
+25    Sitecore.Analytics.Data.DataAccess.MongoDb.MongoDbDataAdapterProvider.Try(Action action) +173    Sitecore.Analytics.Data.DataAccess.MongoDb.MongoDbDataAdapterProvider.SaveContact(IContact contact, ContactSaveOptions saveOptions) +222    Sitecore.Analytics.Data.ContactRepository.SaveContact(Contact contact, ContactSaveOptions options) +590
  • Have you tried this? briancaos.wordpress.com/2016/09/26/… – Chris Auer Jan 25 '17 at 15:00
  • I have tried implementing the code in the link you provided but it still not working with the same error message. From what I can tell if it doesn't find a contact it then tries to create the contact and save it to xDB and then load it. It tries this 10 times. If that doesn't work then it throws the exception. – Corey Burnett Jan 25 '17 at 19:04
  • I wonder if there is some other way for me to create a contact in xDB. Is it possible in my code to somehow call Sitecore.Analytics.Tracker.Current.Session.Identify and pass in the email address so that it creates the contact? Or perhaps my code could hit a custom ASPX page that I create and pass the email address in the URL so that custom ASPX page could start a new session, identify with the email address, the abandon the session. It seems like a hack but maybe it would work. – Corey Burnett Jan 26 '17 at 15:56
  • I have done it the way you have in 8.1. Give this a go. Dmytro has the best intel on the xDB. sitecore.stackexchange.com/questions/3054/… – Chris Auer Jan 26 '17 at 16:23
3

Finding the contact in the Collection database

The error you're seeing occurs when the given identifier has already been used for another contact.

Contact identifiers are stored in a separate collection named Identifiers. You can verify which contact has (or had) the identifier in question with the following MongoDB query. Make sure to convert the identifier to upper case.

db.Identifiers.find({ _id: "JDOE@COMPANY.COM" })

You'll see an object that looks like this:

{
    "_id" : "JDOE@COMPANY.COM",
    "contact" : NUUID("604bd137-84e6-4996-8cdb-37fec38eb7f5"),
    "updated" : ISODate("2016-02-25T08:44:30.009Z")
}

You can then find what contact the identifier refers to (or used to refer to) using this query:

db.Contacts.find({ _id: CSUUID("604bd137-84e6-4996-8cdb-37fec38eb7f5") })

Note that I have changed NUUID to CSUUID in order for the query to work. Robomongo supports CSUUID in its JSON parser, but not NUUID.

Why wasn't the contact found by this identifier?

When you call manager.LoadContactReadOnly(identifier), Sitecore will first query the Identifiers collection to find the contact ID, much like we did above. It'll then load the contact from the Contacts collection. But there's a catch: it will only return the contact if its current identifier is the same as the one you passed to LoadContactReadOnly. If the current identifier is different, it will return null.

I can think of two reasons why LoadContactReadOnly may return null in your case:

  • The contact has been manually removed from Contacts, but the Identifiers entry stayed;
  • The contact's identifier was changed, but the Identifiers entry was not updated. As a matter of fact, this may be a defect in Sitecore—I don't see any logic that would update Identifiers when the contact's identifier changes.

Hot to approach this problem?

First of all, register this as a defect with Sitecore Support. Changing a contact's identifier should not lead to a bug like this. Feel free to point out my findings to the support team, and maybe they'll provide you with a quick fix.

In the meanwhile, the safest option would be not to change identifiers for existing contacts.

But if you must, then, whenever you save a contact with a changed identifier, you'll need to remove the old identifier from the Identifiers collection:

var driver = MongoDbDriver.FromConnectionString("analytics");

string id = "jdoe@company.com";

driver["Identifiers"].Remove(
    Query.EQ("_id", (BsonValue) id.ToUpperInvariant()),
    RemoveFlags.None,
    WriteConcern.Acknowledged);
  • Thanks. That was the problem. When I queried the Identifiers collection I found an entry for jdoe@company.com even though there is no contact that is using that email address as an identifier. So this does seem like a bug. You mentioned in your answer that the safest option is not to change identifiers for existing contacts. I would think that a very popular identifier for a contact would be an email address. And people can change their email address in real life. So this seems like a drawback of the architecture if you shouldn't change your email address. – Corey Burnett Jan 27 '17 at 18:06
  • @CoreyBurnett I agree. But still, try to contact Sitecore support about this. Maybe they have a fix for this. – Dmytro Shevchenko Jan 27 '17 at 18:09
  • One more comment - I can't seem to find the MongoDbIdentifierStorage class. Where is that? – Corey Burnett Jan 27 '17 at 18:25
  • @CoreyBurnett It's in Sitecore.Analytics.Data.DataAccess.MongoDb. – Dmytro Shevchenko Jan 27 '17 at 18:37
  • Hmmm...my version of the Sitecore.Analytics.MongoDB.dll does not have that. I am using version 8.1.0.5526. – Corey Burnett Jan 27 '17 at 18:47
2

I had the same error while importing users via xConnect to the xDb.

It occurs when there is a contact with a reference to a successor and also still has a reference to the Identifiers collection. So at that moment there are two references to the same Identifier object.

I've used this query to lookup how many we had:

db.getCollection('Contacts').find({Successor:{$exists:true},Successor:{$exists:true}, Identifiers:{$exists:true}})

and then something like this to clean it up

db.getCollection('Contacts').update({ _id:<LUUID>},{ $unset: { Identifiers: ""} })

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.