OrganizationServiceContext: System.InvalidOperationException: The context is already tracking the 'contact' entity - dynamics-crm-2011

I'm trying to create a plugin that changes all related contacts' address fields if the parent account's address field is changed in account form. I created a plugin to run in pre operation stage (update message against account entity) synchronously.
I used LINQ query to retrieve all related contacts and it works. Then I'm using foreach loop to loop trough all contacts and change them address fields. I'm using OrganizationServiceContext.AddObject(); function to add every contact to the tracking pipeline (or whatever it's called) and finally I'm using OrganizationServiceContext.SaveChanges(); to trying to save all contacts. But that's when I'm getting this error:
System.InvalidOperationException: The context is already tracking the 'contact' entity.
Here's my code
// Updating child contacts' fields
if (context.PreEntityImages.Contains("preAccount") && context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity) {
if (((Entity)context.InputParameters["Target"]).Contains("address2_name")) {
Entity account = (Entity)context.InputParameters["Target"];
Entity preAccount = (Entity)context.PreEntityImages["preAccount"];
if (account["address2_name"] != preAccount["address2_name"]) {
EntityReference parentCustomer = new EntityReference(account.LogicalName, account.Id);
Contact[] childContacts = orgService.ContactSet.Where(id => id.ParentCustomerId == parentCustomer).ToArray();
foreach (Contact contact in childContacts) {
contact.Address2_Name = (string)account["address2_name"];
orgService.AddObject(contact);
}
orgService.SaveChanges();
}
}
}
What I'm doing wrong?

You already attached the entities to the context when you retrieved the contacts with the query
Contact[] childContacts = orgService.ContactSet.Where(id => id.ParentCustomerId == parentCustomer).ToArray();
so you don't need to add again the entities to the context, instead you need to update them, by:
orgService.UpdateObject(contact); // this row instead of orgService.AddObject(contact);

Related

Acumatica GetList error: Optimization cannot be performed.The following fields cause the error: Attributes.AttributeID

Developer's version of Acumatica 2020R1 is installed locally. Data for sample tenant MyTenant from training for I-300 were loaded, and WSDL connection established.
DefaultSoapClient is created fine.
However, attempts to export any data by using Getlist cause errors:
using (Default.DefaultSoapClient soapClient =
new Default.DefaultSoapClient())
{
//Sign in to Acumatica ERP
soapClient.Login
(
"Admin",
"*",
"MyTenant",
"Yogifon",
null
);
try
{
//Retrieving the list of customers with contacts
//InitialDataRetrieval.RetrieveListOfCustomers(soapClient);
//Retrieving the list of stock items modified within the past day
// RetrievalOfDelta.ExportStockItems(soapClient);
RetrievalOfDelta.ExportItemClass(soapClient);
}
public static void ExportItemClass(DefaultSoapClient soapClient)
{
Console.WriteLine("Retrieving the list of item classes...");
ItemClass ItemClassToBeFound = new ItemClass
{
ReturnBehavior = ReturnBehavior.All,
};
Entity[] ItemClasses = soapClient.GetList(ItemClassToBeFound);
string lcItemType = "", lcValuationMethod = "";
int lnCustomFieldsCount;
using (StreamWriter file = new StreamWriter("ItemClass.csv"))
{
//Write the values for each item
foreach (ItemClass loItemClass in ItemClasses)
{
file.WriteLine(loItemClass.Note);
}
}
The Acumatica instance was modified by adding a custom field to Stock Items using DAC, and by adding several Attributes to Customer and Stock Items.
Interesting enough, this code used to work until something broke it.
What is wrong here?
Thank you.
Alexander
In the request you have the following line: ReturnBehavior = ReturnBehavior.All
That means that you try to retrieve all linked/detail entities of the object. Unfortunately, some object are not optimized enough to not affect query performance in GetList scenarios.
So, you have to options:
Replace ReturnBehavior=All by explicitly specifying linked/detail entities that you want to retrieve and not include Attributes into the list.
Retrieve StockItem with attributes one by one using Get operation instead of GetList.
P.S. The problem with attributes will most likely be fixed in the next version of API endpoint.
Edit:
Code sample for Get:
public static void ExportItemClass(DefaultSoapClient soapClient)
{
Console.WriteLine("Retrieving the list of item classes...");
ItemClass ItemClassToBeFound = new ItemClass
{
ReturnBehavior = ReturnBehavior.Default //retrieve only default fields (without attributes and other linked/detailed entities)
};
Entity[] ItemClasses = soapClient.GetList(ItemClassToBeFound);
foreach(var entity in ItemClasses)
{
ItemClass itemClass= entity as ItemClass;
ItemClass.ReturnBehavior=ReturnBehavior.All;
// retrieve each ItemClass with all the details/linked entities individually
ItemClass retrievedItemCLass = soapClient.Get(itemClass);
}

null ContentItem in Orchard

First - disclaimer: I know I should not do it like this, but use LazyField instead and that model should not contain logic and I will modify my code accordingly, but I wanted to explore the 1-n relationship between content items and orchard in general.
I'm creating a system where user can respond to selected job offer, so I have two content types - Job, which lists all available jobs, and JobAnswer which contains my custom part and provides link to appropriate Job content item:
public class JobPart : ContentPart<JobPartRecord>
{
public ContentItem Job
{
get
{
if (Record.ContentItemRecord != null)
{
var contentItem = ContentItem.ContentManager.Get(Record.Job.Id);
return contentItem;
}
var nullItem = ContentItem.ContentManager.Query("Job").List().First();
return nullItem;
}
set { Record.Job = value.Record; }
}
}
This works, but I'm not sure how should I handle returning a null contentItem, when creating new content item, now it just returns first Job content item, which is far from ideal.

Sitecore Droplink for User Roles

I'm building a custom workflow where all users that are members of a specific role will receive email notifications depending on certain state changes. I've begun fleshing out e-mail templates via Sitecore items with replaceable tokens, but I'm struggling to find a way to allow the setting of the recipient role in Sitecore. I'd like to avoid having users enter a string representation of the role, so a droplink would be ideal if there were a way to populate it with the various roles defined in sitecore. Bonus points if I can filter the roles that populate the droplink.
I'm aware that users/roles/domains aren't defined as items in the content tree, so how exactly does one go about configuring this droplink?
Sitecore 6.5.
I'm not sure if there is a module for this already made, but you can use this technique: http://newguid.net/sitecore/2013/coded-field-datasources-in-sitecore/
It explains how you can use a class as data source. So you could create a class that lists all user roles.
You might want to take a look at http://sitecorejunkie.com/2012/12/28/have-a-field-day-with-custom-sitecore-fields/ which presents a multilist to allow you to select a list of users.
Also take a look at the Workflow Escaltor Module form which you can borrow the AccountSelector control which allows you to select either individual person or roles.
This is the module I previously used to do this exact thing. The following code gets all the unique email addresses of users and only for those users that have read access to the item (it was a multisite implementation, the roles were restricted to each site but the workflow was shared).
protected override List<string> GetRecipientList(WorkflowPipelineArgs args, Item workflowItem)
{
Field recipientsField = workflowItem.Fields["To"];
Error.Assert((recipientsField != null || !string.IsNullOrEmpty(recipientsField.Value)), "The 'To' field is not specified in the mail action item: " + workflowItem.Paths.FullPath);
List<string> recepients = GetEmailsForUsersAndRoles(recipientsField, args);
if (recepients.Count == 0)
Log.Info("There are no users with valid email addresses to notify for item submission: " + workflowItem.Paths.FullPath);
return recepients;
}
//Returns unique email addresses of users that correspond to the selected list of users/roles
private List<string> GetEmailsForUsersAndRoles(Field field, WorkflowPipelineArgs args)
{
List<string> emails = new List<string>();
List<User> allUsers = new List<User>();
AccountSelectorField accountSelectorField = new AccountSelectorField(field);
List<Account> selectedRoles = accountSelectorField.GetSelectedAccountsByType(AccountType.Role);
List<Account> selectedUsers = accountSelectorField.GetSelectedAccountsByType(AccountType.User);
foreach (var role in selectedRoles)
{
var users = RolesInRolesManager.GetUsersInRole(Role.FromName(role.Name), true).ToList();
if (users.Any())
allUsers.AddRange(users);
}
selectedUsers.ForEach(i => allUsers.Add(Sitecore.Security.Accounts.User.FromName(i.Name, false)));
foreach (var user in allUsers)
{
if (user == null || !args.DataItem.Security.CanRead(user)) continue; //move on if user does not have access to item
if (!emails.Contains(user.Profile.Email.ToLower()))
{
if(user.Profile.Email != null && !string.IsNullOrEmpty(user.Profile.Email.Trim()))
emails.Add(user.Profile.Email.ToLower());
else
Log.Error("No email address setup for user: " + user.Name);
}
}
return emails;
}

How can I update a content item (draft) from a background task in Orchard?

I have a simple IBackgroundTask implementation that performs a query and then either performs an insert or one or more updates depending on whether a specific item exists or not. However, the updates are not persisted, and I don't understand why. New items are created just as expected.
The content item I'm updating has a CommonPart and I've tried authenticating as a valid user. I've also tried flushing the content manager at the end of the Sweep method. What am I missing?
This is my Sweep, slightly edited for brevity:
public void Sweep()
{
// Authenticate as the site's super user
var superUser = _membershipService.GetUser(_orchardServices.WorkContext.CurrentSite.SuperUser);
_authenticationService.SetAuthenticatedUserForRequest(superUser);
// Create a dummy "Person" content item
var item = _contentManager.New("Person");
var person = item.As<PersonPart>();
if (person == null)
{
return;
}
person.ExternalId = Random.Next(1, 10).ToString();
person.FirstName = GenerateFirstName();
person.LastName = GenerateLastName();
// Check if the person already exists
var matchingPersons = _contentManager
.Query<PersonPart, PersonRecord>(VersionOptions.AllVersions)
.Where(record => record.ExternalId == person.ExternalId)
.List().ToArray();
if (!matchingPersons.Any())
{
// Insert new person and quit
_contentManager.Create(item, VersionOptions.Draft);
return;
}
// There are at least one matching person, update it
foreach (var updatedPerson in matchingPersons)
{
updatedPerson.FirstName = person.FirstName;
updatedPerson.LastName = person.LastName;
}
_contentManager.Flush();
}
Try to add _contentManager.Publish(updatedPerson). If you do not want to publish, but just to save, you don't need to do anything more, as changes in Orchard as saved automatically unless the ambient transaction is aborted. The call to Flush is not necessary at all. This is the case both during a regular request and on a background task.

Update child entity / Attach entity to context

I am creating a pre event plugin for CRM 2011 that sets the account owner and updates all the child contacts with the same owner. The plugin is installed correctly and updates the main account record correctly but the child contact owner does not change . I have pushed the owner name into another field of the contact to check I have the correct details and that field is updating.
I'm sure its something to do with attaching the child contacts to the correct context but so far I have drawn a blank.
//Set new account owner - Works fine
account.OwnerId = new EntityReference(SystemUser.EntityLogicalName, ownerId);
//Pass the same owner into the contacts - Does not get updated
UpdateContacts(account.Id, ownerId, service, tracingService);
The system is successfully updating the account owner and the description label of the child record.
public static void UpdateContacts(Guid parentCustomerId, Guid ownerId, IOrganizationService service, ITracingService tracingService)
{
// Create the FilterExpression.
FilterExpression filter = new FilterExpression();
// Set the properties of the filter.
filter.FilterOperator = LogicalOperator.And;
filter.AddCondition(new ConditionExpression("parentcustomerid", ConditionOperator.Equal, parentCustomerId));
// Create the QueryExpression object.
QueryExpression query = new QueryExpression();
// Set the properties of the QueryExpression object.
query.EntityName = Contact.EntityLogicalName;
query.ColumnSet = new ColumnSet(true);
query.Criteria = filter;
// Retrieve the contacts.
EntityCollection results = service.RetrieveMultiple(query);
tracingService.Trace("Results : " + results.Entities.Count);
SystemUser systemUser = (SystemUser)service.Retrieve(SystemUser.EntityLogicalName, ownerId, new ColumnSet(true));
tracingService.Trace("System User : " + systemUser.FullName);
XrmServiceContext xrmServiceContext = new XrmServiceContext(service);
for (int i = 0; i < results.Entities.Count; i++)
{
Contact contact = (Contact)results.Entities[i];
contact.OwnerId = new EntityReference(SystemUser.EntityLogicalName, systemUser.Id);
contact.Description = systemUser.FullName;
xrmServiceContext.Attach(contact);
xrmServiceContext.UpdateObject(contact);
xrmServiceContext.SaveChanges();
tracingService.Trace("Updating : " + contact.FullName);
}
}
The tracing service prints out everything I would expect. Do I need to also attach the system user and somehow attach the entity reference to the context?
Any help appreciated.
You have to make a separate web service call using AssignRequest to change the ownership of a record. Unfortunately you cannot just change the Owner attribute.
I think I was getting myself into all sorts of mess with this plugin as by default changing the account owner automatically changes the associated contacts owner. I was therefore trying to overwrite something that it was already doing.
By using the AssignRequest to set the account owner rather than the child records it worked fine. Credit given to Chris as he pointed me in the right direction.
All that was needed was to change the first line of my code to use AssignRequest and the entire UpdateContacts method became obselete.

Resources