Resolving/deactivating entities in a CRM based job or bug tracking system

I have a prototype job tracking system based on CRM 2011. I’ve used job tracking systems and bugbases where reporting is a low point, but here we get the power of CRM reporting, dashboards and views; users are free to design their own private views and dashboards and of course they can also use the sdk to extend those capabilities.

The data model is fairly simple. There are two main entities; a "run" entity which stores the “macro” data about a set of jobs, and a "job" entity. In this prototype solution jobs have just two states in their lifecycle. They start as "Active" and eventually their state becomes “Resolved”. The actual "jobs" are investigations of test automation failures. These are the initial investigations whicgh simply determine if there is further investigation required.

 Investigation of a failure at this stage will lead to one of three results:

- This test passes when re-run or run manually (not a product bug, but an intermittent automation failure)

- This is a product bug (in which case you must record the bug number)

- This is a test code bug (in which case you must record the bug number)

 

So there are the possible values for a resolution.

The job entity displays the job attributes and allows the user to choose resolution from a simple drop-down:

So at the end of an automation run I create an entity records to store the attributes of each failure. Engineers then investigate the failures and resolve them by setting one of the 3 options. If they have set them as product or test fails then they must have data (bug nos.) in the bug field.

In terms of CRM development, there are three tasks for the implementation of job resolution:

1) Validate that bug id values exists for test or product bug resolutions

2) Deactivate the record.

3) Update the rolling totals associated with the parent “run” entity

The first of these, the validation is covered with some JavaScript, fired by the job forms “OnSave” event:

//Check the bug id field on save- if a resolution is chosen function CheckResolutions() { //need to check if there is anything at all selected in the resolution options first var varResolutionValue = Xrm.Page.getAttribute("new_resolutions").getValue() ; if (varResolutionValue == "" || varResolutionValue == null) { return true; } //now get the text of the selected resolution option var varResolutionText= Xrm.Page.getAttribute("new_resolutions").getSelectedOption().text; //get the text of the Bug ID field var varBugID = Xrm.Page.getAttribute("new_psbugid").getValue() ; if ((varResolutionText== "Test Bug" || varResolutionText== "Product Bug") && (varBugID == "" || varBugID == null)) { alert("Must enter ID of "+varResolutionText+"!"); event.returnValue=false; return false; } else //resolution must be "Pass on ReRun", so let Save event take it's course (plugin will kick in to deactivate record) { return true; } }

 

The second two tasks are addressed by a small plug-in, registered on the Update step for the job entity. Now because I am triggering my plug-in with the Update message (and it needs to update a related entity) I had to be careful not to cause a circular infinite update process. This is really a case of of ensuring that I didn't fire asn update of the job entity anywhere in the plug-in code.

The main plug-in method checks it has the right entity type etc.. and then calls another method to update the parent record attributes:

public void Execute(IServiceProvider serviceProvider){ // Obtain the execution context from the service provider. IPluginExecutionContext context = (IPluginExecutionContext) serviceProvider.GetService(typeof(IPluginExecutionContext)); // The InputParameters collection contains all the data passed in the message request. if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity) { // Obtain the target entity from the input parmameters. Entity entity = (Entity)context.InputParameters["Target"]; try { //if it's not the right entity or is not updating the resolutions field, quit if ((entity.LogicalName != "new_autofailinvestigation") || (!entity.Attributes.Contains("new_resolutions"))) { return; } // Obtain the organization service reference. IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId); OptionSetValue resolution = entity.Attributes["new_resolutions"] as OptionSetValue; if (resolution.Value == 100000000/*"Pass On ReRun"*/) { //dictionary to hold the parent record attributes and increment/decrement actions to perform on them Dictionary<string, Int32> updateAttributes = new Dictionary<string, Int32>(); updateAttributes.Add("new_currentpasscount", +1); updateAttributes.Add("new_currentfailcount", -1); UpdateParentAttributes(updateAttributes, entity, service); //deactivate the fail investigation record DeactivateRecord(service, entity.Id, entity.LogicalName); } if (entity.Attributes.Contains("new_psbugid")) { string psBugID = entity["new_psbugid"].ToString(); if (resolution.Value == 100000002 /*"Test Bug"*/) { DeactivateRecord(service, entity.Id, entity.LogicalName); Dictionary<string, Int32> updateAttributes = new Dictionary<string, Int32>(); updateAttributes.Add("new_currenttestbugcount", +1); UpdateParentAttributes(updateAttributes, entity, service); } if (resolution.Value == 100000001 /*"Product Bug"*/) { DeactivateRecord(service, entity.Id, entity.LogicalName); Dictionary<string, Int32> updateAttributes = new Dictionary<string, Int32>(); updateAttributes.Add("new_currentproductbugcount", +1); UpdateParentAttributes(updateAttributes, entity, service); } } } catch (FaultException ex) { throw new InvalidPluginExecutionException("An error occurred in the plug-in.", ex); } catch (KeyNotFoundException ex) { throw new InvalidPluginExecutionException("An error occurred in the plug-in.", ex); } }

}

 

The extra method is mainly to a result of refactoring to use less code to do updates on the parent entity in the different scenarios - hence the use of the dictionary parameter:

 

private void UpdateParentAttributes(Dictionary<string, Int32> updateAttributes, Entity entity, IOrganizationService service) { //build up the query to retrieve the related auto run record associated with this fail record QueryExpression query = new QueryExpression(); query.EntityName = "new_autorun"; query.ColumnSet = new ColumnSet(true); Relationship relationship = new Relationship(); query.Criteria = new FilterExpression(); query.Criteria.AddCondition(new ConditionExpression("statecode", ConditionOperator.Equal, "Active")); relationship.SchemaName = "new_autorun_autofailinvestigation_RelatedRunRecord"; RelationshipQueryCollection relatedEntity = new RelationshipQueryCollection(); relatedEntity.Add(relationship, query); RetrieveRequest request = new RetrieveRequest(); request.RelatedEntitiesQuery = relatedEntity; request.ColumnSet = new ColumnSet(true); request.Target = new EntityReference { Id = entity.Id, LogicalName = entity.LogicalName }; RetrieveResponse response = (RetrieveResponse)service.Execute(request); if ((((RelatedEntityCollection)(response.Entity.RelatedEntities)).Contains(new Relationship("new_autorun_autofailinvestigation_RelatedRunRecord"))) && (((RelatedEntityCollection)(response.Entity.RelatedEntities))[new Relationship("new_autorun_autofailinvestigation_RelatedRunRecord")].Entities.Count > 0)) { Entity relatedRun = ((RelatedEntityCollection)(response.Entity.RelatedEntities))[new Relationship("new_autorun_autofailinvestigation_RelatedRunRecord")].Entities[0]; foreach (string stringAttribute in updateAttributes.Keys) { int attributeCount = Convert.ToInt32(relatedRun.Attributes[stringAttribute]); Int32 addValue = updateAttributes[stringAttribute]; attributeCount+=addValue; relatedRun.Attributes[stringAttribute] = attributeCount; } service.Update(relatedRun); } `` }

This works well and the resolved jobs are removed from the active set. As we start to use this solution I will need to add functionality for reactivations as well as the other housekeeping jobs required for this type of solution, but I think the basics as outlined here are a decent foundation.