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;
}
Related
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
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).
Error message: Attaching an entity of type failed because another entity of the same type already has the same primary key value.
Question: How do I attached an entity in a similar fashion as demonstrated in the AttachActivity method in the code below?
I have to assume the "another entity" part of the error message above refers to an object that exists in memory but is out of scope (??). I note this because the Local property of the DBSet for the entity type I am trying to attach returns zero.
I am reasonably confident the entities do not exist in the context because I step through the code and watch the context as it is created. The entities are added in the few lines immediately following creation of the dbcontext.
Am testing for attached entities as specified here:what is the most reasonable way to find out if entity is attached to dbContext or not?
When looking at locals in the locals window of visual studio I see no entities of type Activity (regardless of ID) except the one I am trying to attach.
The code executes in this order: Try -> ModifyProject -> AttachActivity
Code fails in the AttachActivity at the commented line.
Note the code between the debug comments which will throw if any entities have been added to the context.
private string AttachActivity(Activity activity)
{
string errorMsg = ValidateActivity(activity); // has no code yet. No. It does not query db.
if(String.IsNullOrEmpty(errorMsg))
{
// debug
var state = db.Entry(activity).State; // Detached
int activityCount = db.Activities.Local.Count;
int projectCount = db.Activities.Local.Count;
if (activityCount > 0 || projectCount > 0)
throw new Exception("objects exist in dbcontext");
// end debug
if (activity.ID == 0)
db.Activities.Add(activity);
else
{
db.Activities.Attach(activity); // throws here
db.Entry(activity).State = System.Data.Entity.EntityState.Modified;
}
}
return errorMsg;
}
public int ModifyProject(Presentation.PresProject presProject, out int id, out string errorMsg)
{
// snip
foreach (PresActivity presActivity in presProject.Activities)
{
Activity a = presActivity.ToActivity(); // returns new Activity object
errorMsg = ValidateActivity(a); // has no code yet. No. It does not query db.
if (String.IsNullOrEmpty(errorMsg))
{
a.Project = project;
project.Activities.Add(a);
AttachActivity(a);
}
else
break;
}
if (string.IsNullOrEmpty(errorMsg))
{
if (project.ID == 0)
db.Projects.Add(project);
else
db.AttachAsModfied(project);
saveCount = db.SaveChanges();
id = project.ID;
}
return saveCount;
}
This is the class that news up the dbContext:
public void Try(Action<IServices> work)
{
using(IServices client = GetClient()) // dbContext is newd up here
{
try
{
work(client); // ModifyProject is called here
HangUp(client, false);
}
catch (CommunicationException e)
{
HangUp(client, true);
}
catch (TimeoutException e)
{
HangUp(client, true);
}
catch (Exception e)
{
HangUp(client, true);
throw;
}
}
I am not asking: How do I use AsNoTracking What difference does .AsNoTracking() make?
One solution to avoid receiving this error is using Find method. before attaching entity, query DbContext for desired entity, if entity exists in memory you get local entity otherwise entity will be retrieved from database.
private void AttachActivity(Activity activity)
{
var activityInDb = db.Activities.Find(activity.Id);
// Activity does not exist in database and it's new one
if(activityInDb == null)
{
db.Activities.Add(activity);
return;
}
// Activity already exist in database and modify it
db.Entry(activityInDb).CurrentValues.SetValues(activity);
db.Entry(activityInDb ).State = EntityState.Modified;
}
Attaching an entity of type failed because another entity of the same type already has the same primary key value. This can happen when using the Attach method or setting the state of an entity to Unchanged or Modified if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the Add.
The solution is that
if you had to use GetAll()
public virtual IEnumerable<T> GetAll()
{
return dbSet.ToList();
}
Change To
public virtual IEnumerable<T> GetAll()
{
return dbSet.AsNoTracking().ToList();
}
I resolved this error by changing Update method like below.
if you are using generic repository and Entity
_dbContext.Set<T>().AddOrUpdate(entityToBeUpdatedWithId);
or normal(non-generic) repository and entity , then
_dbContext.Set<TaskEntity>().AddOrUpdate(entityToBeUpdatedWithId);
If you use AddOrUpdate() method, please make sure you have added
System.Data.Entity.Migrations namespace.
I have a method inside a root entity (of type ProductOptionGroup) that sets properties on a reference to another object (target, also of type ProductOptionGroup).
I have to pass in by reference as the target variable is modified in the below method snippet:
void SetOptionDependency(Product sourceProduct, Product targetProduct, ref ProductOptionGroup target)
{
if (this.targetDependencyId == null)
{
this.targetDependencyId = target.Id;
}
if (this.targetDependencyId != target.Id)
{
// abort - reassignement of active dependency not allowed
}
if (this.map.Contains(sourceProduct.Id) == false)
{
// abort - the provided id is not associated with us
}
if (target.map.Contains(targetProduct.Id) == false)
{
// abort - the supplied id is not associated with the dependency target
}
// ** here the parameter passed in is modified **
target.associationCount[targetProduct.Id]++;
this.map.Add(sourceProduct.Id, targetProduct.Id);
}
I have put the code within the ProductOptionGroup aggregate as the business rules are most easily read there.
I guess the alternative could be to use a domain service to make the associations, but then some of the business logic and checking would come out of the entity and in to the service. I am not sure I like this idea.
The downside i see is that who ever is calling the method on the entity for would need to ensure that the entity is saved, along with the object modified by reference is also saved - the ref keyword on the method gives a big hint but is not explicit.
My question is does modifying variables passed in by reference to a entity method violate any DDD rules?
To give better context if needed, full code snippets below:
public class ProductOptionGroup
{
string Id; // our Id
string targetDependencyId; // we can link to one other ProductOptionsGroup by reference
MapList<string, string> map = new MapList<string, string>();
Dictionary<string, int> associationCount = new Dictionary<string, int>();
void AssociateProduct(Product product)
{
this.map.AddKey(product.Id);
}
void DisassociatedProduct(Product product)
{
if (this.map.Contains(product.Id) == false)
{
// abort - the provided id is not associated with us
}
// check children are not referring to this product
int count = 0;
if (this.associationCount.TryGetValue(product.Id, out count) && count > 0)
{
// abort - this product is being referenced
}
else
{
this.map.Remove(product.Id);
}
}
void SetOptionDependency(Product sourceProduct, Product targetProduct, ref ProductOptionGroup target)
{
if (this.targetDependencyId == null)
{
this.targetDependencyId = target.Id;
}
if (this.targetDependencyId != target.Id)
{
// abort - reassignement of active dependency not allowed
}
if (this.map.Contains(sourceProduct.Id) == false)
{
// abort - the provided id is not associated with us
}
if (target.map.Contains(targetProduct.Id) == false)
{
// abort - the supplied id is not associated with the dependency target
}
target.associationCount[targetProduct.Id]++;
this.map.Add(sourceProduct.Id, targetProduct.Id);
}
}
In short I have concluded modifying the state of one variable inside the function of another function is really bad news, I view this as a side effect.
As was mentioned in the comments, needing to modify multiple things at once pointed to deeper domain insight that was waiting to be explored to allow a more natural and clean model, and associated code.
https://softwareengineering.stackexchange.com/questions/40297/what-is-a-side-effect
This is driving me nuts. I'm not sure what else to try. This is my latest attempt to update a list of objects within another object using EF 4.3.
The scenario is that a user has added a new Task to an Application that already has one task in its Tasks property. The Application is not attached to the DB context because it was retrieved in a prior logic/DB call. This is the class and property:
public class Application : EntityBase
{
public ObservableCollection<TaskBase> Tasks { // typical get/set code here }
}
This is my attempt to update the list. What happens is that the new Task gets added and the association correctly exists in the DB. However, the first task, that wasn't altered, has its association removed in the DB (its reference to the Application).
This is the Save() method that takes the Application that the user modified:
public void Save(Application newApp)
{
Application appFromContext;
appFromContext = this.Database.Applications
.Include(x => x.Tasks)
.Single(x => x.IdForEf == newApp.IdForEf);
AddTasksToApp(newApp, appFromContext);
this.Database.SaveChanges();
}
And this is the hooey that's apparently necessary to save using EF:
private void AddTasksToApp(Application appNotAssociatedWithContext, Application appFromContext)
{
List<TaskBase> originalTasks = appFromContext.Tasks.ToList();
appFromContext.Tasks.Clear();
foreach (TaskBase taskModified in appNotAssociatedWithContext.Tasks)
{
if (taskModified.IdForEf == 0)
{
appFromContext.Tasks.Add(taskModified);
}
else
{
TaskBase taskBase = originalTasks.Single(x => x.IdForEf == taskModified.IdForEf); // Get original task
this.Database.Entry(taskBase).CurrentValues.SetValues(taskModified); // Update with new
}
}
}
Can anyone see why the first task would be losing its association to the Application in the DB? That first task goes through the else block in the above code.
Next, I'll need to figure out how to delete one or more items, but first things first...
After continual trial and error, this appears to be working, including deleting Tasks. I thought I'd post this in case it helps someone else. I'm also hoping that someone tells me that I'm making this more complicated than it should be. This is tedious and error-prone code to write when saving every object that has a list property.
private void AddTasksToApp(Application appNotAssociatedWithContext, Application appFromContext)
{
foreach (TaskBase taskModified in appNotAssociatedWithContext.Tasks)
{
if (taskModified.IdForEf == 0)
{
appFromContext.Tasks.Add(taskModified);
}
else
{
TaskBase taskBase = appFromContext.Tasks.Single(x => x.IdForEf == taskModified.IdForEf); // Get original task
this.Database.Entry(taskBase).CurrentValues.SetValues(taskModified); // Update with new
}
}
// Delete tasks that no longer exist within the app.
List<TaskBase> tasksToDelete = new List<TaskBase>();
foreach (TaskBase originalTask in appFromContext.Tasks)
{
TaskBase task = appNotAssociatedWithContext.Tasks.Where(x => x.IdForEf == originalTask.IdForEf).FirstOrDefault();
if (task == null)
{
tasksToDelete.Add(originalTask);
}
}
foreach (TaskBase taskToDelete in tasksToDelete)
{
appFromContext.Tasks.Remove(taskToDelete);
this.Database.TaskBases.Remove(taskToDelete);
}
}