Dynamics CRM 2011 check previous value in preimage in workflow

Sometimes it is required to check the previous value in a workflow triggered when record is updated.

The out of the box workflow create user interface only provide access to current value of the record which is the value after modification.

We could write one custom workflow activity to retrieve that value from IExecutionContext, which can be accessed from IWorkflowContext within custom workflow activity.


protected override void Execute(CodeActivityContext executionContext)
 {
IWorkflowContext context = executionContext.GetExtension<IWorkflowContext>();
Entity preImageEntity = context.PreEntityImages.Values.FirstOrDefault();
}

When use InOutArgument<EntityReference > in custom workflow activity, we need to specify the entity name which made it difficult to reuse, instead we could accept the attribute name of the lookup field and output entity name and record id for that lookup field. We can use entity name and record id in other workflow activity to perform generic tasks.

Here is a sample code to retrieve record id and entity name from custom workflow activity


public sealed class GetRecordIdEntityName : CodeActivity
 {
 [Input("Attribute Name for Related Record")]
 public InArgument<String> AttributeName { get; set; }

[Output("Primary Entity Name")]
 public OutArgument<String> PrimaryEntityName { get; set; }

[Output("Primary Record Id")]
 public OutArgument<String> PrimaryRecordId { get; set; }

[Output("Related Entity Name")]
 public OutArgument<String> RelatedEntityName { get; set; }

[Output("Pre Related Record Id")]
 public OutArgument<String> PreRelatedRecordId { get; set; }

[Output("Post Related Record Id")]
 public OutArgument<String> PostRelatedRecordId { get; set; }

/// <summary>
 /// Executes the workflow activity.
 /// </summary>
 /// <param name="executionContext">The execution context.</param>
 protected override void Execute(CodeActivityContext executionContext)
 {
 // Create the tracing service
 ITracingService tracingService = executionContext.GetExtension<ITracingService>();

if (tracingService == null)
 {
 throw new InvalidPluginExecutionException("Failed to retrieve tracing service.");
 }

tracingService.Trace("Entered GetRecordId.Execute(), Activity Instance Id: {0}, Workflow Instance Id: {1}",
 executionContext.ActivityInstanceId,
 executionContext.WorkflowInstanceId);

// Create the context
 IWorkflowContext context = executionContext.GetExtension<IWorkflowContext>();

if (context == null)
 {
 throw new InvalidPluginExecutionException("Failed to retrieve workflow context.");
 }

tracingService.Trace("GetRecordId.Execute(), Correlation Id: {0}, Initiating User: {1}",
 context.CorrelationId,
 context.InitiatingUserId);

IOrganizationServiceFactory serviceFactory = executionContext.GetExtension<IOrganizationServiceFactory>();
 IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

try
 {
 // TODO: Implement your custom Workflow business logic.
 ExecuteCore(executionContext, context, service);
 }
 catch (FaultException<OrganizationServiceFault> e)
 {
 tracingService.Trace("Exception: {0}", e.ToString());

// Handle the exception.
 throw;
 }

tracingService.Trace("Exiting GetRecordId.Execute(), Correlation Id: {0}", context.CorrelationId);
 }

private void ExecuteCore(CodeActivityContext executionContext, IWorkflowContext context, IOrganizationService service)
 {
 this.PrimaryRecordId.Set(executionContext, context.PrimaryEntityId.ToString());

this.PrimaryEntityName.Set(executionContext, context.PrimaryEntityName.ToLowerInvariant());

string attributeName = this.AttributeName.Get(executionContext);

this.RelatedEntityName.Set(executionContext, null);

if (!string.IsNullOrWhiteSpace(attributeName))
 {
 Entity preImageEntity = context.PreEntityImages.Values.FirstOrDefault();
 if (preImageEntity != null && preImageEntity.Contains(attributeName) && preImageEntity[attributeName] is EntityReference)
 {
 EntityReference entityRef = preImageEntity.GetAttributeValue<EntityReference>(attributeName);
 this.RelatedEntityName.Set(executionContext, entityRef.LogicalName.ToLowerInvariant());
 this.PreRelatedRecordId.Set(executionContext, entityRef.Id.ToString());
 }
 else
 {
 this.PreRelatedRecordId.Set(executionContext, null);
 }

Entity postImageEntity = context.PostEntityImages.Values.FirstOrDefault();
 if (postImageEntity != null && postImageEntity.Contains(attributeName) && postImageEntity[attributeName] is EntityReference)
 {
 EntityReference entityRef = postImageEntity.GetAttributeValue<EntityReference>(attributeName);
 this.RelatedEntityName.Set(executionContext, entityRef.LogicalName.ToLowerInvariant());
 this.PostRelatedRecordId.Set(executionContext, entityRef.Id.ToString());
 }
 else
 {
 this.PostRelatedRecordId.Set(executionContext, null);
 }
 }
 else
 {
 this.PreRelatedRecordId.Set(executionContext, null);
 this.PostRelatedRecordId.Set(executionContext, null);
 }
 }
 }

Advertisements

Dynamics CRM 2011 View to display records shared with current user

In my current project, there is a requirement to create a view to display shared record of certain entity.

This kind of view is supported but can not be achieved through customization user interface.

System view and user view are defined by using fetchxml and layoutxml, we could create a view through CRM API and set the correct fetchxml with is not support in out of the box customization user interface.

When records are shared, CRM create a record of “principalobjectaccess” entity; so we can link the entity we want to display to this entity to construct proper fetchxml.

The drawback of the approach is that you can not use customization user interface to modify the column layout and sort order.

Suppose we have an entity called “zzhou_cfiapplication” and we want to create a view to display records shared with current user.

The important piece of xml to add to view fetchxml is the link entity to “principalobjectaccesss” entity.


<link-entity name='principalobjectaccess' to='zzhou_cfiapplicationid' from='objectid' link-type='inner' alias='share'>
 <filter type='and'>
 <condition attribute='principalid' operator='eq-userid' />
 </filter>
</link-entity>

It is much more easier to configure the view in customization user interface to set up correct columns and sort order, and then modify the fetchxml for that view.

We can use the following code to retrieve the view that we want to modify


public Entity RetrieveSavedQuery(OrganizationServiceContext context, Guid queryId)
 {
 var query = from q in context.CreateQuery("savedquery")
 where q.GetAttributeValue<Guid>("savedqueryid") == queryId
 select q;

Entity record = query.FirstOrDefault();

return record;
 }

we can update the view by using the following code

</pre>
public void UpdateSavedQuery(IOrganizationService service)
 {
 Entity savedQuery = new Entity("savedquery");
 savedQuery.Id = new Guid("00bc926f-26c0-e111-a4f9-00155d4c5b01");
 savedQuery["fetchxml"] = @"<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>
 <entity name='zzhou_cfiapplication'>
 <attribute name='zzhou_name' />
 <attribute name='createdon' />
 <order attribute='zzhou_name' descending='false' />
 <link-entity name='principalobjectaccess' to='zzhou_cfiapplicationid' from='objectid' link-type='inner' alias='share'>
 <filter type='and'>
 <condition attribute='principalid' operator='eq-userid' />
 </filter>
 </link-entity>
 <attribute name='zzhou_cfiapplicationid' />
 </entity>
 </fetch>";

service.Update(savedQuery);
 }
<pre>

Dynamics CRM 2011 Share Secured Fields

Dynamics CRM 2011 support field level security for custom attributes. Depending on your solution and security model for that solution, there might be a requirement to share secured fields in plugin, workflow or JavaScript.

This post describe how to use late bound entities to create a custom workflow activity to share secured fields, the complete code is in the end of this post.

 Note: If users’ security role says they cannot update records, then they will not be able to update any field on that record even if there is a field level security profile says that they can update that field on that entity.

Each time when users share a secured field to other users, CRM create records of entity “principalobjectattributeaccess” with proper value set for “readaccess”, “updateaccess”, “attributeid”,”objectid”, and “principalid” attributes.

To create this entity correctly, we need the id of the attribute metadata, so we can use the following code to retrieve that id based on the attribute name


// Create the request
 RetrieveAttributeRequest attributeRequest = new RetrieveAttributeRequest
 {
 EntityLogicalName = entityName,
 LogicalName = attributeName,
 RetrieveAsIfPublished = true
 };

// Execute the request
 RetrieveAttributeResponse attributeResponse = (RetrieveAttributeResponse)service.Execute(attributeRequest);

CRM does not allow duplicate records for “principalobjectattributeaccess” entity, so we need to uses some query like below to check whether there is record exists first.


// Create the query for retrieve User Shared Attribute permissions.
 QueryExpression queryPOAA = new QueryExpression("principalobjectattributeaccess");
 queryPOAA.ColumnSet = new ColumnSet(new string[] { "readaccess", "updateaccess" });
 queryPOAA.Criteria.FilterOperator = LogicalOperator.And;
 queryPOAA.Criteria.Conditions.Add(new ConditionExpression("attributeid", ConditionOperator.Equal, metadataId));
 queryPOAA.Criteria.Conditions.Add(new ConditionExpression("objectid", ConditionOperator.Equal, objectId));
 queryPOAA.Criteria.Conditions.Add(new ConditionExpression("principalid", ConditionOperator.Equal, principalId));

// Execute the query.
 EntityCollection responsePOAA = service.RetrieveMultiple(queryPOAA);

We can now create or update the record to share and update secured field.

Here is the complete custom workflow activity


// <copyright file="ShareSecuredField.cs" company="">
// Copyright (c) 2012 All Rights Reserved
// </copyright>
// <author></author>
// <date>6/5/2012 11:41:45 PM</date>
// <summary>Implements the ShareSecuredField Workflow Activity.</summary>
namespace Xrm.Workflow.Activities
{
 using System;
 using System.Activities;
 using System.ServiceModel;
 using Microsoft.Xrm.Sdk;
 using Microsoft.Xrm.Sdk.Workflow;
 using Microsoft.Xrm.Sdk.Messages;
 using Microsoft.Xrm.Sdk.Query;

public sealed class ShareSecuredField : CodeActivity
 {
 [RequiredArgument]
 [Input("Attribute Name")]
 public InArgument<String> AttributeName { get; set; }

[Input("Share With User")]
 [ReferenceTarget("systemuser")]
 public InArgument<EntityReference> UserToShare { get; set; }

[Input("Share With Team")]
 [ReferenceTarget("team")]
 public InArgument<EntityReference> TeamToShare { get; set; }

[RequiredArgument]
 [Input("Allow Read")]
 [Default("true")]
 public InArgument<Boolean> AllowRead { get; set; }

[RequiredArgument]
 [Input("Allow Update")]
 [Default("true")]
 public InArgument<Boolean> AllowUpdate { get; set; }

/// <summary>
 /// Executes the workflow activity.
 /// </summary>
 /// <param name="executionContext">The execution context.</param>
 protected override void Execute(CodeActivityContext executionContext)
 {
 // Create the tracing service
 ITracingService tracingService = executionContext.GetExtension<ITracingService>();

if (tracingService == null)
 {
 throw new InvalidPluginExecutionException("Failed to retrieve tracing service.");
 }

tracingService.Trace("Entered ShareSecuredField.Execute(), Activity Instance Id: {0}, Workflow Instance Id: {1}", executionContext.ActivityInstanceId, executionContext.WorkflowInstanceId);

// Create the context
 IWorkflowContext context = executionContext.GetExtension<IWorkflowContext>();

if (context == null)
 {
 throw new InvalidPluginExecutionException("Failed to retrieve workflow context.");
 }

tracingService.Trace("ShareSecuredField.Execute(), Correlation Id: {0}, Initiating User: {1}", context.CorrelationId, context.InitiatingUserId);

IOrganizationServiceFactory serviceFactory = executionContext.GetExtension<IOrganizationServiceFactory>();
 IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

try
 {
 // TODO: Implement your custom Workflow business logic.
 ExecuteCore(executionContext, context, service);
 }
 catch (FaultException<OrganizationServiceFault> e)
 {
 tracingService.Trace("Exception: {0}", e.ToString());

// Handle the exception.
 throw;
 }

tracingService.Trace("Exiting ShareSecuredField.Execute(), Correlation Id: {0}", context.CorrelationId);
 }

private void ExecuteCore(CodeActivityContext executionContext, IWorkflowContext context, IOrganizationService service)
 {
 string entityName = context.PrimaryEntityName;
 Guid entityId = context.PrimaryEntityId;
 string attributeName = this.AttributeName.Get(executionContext);
 EntityReference userToShare = this.UserToShare.Get(executionContext);
 EntityReference teamToShare = this.TeamToShare.Get(executionContext);
 bool allowRead = this.AllowRead.Get(executionContext);
 bool allowUpdate = this.AllowUpdate.Get(executionContext);

if (userToShare != null)
 {
 Guid objectId = userToShare.Id;
 ShareSecuredFieldCore(service, entityName, attributeName, entityId, objectId, allowRead, allowUpdate, false);
 }

if (teamToShare != null)
 {
 Guid objectId = teamToShare.Id;
 ShareSecuredFieldCore(service, entityName, attributeName, entityId, objectId, allowRead, allowUpdate);
 }
 }

private void ShareSecuredFieldCore(IOrganizationService service, string entityName, string attributeName, Guid objectId, Guid principalId, bool allowRead, bool allowUpdate, bool shareWithTeam = true)
 {
 // Create the request
 RetrieveAttributeRequest attributeRequest = new RetrieveAttributeRequest
 {
 EntityLogicalName = entityName,
 LogicalName = attributeName,
 RetrieveAsIfPublished = true
 };

// Execute the request
 RetrieveAttributeResponse attributeResponse = (RetrieveAttributeResponse)service.Execute(attributeRequest);

if (attributeResponse.AttributeMetadata != null && attributeResponse.AttributeMetadata.IsSecured != null && attributeResponse.AttributeMetadata.IsSecured.HasValue && attributeResponse.AttributeMetadata.IsSecured.Value)
 {
 // Create the query for retrieve User Shared Attribute permissions.
 QueryExpression queryPOAA = new QueryExpression("principalobjectattributeaccess");
 queryPOAA.ColumnSet = new ColumnSet(new string[] { "readaccess", "updateaccess" });
 queryPOAA.Criteria.FilterOperator = LogicalOperator.And;
 queryPOAA.Criteria.Conditions.Add(new ConditionExpression("attributeid", ConditionOperator.Equal, attributeResponse.AttributeMetadata.MetadataId));
 queryPOAA.Criteria.Conditions.Add(new ConditionExpression("objectid", ConditionOperator.Equal, objectId));
 queryPOAA.Criteria.Conditions.Add(new ConditionExpression("principalid", ConditionOperator.Equal, principalId));

// Execute the query.
 EntityCollection responsePOAA = service.RetrieveMultiple(queryPOAA);

if (responsePOAA.Entities.Count > 0)
 {
 Entity poaa = responsePOAA.Entities[0];

if (allowRead || allowUpdate)
 {
 poaa["readaccess"] = allowRead;
 poaa["updateaccess"] = allowUpdate;

service.Update(poaa);
 }
 else
 {
 service.Delete("principalobjectattributeaccess", poaa.Id);
 }
 }
 else
 {
 if (allowRead || allowUpdate)
 {
 // Create POAA entity for user
 Entity poaa = new Entity("principalobjectattributeaccess");
 poaa["attributeid"] = attributeResponse.AttributeMetadata.MetadataId;
 poaa["objectid"] = new EntityReference(entityName, objectId);
 poaa["readaccess"] = allowRead;
 poaa["updateaccess"] = allowUpdate;
 if (shareWithTeam)
 {
 poaa["principalid"] = new EntityReference("team", principalId);
 }
 else
 {
 poaa["principalid"] = new EntityReference("systemuser", principalId);
 }

service.Create(poaa);
 }
 }
 }
 }
 }
}

There are sample code in the following location in the SDK download:

SampleCode\CS\FieldSecurity\RetrieveUserSharedAttributePermissions.cs

SampleCode\VB\FieldSecurity\RetrieveUserSharedAttributePermissions.vb