I'm creating a Dynamics CRM workflow assembly to be executed when a new Note is created on another record of any type. I need to be able to access a property Prop1 on that newly created Note entity to accomplish other tasks.
Previously I've only accessed values that were input from a field or from the user, but never on a property of a newly created entity. Any guidance would be appreciated.
UPDATE:
This is regarding CRM 4.0.
More information while I wait:
Ultimately, this workflow assembly will create an email that contains a link to the parent entity of the newly created Note record. The property I need to get is the AnnotationId. Once the Note record is created, I will be retrieving the ObjectId and ObjectTypeCode based on the AnnotationId of the newly created Note.
(In case you were curious)
Ok so if your using 4.0 custom workflows and not 3.0 callouts, you should add a workflow assembly, and use the Context service and executing context of your workflow to pull the values from the new note.
See the example below on how to access a record using the context service and the ID of your current context of execution (that should be your note)
/// <summary>
/// The Execute method is called by the workflow runtime to execute an activity.
/// </summary>
/// <param name="executionContext"> The context for the activity</param>
/// <returns></returns>
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
// Get the context service.
IContextService contextService = (IContextService)executionContext.GetService(typeof(IContextService));
IWorkflowContext context = contextService.Context;
// Use the context service to create an instance of CrmService.
ICrmService crmService = context.CreateCrmService(true);
BusinessEntity newNote = GetNote(crmService, context.PrimaryEntityId);
string noteAttrib;
noteAttrib = newNote.Properties.Contains("AnnotationId") ? ((Lookup)newNote.Properties["annotationid"]).name.ToString() : null;
return ActivityExecutionStatus.Closed;
}
GetNotes method would be a standard query for notes by Id through a CRM service call,
here is an example slightly modified from MSDN to return a note:
private BusinessEntity getNote(ICrmService service, guid noteid)
{
// Create the column set object that indicates the fields to be retrieved.
ColumnSet cols = new ColumnSet();
// Set the columns to retrieve, you can use allColumns but its good practice to specify:
cols.Attributes = new string [] {"name"};
// Create the target object for the request.
TargetRetrieveAnnotation target = new TargetRetrieveAnnotation();
// Set the properties of the target object.
// EntityId is the GUID of the record being retrieved.
target.EntityId = noteid;
// Create the request object.
RetrieveRequest retrieve = new RetrieveRequest();
// Set the properties of the request object.
retrieve.Target = target;
retrieve.ColumnSet = cols;
// Execute the request.
RetrieveResponse retrieved = (RetrieveResponse)service.Execute(retrieve);
return RetrieveResponse;
}
Related
Hi I'm new to Dynamics and plugins in dynamics. I have created a simple Entity called Library that holds books.
After a new book is created I want the price of the book to increment by a GST of 10% on the server side via a plugin.
I know this would normally occur on the page before saving by I'm trying to work out how server side logic works.
I have created a postOperation (synchronous) step for the "Create" message to call the Plugin Execute() method. From my reading this should occur AFTER the record is saved in the database.
I also have a post image entity that I access.
In the Execute method I try to access the saved record via the PostMessageEntity to update the price, but I get an exception saying the record does not exist based on the record identifier that i have obtained. I can confirm the record was never created in the system, yet the postOperation has been called.
How do I access the just saved record in the plugin so that I can update the Price?
My code:
public void Execute(IServiceProvider serviceProvider)
{
// Obtain the execution context from the service provider.
Microsoft.Xrm.Sdk.IPluginExecutionContext context = (Microsoft.Xrm.Sdk.IPluginExecutionContext)
serviceProvider.GetService(typeof(Microsoft.Xrm.Sdk.IPluginExecutionContext));
// create a trace log so you can see where in the code it breaks
ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
// create access to service
IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
tracingService.Trace("have reached execute event in plugin.");
// The InputParameters collection contains all the data passed in the message request.
if (context.InputParameters.Contains("Target") &&
context.InputParameters["Target"] is Entity)
{
tracingService.Trace("We have a target and it is an entity.");
// Obtain the target entity from the input parameters.
Entity entity = (Entity)context.InputParameters["Target"];
if (entity.LogicalName == "new_books")
{
tracingService.Trace("the entity id of the record that was created is .." + entity.Attributes["new_booksid"].ToString());
// do we have a post update image of the new_books entity
if (context.PostEntityImages.Contains("newbookpostImage") && context.PostEntityImages["newbookpostImage"] is Entity)
{
tracingService.Trace("we have a postEntityImage.");
// // yep lets grab it.
Entity postMessageEntity = (Entity)context.PostEntityImages["newbookpostImage"];
// get book price as just saved to db
decimal bookPrice = ((Money)postMessageEntity.Attributes["new_price"]).Value;
// get id of the the record we have
Guid RecordID = ((Guid)postMessageEntity.Attributes["new_booksid"]);
tracingService.Trace("we have a post update bookprice.");
tracingService.Trace("the entity id of the post image entity is ..." + postMessageEntity.Attributes["new_booksid"].ToString());
Entity created_book = new Entity("new_books");
// use service to access a field of the current record as it is in the database and column we want to update.
created_book = service.Retrieve(created_book.LogicalName, RecordID, new ColumnSet(true));
//And the last line is where it dies and tells me new_books with id with d7bfc9e2 - 2257 - ec11 - 8f8f - 00224814e6e0 does not exist.
}
}
}
}
Entity postMessageEntity = (Entity)context.PostEntityImages["newbookpostImage"];
Is your PostEntityImage new_books entity?
Also if you have an entity postMessageEntity you can directly get Entity Record ID by
postMessageEntity.ID
rather than Guid RecordID = ((Guid)postMessageEntity.Attributes["new_booksid"]);
Here your code does nothing more than create empty object of type Entity new_books.
You have not set Priamry name field of entiy or any other. Also you have not created a record, you should use
Entity created_book = new Entity("new_books")
service.Create(created_book);
Below you are trying to fecth Record from Entity new_books based on postMessageEntity.Id
You should check postMessageEntity logical name is same as created_book.LogicalName and then use postMessageEntity.ID rather than RecordID
created_book = service.Retrieve(created_book.LogicalName, RecordID, new ColumnSet(true));
In the plugin pipeline you can actually add, modify and even remove attributes in the entity object on the fly. This must be done before the main operations take place: in the prevalidation or in the preoperation stage.
So, your code can be simplified like this:
public void Execute(IServiceProvider serviceProvider)
{
var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
Debug.Assert(context.Stage <= 20); // This only works in prevalidation and preoperation stages.
var book = (Entity)context.InputParameters["Target"]; // For message Create a Target parameter is always available.
// Using GetAttributeValue is more safe, because when price is not set, the attribute will not be available in the collection.
decimal? price = book.GetAttributeValue<Money>("new_price")?.Value;
if (price.HasValue)
book["new_price"] = new Money(price.Value * 1.1M);
}
In the synchronous post create stage you are still in a database transaction. At that point the record is created, but not yet committed.
The ID of the record created can be found in the OutputParameters collection. You can pick it up like this:
var recordId = (Guid)context.OutputParameters["id"];
There is no need to do checks on the context object. When your plugin is registered properly, all items you would expect to be available will be there. If not, a proper exception log will be your best friend. Just add a generic exception handler responsible for writing the error context to the plugin trace log.
I'm currently using Quartz .NET version 1.0.3 in a project. For some reason I can't upgrade it to the newest version.
These are the methods that I use:
ScheduleUnreserveCarJob
Trigger trigger = CreateTrigger(typeof(UnreserveCarJob), lotId.ToString(), startTimeUtc);
trigger.JobDataMap.PutAsString(JobDataMapKeys.LotId, lotId);
scheduler.ScheduleJob(trigger);
UnreserveCarJob
[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
protected override void ExecuteJob(JobExecutionContext context)
{
using (IAuctionProcessClient auctionProcessClient = new AuctionServiceClient())
{
var request = LotGetRequest(context.MergedJobDataMap);
auctionProcessClient.LotGet(request);
}
}
private static LotGetRequest LotGetRequest(JobDataMap jobDataMap)
{
return new LotGetRequest { Lot = jobDataMap.GetInt(JobDataMapKeys.LotId) };
}
CreateTrigger
var jobTypeName = jobType.Name;
return new SimpleTrigger
{
Group = jobTypeName,
JobName = jobTypeName,
MisfireInstruction = MisfireInstruction.SimpleTrigger.FireNow,
Name = name,
StartTimeUtc = startTimeUtc
};
When I want to execute ScheduleJob from ScheduleUnreserveCarJob, I always get the error The job DEFAULT.UnreserveCarJob referenced by the trigger does not exist.
The triggers are saved in the DB, so in the QRTZ_JOB_LISTENERS and QRTZ_JOB_DETAILS I have a row with JOB_NAME UnreserveCarJob and JOB_GROUP DEFAULT.
What could be the reason of this problem?
EDIT:
I've insert the details for the QRTZ_JOB tables manually, so the trigger can go to the QRTZ_TRIGGERS table (did that because of the FKs). My guess is that the Quartz.dll is not inserting any rows in the DB. I still don't know why.
EDIT 2:
I've tracked the issue with SQL Server Profiler and the problem is at scheduler.ScheduleJob(trigger);. When running this line, an INSERT must be made on QRTZ_TRIGGERS and QRTZ_SIMPLE_TRIGGERS. Unfortunately, I get The job referenced by the trigger does not exist. For some reason, quartz.dll is not inserting in the tables.
When a trigger is fired, the Quartz.NET finds a JobDetail instance by its identity specified by the trigger. The identity is a pair value of job name and job group. If no instance is found, the error The job DEFAULT.UnreserveCarJob referenced by the trigger does not exist will be returned. There are two possible cases: no job detail is created yet or the job detail is created with the wrong job name or job group. So, your case sounds like the first one according to your EDIT notes.
If you want to schedule a new trigger for a new job, the correct API should be scheduler.ScheduleJob(jobDetail, trigger); and jobDetail is the instance of IJobDetail class that can be created through JobBuilder. Note: the identity of the job detail must be identical to the one you specify in the trigger.
Side note: the source code of CreateTrigger might be wrong because it sets job group by job name.
var jobTypeName = jobType.Name;
return new SimpleTrigger
{
Group = jobTypeName, <-- WRONG
JobName = jobTypeName,
In CRM when emails arrive and have the tracking token in them they automatically set the regarding field to be the incident (or whatever they relate to)
Unfortunately the Record wall isn't updated with this info so even if you are following the case nothing alerts you to the new activity.
I want to write a plugin on email or incident (or both) that updates the record wall and creates a task to follow up on that email with in 3 days.
I'm looking at the SDK and I can't see what the appropriate event in the pipe line would be to work out when an email is/has its regarding field set on arrival in the CRM.
The CRM email creation life-cycle is not well described in the documentation. [shakes fist]
Extra things that are bothering me
I can't seem to include a reference to get a strongly typed Email, Post or Case (driving me crazy)
Testing this is really hard (harder than it should be)
EDIT Here is my current code
namespace Assembly.Plugins
{
using System;
using System.ServiceModel;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
/// <summary>
/// PostEmailDeliverIncoming Plugin.
/// </summary>
public class PostEmailDeliverIncoming : Plugin
{
/// <summary>
/// Initializes a new instance of the <see cref="PostEmailDeliverIncoming"/> class.
/// </summary>
public PostEmailDeliverIncoming()
: base(typeof(PostEmailDeliverIncoming))
{
RegisteredEvents.Add(new Tuple<int, string, string, Action<LocalPluginContext>>(40, "DeliverIncoming", "email", ExecutePostEmailDeliverIncoming));
// Note : you can register for more events here if this plugin is not specific to an individual entity and message combination.
// You may also need to update your RegisterFile.crmregister plug-in registration file to reflect any change.
}
protected void ExecutePostEmailDeliverIncoming(LocalPluginContext localContext)
{
if (localContext == null)
{
throw new ArgumentNullException("localContext");
}
//Extract the tracing service for use in debugging sandboxed plug-ins.
ITracingService tracingService = localContext.TracingService;
// Obtain the execution context from the service provider.
IPluginExecutionContext context = localContext.PluginExecutionContext;
// Obtain the organization service reference.
var service = localContext.OrganizationService;
// The InputParameters collection contains all the data passed in the message request.
if (!context.InputParameters.Contains("Target") || !(context.InputParameters["Target"] is Entity))
return;
// Obtain the target entity from the input parmameters.
var target = (Entity)context.InputParameters["Target"];
// Verify that the target entity represents an account.
// If not, this plug-in was not registered correctly.
if (target.LogicalName != "email")
return;
if((string)target["direction"] != "Incoming")
return;
if (target["regardingobjectid"] == null)
return;
try
{
// if its not a case I don't care
var incident = service.Retrieve("incident", (Guid)target["regardingobjectid"], new ColumnSet(true));
if (incident == null)
return;
var post = new Entity("post");
post["regardingobjectid"] = target["regardingobjectid"];
post["source"]=new OptionSetValue(0);
post["text"] = String.Format("a new email has arrived.");
// Create the task in Microsoft Dynamics CRM.
tracingService.Trace("FollowupPlugin: Creating the post.");
service.Create(post);
// Create a task activity to follow up with the account customer in 7 days.
var followup = new Entity("task");
followup["subject"] = "Follow up incoming email.";
followup["description"] = "An email arrived that was assigned to a case please follow it up.";
followup["scheduledstart"] = DateTime.Now.AddDays(3);
followup["scheduledend"] = DateTime.Now.AddDays(3);
followup["category"] = context.PrimaryEntityName;
// Refer to the email in the task activity.
if (context.OutputParameters.Contains("id"))
{
var regardingobjectid = new Guid(context.OutputParameters["id"].ToString());
followup["regardingobjectid"] = new EntityReference("email", regardingobjectid);
}
// Create the task in Microsoft Dynamics CRM.
tracingService.Trace("FollowupPlugin: Creating the task activity.");
service.Create(followup);
}
catch (FaultException<OrganizationServiceFault> ex)
{
throw new InvalidPluginExecutionException("An error occurred in the FollupupPlugin plug-in.", ex);
}
catch (Exception ex)
{
tracingService.Trace("FollowupPlugin: {0}", ex.ToString());
throw;
}
}
}
}
I've just been fighting with this exact same issue and came across this post. I thought I'd post the solution for you (if you still need it) and anyone else who comes across the issue in the future.
Here's the solution I arrived at:
- Using the Plugin Registration Tool register a New Image on the appropriate step( Stage = "40", MessageName = "DeliverIncoming")
- Set the New Image to be a Post Image
- In your plugin fetch the Post Image's entity ID:
Guid emailID = context.PostEntityImages["PostImage"].Id;
Entity emailFromRetrieve = localContext.OrganizationService.Retrieve(
"email",
emailID,
new Microsoft.Xrm.Sdk.Query.ColumnSet(true));
Email email = emailFromRetrieve.ToEntity<Email>();
if (email.RegardingObjectId == null)
{
return;
}
var regardingObject = email.RegardingObjectId;
Hope this helps!
I'm actually working on a very similar plugin at the moment. Mine creates a custom entity upon arrival of an email addressed to a certain email address. It also associates the incoming email with that new record via the Regarding field. I've added a Pre-Operation step on Create of Email and it works great, including incoming email from the router.
What I'm not sure of is when CRM fills in the Regarding field. You might look at Post-Operation and see if it is set there?
One interesting caveat regarding the Regarding field (haha!): Unlike single lookup fields, the Regarding object's name is actually stored in the ActivityPointer table, so when you update the Regarding field, be sure to set the Name on the EntityReference. If you don't, the Regarding lookup will still have a clickable icon but there won't be any text. I do it like this:
email.RegardingObjectId = [yourentity].ToEntityReference();
email.RegardingObjectId.Name = email.Subject;
Hope that helps!
I ended up doing this in a workflow on the email entity
Steps
Create new workflow, I called it 'incoming email workflow'
Scope is Organisation
Choose Email as the entity and check 'Record field changes'
Add a step that checks Regarding (Case):Case Contains Data
if true:
Add a step that creates a Post
Edit the properties in the Post
Text : This case has had {Direction(E-mail)} email activity from {From(E-mail)}
Source : Auto Post
Regarding : {Regarding(E-mail)}
Add a step that creates a Task
Edit the properties in the Task
Subject : Follow up {Subject(E-mail)}
Regarding : {Regarding(E-mail)}
Try to use the following code:
if ((bool)entity["directioncode"] == false)
Instead of your code:
if((string)target["direction"] != "Incoming")
I'm trying to associate related records of an entity to a newly created one. The pluggin triggers on creation and pre-operation.
The error occurs when trying to associate the collection to the new entity : "new_ligneContrat With Id = ad630ba6-684e-e111-92e3-00155d151905 Does Not Exist"
Here is my code :
public void Execute(IServiceProvider serviceProvider)
{
// Instanciation des services
IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
IOrganizationService service = factory.CreateOrganizationService(null);
Entity target = (Entity)context.InputParameters["Target"];
EntityReference contrats = (EntityReference)target.Attributes["new_contratsid"];
FetchExpression fetch = new FetchExpression(#"
<fetch distinct='false' mapping='logical'>
<entity name='" + context.PrimaryEntityName + "'><link-entity name='new_contrats' alias='nombreligne' from='new_contratsid' to='new_contratsid'><filter type='and'><condition attribute='new_contratsid' value='" + contrats.Id + "' operator='eq'></condition></filter></link-entity></entity></fetch>");
EntityCollection lines = service.RetrieveMultiple(fetch);
// Vérification qu'il y a au moins une ligne de contrat associée
if (lines.Entities.Any())
{
var first = lines.Entities.Last();
if (first.GetAttributeValue<OptionSetValue>("statecode").Value == 1)
{
FetchExpression query = new FetchExpression(#"
<fetch distinct='false' mapping='logical'>
<entity name='incident'><filter type='and'><condition attribute='new_lignecontrat' value='"+first.Id+"' operator='eq'/></filter></entity></fetch>");
EntityCollection incident = service.RetrieveMultiple(query);
if (incident.Entities.Any())
{
foreach (var e in incident.Entities)
{
e.Attributes["new_lignecontrat"] = new EntityReference (target.LogicalName, target.Id);
}
}
}
}
What is wrong???
Thanks in advance!!
Edit 1: ok seems logical since the record does not exist yet ><. Just one thing : How can I change the value of a lookup field? What is its type?
Edit 2: I've got no error when executing my code, but fields of the incident entity do not update ><'....I've tested my code with invalidPluginExceptions and the end of the code is reached...Here is the code :
Edit 3 : Code updated...
To answer your original question and its edit, yeah, you can't associate the record with another record when the core database operation hasn't been done yet.
Pre-operation: Stage in the pipeline for plug-ins that are to execute before the main
system operation. Plug-ins registered in this stage are executed
within the database transaction.
So to handle association, you can either change the stage to post-operation, or have one IPlugin class handle the pre-operation stage and another handle the post-operation stage, either in one or multiple projects.
To answer the edit, lookup fields are of the class EntityReference. (Looks like you're working with a 1:N relationship?)
To answer the second edit, I don't see anywhere in your code snippet where you assign the new EntityReference to your target Entity. Moreover, you don't have to issue an Update request to the service in the pre-operation stage because the core database operation has not been performed yet. You can just set the attribute of the Entity to be equal to the the value of your choice, and this change will be carried over to the database.
if (entity.Attributes.ContainsKey("new_lignecontrat"))
{
entity.Attributes["new_lignecontrat"] = YourEntityReference;
}
else //attribute not included in the plugin operation
{
entity.Attributes.Add("new_lignecontrat", YourEntityReference);
}
Microsoft has a demonstration of this concept in the SDK: \sdk\samplecode\cs\plug-ins\accountnumberplugin.cs
I have 3 related tables in my database.
Farm ----> FarmCrops <----- Crops
I'm trying to update the a farm entity with a collection of crops but am running into problems. I've been working on this with no success for hours now so any help would be greatly appreciated.
The error I'm receiving is this:
The object cannot be attached because
it is already in the object context.
An object can only be reattached when
it is in an unchanged state.
My update logic is as follows (my apologies for the large amount of code. I'd just like to be as clear as possible):
bool isNew = false;
Farm farm;
// Insert or update logic.
if (viewModel.Farm.FarmId.Equals(Guid.Empty))
{
farm = new Farm
{
FarmId = Guid.NewGuid(),
RatingSum = 3,
RatingVotes = 1
};
isNew = true;
}
else
{
farm = this.ReadWriteSession
.Single<Farm>(x => x.FarmId == viewModel.Farm.FarmId);
}
// Edit/Add the properties.
farm.Name = viewModel.Farm.Name;
farm.Owner = viewModel.Farm.Owner;
farm.Address = viewModel.Farm.Address;
farm.City = viewModel.Farm.City;
farm.Zip = viewModel.Farm.Zip;
farm.WebAddress = viewModel.Farm.WebAddress;
farm.PhoneNumber = viewModel.Farm.PhoneNumber;
farm.Hostel = viewModel.Farm.Hostel;
farm.Details = viewModel.Farm.Details;
farm.Latitude = viewModel.Farm.Latitude;
farm.Longitude = viewModel.Farm.Longitude;
farm.Weather = viewModel.Farm.Weather;
// Add or update the crops.
string[] cropIds = Request.Form["crop-token-input"].Split(',');
List<Crop> allCrops = this.ReadWriteSession.All<Crop>().ToList();
if (!isNew)
{
// Remove all previous crop/farm relationships.
farm.Crops.Clear();
}
// Loop through and add any crops.
foreach (Crop crop in allCrops)
{
foreach (string id in cropIds)
{
Guid guid = Guid.Parse(id);
if (crop.CropId == guid)
{
farm.Crops.Add(crop);
}
}
}
if (isNew)
{
this.ReadWriteSession.Add<Farm>(farm);
}
else
{
this.ReadWriteSession.Update<Farm>(farm);
}
this.ReadWriteSession.CommitChanges();
My update code within the ReadWriteSession is simple enough (GetSetName<T> just returns the types name from it's PropertyInfo.):
/// <summary>
/// Updates an instance of the specified type.
/// </summary>
/// <param name="item">The instance of the given type to add.</param>
/// <typeparam name="T">The type of entity for which to provide the method.</typeparam>
public void Update<T>(T item) where T : class, new()
{
this.context.AttachTo(this.GetSetName<T>(), item);
this.context.ObjectStateManager.ChangeObjectState(item, EntityState.Modified);
}
You are adding existing Crop objects (from the allCrops list) to the new Farm. When you connect a new entity to an existing one, the new entity automatically gets attached to the context. Therefore you get the error when you try to attach the Farm to the context the second time.
The Add<Farm>(farm) statement in your code is not even necessary to connect the Farm to the context, and if you have an existing Farm that is loaded from the context, it is already attached to the context.
The whole of your if (isNew) statement is unnecessary. Entity framework tracks object state itself, so you don't need to set the modified state.
you don't have to attach the "farm" object at the end, because it's already attached as modified when you change one of its properties. try removing the else statement at the end:
if (isNew)
{
this.ReadWriteSession.Add<Farm>(farm);
}
Hope this helps :)
The problem is in your update method. You can't attach the Farm instance because you have loaded it from the same context so it is already attached and you don't need to call your Update at all because changes to attached objects are tracked automatically.