Below is what I believe the proper way of doing this. Note that, since you're not in the context of a live session, some Sitecore mechanisms (such as cross-cluster session migration) aren't available to you. Hence, you must guard against edge cases, particularly when the contact with the identifier already exists and is currently locked by another process.
Since your new contact doesn't exist yet, there's no need for an actual merge. You just need to get the existing contact and update its fields in place.
public void EnsureUser(string email)
{
LeaseOwner leaseOwner = new LeaseOwner("YOUR_WORKER_NAME", LeaseOwnerType.OutOfRequestWorker);
ContactRepositoryBase contactRepository = Factory.CreateObject("contactRepository", true) as ContactRepositoryBase;
// Attempt to obtain an exclusive lock on an existing contact in xDB.
LockAttemptResult<Contact> lockResult = contactRepository.TryLoadContact(email, leaseOwner, TimeSpan.FromMinutes(1));
Contact contact = null;
if (lockResult.Status == LockAttemptStatus.AlreadyLocked)
{
// Another worker or a live web session has an exclusive lock on the contact.
// You can't use this contact right now. It's up to you what to do in this case.
/* ... */
}
else if (lockResult.Status == LockAttemptStatus.DatabaseUnavailable)
{
// Database is down. Try to handle this gracefully.
/* ... */
}
else if (lockResult.Status == LockAttemptStatus.NotFound)
{
// A contact with the given identifier doesn't exist.
// Just create a new contact object.
contact = contactRepository.CreateContact(Guid.NewGuid());
// Identify it.
contact.Identifiers.Identifier = email;
// And make it known.
contact.Identifiers.IdentificationLevel = Sitecore.Analytics.Model.ContactIdentificationLevel.Known;
}
else
{
// We successfull locked an existing contact.
contact = lockResult.Object;
}
// Set some other data on the contact:
/* ... */
// Save the contact and release the lock.
if (contact != null)
{
var options = new ContactSaveOptions(release: true, owner: leaseOwner);
contactRepository.SaveContact(contact, options);
}
}