I am writing a Plugin to validate a quote before it saves. We want to enforce that there must be a quote product line
Item on a quote before a quote can be activated, won or lost. Draft mode does not have this requirement.
I wrote the following code and when pressing the "Close Quote" button on the ribbon and selecting Won as the reason, a business process error box pops up with the error message.
However, upon closing the error message and refreshing the page, the quote is set to closed. Why does the quote close even though the exception was thrown?
FYI, the plugin stage is set to Pre-operation.
Here is my source code (Updated 10/02/2017):
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ValbrunaPlugins
{
public class QuoteValidation : IPlugin
{
private ITracingService tracingService;
public void Execute(IServiceProvider serviceProvider)
{
// retrieve the context, factory, and service
IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
IOrganizationService service = factory.CreateOrganizationService(context.UserId);
bool isCorrectEvent = context.MessageName == "SetStateDynamicEntity" || context.MessageName == "SetState" || context.MessageName == "Win" || context.MessageName == "Close";
bool hasEnityMoniker = context.InputParameters.Contains("EntityMoniker");
// ensure we are handling the correct event and we were passed an entity from the context
if (!isCorrectEvent || !hasEnityMoniker) return;
// get the reference to the quote entity
EntityReference quoteEntityReference = (EntityReference)context.InputParameters["EntityMoniker"];
Entity quoteEntity = null;
try
{
// get the quote entity from the entity reference
quoteEntity = ActualEntity.GetActualEntity(quoteEntityReference, service);
} catch (Exception ex)
{
throw new InvalidPluginExecutionException("Quote with id " + quoteEntityReference.Id + " not found.");
}
// ensure that we have the correct entity
if (quoteEntity.LogicalName != "quote") return;
// write query to retrieve all the details for this quote
QueryExpression retrieveQuoteDetailsQuery = new QueryExpression
{
EntityName = "quotedetail",
ColumnSet = new ColumnSet(),
Criteria = new FilterExpression
{
Conditions =
{
new ConditionExpression
{
AttributeName = "quoteid",
Operator = ConditionOperator.Equal,
Values = { (Guid)quoteEntity.Id }
}
}
}
};
// execute the query to retrieve the details for this quote
EntityCollection quoteDetails = service.RetrieveMultiple(retrieveQuoteDetailsQuery);
// retrieve the current status of the quote
// 0 - Draft
// 1 - Active
// 2 - Won
// 3 - Closed
int quoteStatus = ((OptionSetValue)(quoteEntity.Attributes["statecode"])).Value;
// if not in draft mode
if (quoteStatus != 0)
{
// if the amount of details for the quote is less than 1
if (quoteDetails.Entities.Count < 1)
{
throw new InvalidPluginExecutionException("There must be a quote product line item on a quote before a quote can be activated, won, or lost while not in draft mode.");
}
}
}
}
}
Update 10/02/2017:
I created a separate step for SetState and updated my source code.
However, I am still having the same issue. When I close the quote I get the error but when I refresh the page, the quote has been set to closed.
NOTICE: Quote is active, there is no quote detail, so the quote can not be won.
A business process error is displayed as it should be. However, when I refresh the page, the quote's status has been set to "Closed". Why did it do this if the exception was thrown?
You have to register the plugin steps for both SetState and SetStateDynamicEntity messages.
reference
Why do I need to register on SetState and SetStateDynamicEntity
separately?
As I mentioned earlier there are multiple messages that
perform the same action in CRM. One such example is SetStateRequest
and SetStateDyanmicEntityRequest . If you want to write a plug-in on
SetState, then you need to register it on both messages.
Read more
Related
I've made a plugin for primary entity 'incident' which is triggered on pre-operation stage. I am getting the error 'The given key was not present in the dictionary'. I am not sure what could be wrong here since it's my first plugin. The aim is to check if Case's serial number (zst_txtsn) exists in entity 'Warranty'
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Xrm.Sdk;
using System.ServiceModel;
using Microsoft.Xrm.Sdk.Query;
namespace ActiveWarranty
{
public class Class1 : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
ITracingService tracingService =
(ITracingService)serviceProvider.GetService(typeof(ITracingService));
IPluginExecutionContext context = (IPluginExecutionContext)
serviceProvider.GetService(typeof(IPluginExecutionContext));
if (context.InputParameters.Contains("Target") &&
context.InputParameters["Target"] is Entity)
{
Entity Case = (Entity)context.InputParameters["Target"];
IOrganizationServiceFactory serviceFactory =
(IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
try
{
string serialNumber = Case.Attributes["zst_txtsn"].ToString();
Entity Warranty = new Entity("zst_warranty");
QueryExpression query = new QueryExpression("zst_warranty");
query.ColumnSet = new ColumnSet(new string[] { "zst_name" });
query.Criteria.AddCondition("zst_serialno", ConditionOperator.Equal, serialNumber);
EntityCollection collection = service.RetrieveMultiple(query);
if (collection.Entities.Count > 0)
{
Case["zst_activewarranty"] = true;
}
else if (collection.Entities.Count == 0)
{
Case["zst_activewarranty"] = false;
}
service.Update(Case);
}
catch (FaultException<OrganizationServiceFault> ex)
{
throw new InvalidPluginExecutionException("An error occurred in FollowUpPlugin.", ex);
}
catch (Exception ex)
{
tracingService.Trace("FollowUpPlugin: {0}", ex.ToString());
throw;
}
}
}
}
}
Any help would be much appreciated.
When the object is null, .ToString() will throw an error. You can try one of these.
string serialNumber = Case.Attributes["zst_txtsn"] + "";
or
string serialNumber = "";
if(Case.GetAttributeValue<string>("zst_txtsn") != null)
{
serialNumber = Case.GetAttributeValue<string>("zst_txtsn").ToString();
}
Few things to note
1-
If this plugin is registered on the create event
The service.update(case) will fail because Target has no id yet
In this case any updates should be done on the Target without a service.update
2-
If this plugin is registered on the update event it’ll cause a loop if you’re not setting the filtering attributes properly for the trigger in the plugin registration tool. Additionally, since it’s on the pre stage, it should be enough to update the target with your boolean value to pass it to the platform.
In both cases above you should remove the service.update
3-
If zst_serialno is a lookup and you’re querying using the serial number text value
Then your query is wrong
You’ll have to use LinkEntities / $expand to query by related entity attributes
In my method below which is written using c# in asp.net core, I am getting error when executing any of the below update statements. What is the issue with using await SaveContextAsync(); I have noticed I dont get error when only using SaveContext();
The error that I am getting is as follows
Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection
and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling Dispose()
on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency
injection container take care of disposing context instances.
Object name: 'FXDB1Context'.
I am not sure why I am getting this error.
public async Task<IdentityResult> ApproveUserChangeRequest(UserChangeRequest userChangeRequest, string approvedByAuthUserName, string authApplicationName)
{
var userChangeRequestRepository = UserChangeRequestRepository.GetAllAsList();
var userChangeRequestApprovalRepository = UserChangeRequestApprovalRepository.GetAllAsList();
var appSettingRepository = AppSettingRepository.GetAllAsList();
var clientCompanyContactRepository = ClientCompanyContactRepository.GetAllAsList();
var applicationUserRepo = ApplicationUserRepo.GetAllAsList();
int approvedByAuthUserID = GetApprovedByUserId(authApplicationName, approvedByAuthUserName);
// Check if UserChangeRequest is still Pending
bool isUserChangeRequestPending = userChangeRequestRepository.Any(x => x.Id == userChangeRequest.Id && x.ChangeStatus == "Pending");
if (isUserChangeRequestPending && approvedByAuthUserID > 0)
{
// Inserting record in the UserChangeRequestApproval table
InsertUserChangeRequestApproval(userChangeRequest);
await SaveContextAsync();
using (var userTransaction = Context.Database.BeginTransaction())
{
using (var securityTransaction = _securityContext.Database.BeginTransaction())
{
try
{
//Get the Number of approval required for Internal and External Users
int? internalApprovalsRequired = GetApprovals("InternalUserChangeRequestApprovalsRequired", appSettingRepository);
int? externalApprovalsRequired = GetApprovals("ExternalUserChangeRequestApprovalsRequired", appSettingRepository);
//Get the name of the application the auth user belongs to
var authUserApplicationName = GetApplicationName(userChangeRequest.AuthUserId);
//Get the Number of approvals for the request
var numberOfAprovals = userChangeRequestApprovalRepository.Where(x => x.UserChangeRequestId == userChangeRequest.Id).Count();
//If the number of approvals is equal or greater than the Approvals required then Update AppUser or Contact details
if ((authUserApplicationName == "ArgentexTrader" && numberOfAprovals >= internalApprovalsRequired) || (authUserApplicationName == "ArgentexClient" && numberOfAprovals >= externalApprovalsRequired))
{
//Updating the clientcontact table
UpdateClientContact(userChangeRequest, clientCompanyContactRepository);
//Updating the auth user table
UpdateAuthUser(userChangeRequest);
//Updating the IdentityDB user table
UpdateIdentityDBUser(userChangeRequest, applicationUserRepo);
//Updating the UserChangeRequest table
userChangeRequest.ChangeStatus = "Approved";
UserChangeRequestRepository.Update(userChangeRequest);
await SaveContextAsync();
userTransaction.Commit();
securityTransaction.Commit();
return IdentityResult.Success;
}
}
catch (Exception ex)
{
userTransaction.Rollback();
securityTransaction.Rollback();
_logger.Error(ex);
return IdentityResult.Failed(new IdentityError { Description = ex.Message });
}
}
}
}
return null;
}
The error says that a resolved context is been trying to made Disposable.
For More Information: Using Statements in C#
using statement is a scope only process, so this error means that your program tries to use the same instances of "context" in somewhere else, which it cant use it, because as I mentioned it is a "scope only" process.
In your example:
using (var userTransaction = Context.Database.BeginTransaction())
{
using (var securityTransaction = _securityContext.Database.BeginTransaction())
{
}
}
You can make it simple and dont use a using scope or you can be sure that, the context is just used in those using scopes.
I'm writing a plugin in C# for Dynamics CRM 2011 (On Prem).
The plugin is to prevent grandfathering in Account records (Accounts can have children OR a parent, but not both at the same time).
I'm struggling to check whether the Account has an associated parent or not, the attribute for parent ID is always null and I don't understand why.
I've correctly generated the early bound classes using crmsvcutil.exe and added the file to my project.
The generated code for the parent ID looks like this:
/// <summary>
/// Unique identifier of the parent account.
/// </summary>
[Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("parentaccountid")]
public Microsoft.Xrm.Sdk.EntityReference ParentAccountId
{
get
{
return this.GetAttributeValue<Microsoft.Xrm.Sdk.EntityReference>("parentaccountid");
}
set
{
this.OnPropertyChanging("ParentAccountId");
this.SetAttributeValue("parentaccountid", value);
this.OnPropertyChanged("ParentAccountId");
}
}
A basic version of my plugin looks like this:
namespace NoGrandparentAccounts
{
using CRM2011GeneratedBusinessModel; // Namespace for crmsvcutil generated early bound classes
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.ServiceModel;
public class Main : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
// OBTAIN EXECUTION CONTEXT
var context(IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
// TRACING SERVICE FOR LOGGING
var tracingService =
(ITracingService)serviceProvider.GetService(typeof(ITracingService));
// WILL REPRESENT THE CURRENT ENTITY
Entity entity = null;
// INPUTPARAMETERS COLLECTION CONTAINS ALL DATA PASSED IN THE MESSAGE REQUEST
if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity)
{
// SET ENTITY TO THE CURRENT ENTITY
entity = (Entity)context.InputParameters["Target"];
// CHECK WE'RE IN ACCOUNTS ENTITY
if (context.PrimaryEntityName != "account")
return;
// CHECK WE'RE CREATING OR UPDATING THE ENTITY
if (context.MessageName != "Create" && context.MessageName != "Update")
return;
}
else
{
return;
}
try
{
// GET ORGANIZATION SERVICE
var serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
var service = serviceFactory.CreateOrganizationService(context.UserId);
// GET CURRENT ACCOUNT
var account = entity.ToEntity<Account>();
// HAS A PARENT ACCOUNT?
var hasParent = (account.ParentAccountId != null && account.ParentAccountId.Id != Guid.Empty);
}
catch (FaultException<OrganizationServiceFault> ex)
{
tracingService.Trace($#"Exception: {ex.Message} - {ex.StackTrace}");
throw new InvalidPluginExecutionException(#"An error occurred in the plugin.", ex);
}
catch (Exception ex)
{
tracingService.Trace($#"Exception: {ex.Message} - {ex.StackTrace}");
throw new InvalidPluginExecutionException(#"An error occurred in the plugin.", ex);
}
}
}
}
Unfortunately hasParent is always false because account.ParentAccountId is null. I don't know why because there is most definitely a parent account associated with this record and the attribute name is definitely parentaccountid.
The call to find the child Accounts works fine using parentaccountid:
// FETCH CHILDREN
var cExpr = new ConditionExpression("parentaccountid", ConditionOperator.Equal, account.Id);
var qExpr = new QueryExpression(Account.EntityLogicalName);
qExpr.ColumnSet = new ColumnSet(new string[] { "accountid" });
qExpr.Criteria.AddCondition(cExpr);
var results = service.RetrieveMultiple(qExpr);
// HAS CHILDREN
var hasChildren = results.Entities.Count > 0;
The entity count shows correctly as 3 and therefore hasChildren is true.
I've checked whether the attributes for this entity contains parentaccountid and it returns false which I assume is related to the problem:
if (entity.Attributes.Contains("parentaccountid"))
{
// Never gets here as this is false!?
}
What am I doing wrong?
The Target entity itself contains only basic entity identity data. If you want to access attributes for the entity you should configure a suitable PreImage or PostImage with the attributes that you want to access context.PreEntityImages, and you can then retrieve this via context.PreEntityImages or context.PostEntityImages. Alternatively (and less optimally) you can retrieve the latest version of the entity from within the PlugIn with the attributes you desire.
Do this
// GET CURRENT ACCOUNT
var account = (Account)service.Retrieve("account", entity.Id, new ColumnSet(new string[] { "accountid", "parentaccountid" }));
Instead of this
// GET CURRENT ACCOUNT
var account = entity.ToEntity<Account>();
As Josh Painter indicated in his comment, its possible that the event you registered against isn't passing in this value to the PluginExecutionContext.InputParameters["Target"], and thus the attribute becomes null when casting the Entity to an Account. Above is how to do a query to get the full record within the plugin, and that will always be populated, unless the plugin is registered pre-stage create (in which case your current code will work just fine, but since you're getting a null value, I assume you're not registering on pre-stage create).
i implemented my first LINQ query to check for duplicate records while the user adds a new record but it is not getting fired
I am working on CRM2011 and i wrote the plugin using LINQ and registered it with Plugin Registration Tool
Below is my code
if (context.Depth == 1)
{
if (context.InputParameters.Contains("Target") &&
context.InputParameters["Target"] is Entity)
{
target =(Entity)context.InputParameters["Target"];
if (target != null)
{
householdname = target.GetAttributeValue<string>("mcg_HouseholdName");
}
}
OrganizationServiceContext orgcontext = new OrganizationServiceContext(service);
{
var query = (from c in orgcontext.CreateQuery<mcg_household>()
where c.mcg_HouseholdName == householdname
select c
);
List<mcg_household> householdlist = query.ToList<mcg_household>();
if (householdlist.Count > 0)
{
throw new InvalidPluginExecutionException("Record Already Exists!");
}
}
}
i think the problem is with getattribute because when i check it with some hardcoded value it runs. please tell me in what stage i should register this plugin and if there is anything wrong with the code.
If your code works with hardcoded example it is probably problem with stage of execution. You have to register your plugin step in Pre-Operation stage of execution and Synchronous mode. Check this article for details.
Also, check if "mcg_HouseholdName" is correct string.
All users but a specific one should not be allowed to edit or update Quote Details which not satisfy a specific condition, but they have to be capable of revising the Quote if they want to.
The issue is, revising the Quote (i.e. the user clicks the "Revise" button in an Active form record) triggers the Update of the Quote Details and I can't figure out how to recognize what's going on.
My current attempt is based on a plugin which code looks like this:
public class PreQuoteProductUpdate : Plugin
{
// I work with CRM Developer Tools to build plugins
// This goes in Update Message, Pre-Operation, Server Only, pre-image called "preImage"
protected void ExecutePreQuoteProductUpdate(LocalPluginContext localContext)
{
if (localContext == null)
{
throw new ArgumentNullException("localContext");
}
IPluginExecutionContext context = localContext.PluginExecutionContext;
IOrganizationService srv = localContext.OrganizationService;
Entity preImageEntity = (context.PreEntityImages != null && context.PreEntityImages.Contains(this.preImageAlias)) ? context.PreEntityImages[this.preImageAlias] : null;
try
{
PluginBody(context, srv, preImageEntity);
}
catch (Exception ex)
{
throw new InvalidPluginExecutionException("Quote Details Pre-Update", ex);
}
}
protected void PluginBody(IPluginExecutionContext context, IOrganizationService srv, Entity preImage)
{
if(IsRevising()) return;
CheckSomeCondition(context, srv);
if (preImage.Attributes.ContainsKey("ica_daconfigurazione") && preImage.GetAttributeValue<bool>("ica_daconfigurazione"))
{
CheckUser(context, srv);
}
}
protected void IsRevising()
{
// I have no clue about the logic to put here: see below.
}
protected void CheckSomeCondition(IPluginExecutionContext context, IOrganizationService srv)
{
var entity = (Entity)context.InputParameters["Target"];
// if some fields of entity contain some specific data, throw
// this always happens
}
protected void CheckUser(IPluginExecutionContext context, IOrganizationService srv)
{
//allowedUser is read from a configuration entity
var allowedUser = new Guid();
if (context.InitiatingUserId.Equals(serviceUser.Id) == false)
throw new InvalidPluginExecutionException("Can't edit quote details");
}
}
I know that (in a Quote plugin) I can know a revision is ongoing by checking the ParentContext, is anything similar available in a QuoteDetail plugin ? I gave it a try but all I get are NullReferenceExceptions thrown at me.
Should I expect to have State/Status available to check ?
For any more info which I may have overlooked, just ask.
Register on the Pre Create message (stage 20) of QuoteDetail and filter on the parent context not being for Quote. If it is, just return (effectively doing nothing).
The same applies to the Update message of the QuoteDetail.
Both messages run in the context of the ReviseQuote message for Quote.
var parentContext = context.ParentContext;
// While there is a parent context...
while (parentContext != null) {
// When parent context is for "quote", return;
if (parentContext.PrimaryEntityName == "quote")
{
return;
}
// Assign parent's parent context to loop parent context.
parentContext = parentContext.ParentContext;
}