Condividi tramite


Transforming Incidents to other WorkItem types via Console Tasks in Service Manager (SCSM Dev custom Solutions)

The entire solution (source code) is available here:  https://github.com/SubZer0MS/TransformWorkItemTasks

I have been asked to create a solution for Service Manager 2012 R2 that contains Console Tasks that allow users us to transform an Incident into a different WorkItem type. In this example, we are going to have 4 Console Tasks, for each of the other 4 WorkItem types beside Incident (Service Request, Change Request, Release Record, Problem). This way, we can assign permissions only to some or only for the one needed for a specific role like (ex. custom - derived from Service Request Analyst role).

This is a pretty nifty example that shows how such a thing can be done and that can be directly used, or stand as a base solution that can be changed or extended as needed :D

This solution will do the following (based on the Task being executed) - example for Service Request ("Transform to SR" Task):

  1. Create a new Service Request using the Default Service Request Template (uses the default/standard Template for each WorkItem type)
  2. Add the Title & Description of the Incident to the Service Request
  3. Add the common WorkItem relationships to the Service Request & delete these relationships from the Incident (less data in DB, better performance)
  4. Add the Incident to the Service Request as related WorkItem (WorkItemRelatesToWorkItem)
  5. Create a new AnalystCommentLog and add it to the Incident as a related AnalystCommentLog containing a comment that the Incident was closed by this transform Task and also containing the new Service Request ID
  6. Close the Incident (set the Status to Closed)

As for permissions needed, it either needs to be run by users of the Advanced Operators role, or this can be configured granular and would need the following permissions:

  • close Incidents and add comment on closing (setting Status to Closed, creating AnalystCommentLog and adding it to the Incident as related AnalystCommentLog)
  • creating and editing the WorkItem of the specific Task being executed (ex. if the "Transform to SR" is being called for example, then Create/Edit permissions are needed for Service Requests)

 

The Management Pack Bundle that contains the solution and can be directly imported into Service Manager 2012 R2 is here: TransformWorkItemTasks.mpb (zip)

The entire solution (source code) is available here:  https://github.com/SubZer0MS/TransformWorkItemTasks

 

Let's take a little tour on this with an example of one of the Tasks (let's continue with the "Transform to SR" Task as I have been using it as example up to this point anyway).

All the needed references are already included in the solution folders, so no need to copy/add any DLLs or MPs that are referenced. It is however, a good thing to look at, in order to see what references are being used.

This is how a new Console Task should be declared (under <Presentation> node, under <ConsoleTasks> ):

 <ConsoleTask ID="TransformIncidentToServiceRequest" Accessibility="Public" Enabled="true" Target="Incident!System.WorkItem.Incident" RequireOutput="false">
  <Assembly>Console!SdkDataAccessAssembly</Assembly>
  <Handler>Microsoft.EnterpriseManagement.UI.SdkDataAccess.ConsoleTaskHandler</Handler>
  <Parameters>
    <Argument Name="Assembly">CustomTransformWorkItemTasks</Argument>
    <Argument Name="Type">CustomTransformWorkItemTasks.TransformTaskHandler</Argument>
    <Argument>Service</Argument>
  </Parameters>
</ConsoleTask>

By setting the Target to System.WorkItem.Incident, we tell SM to display this Task in views where we are displaying Incidents (when selecting an Incident).

We are declaring that the Task Handler will be the default Microsoft.EnterpriseManagement.UI.SdkDataAccess.ConsoleTaskHandler, and we are passing 3 arguments to this Method:

  1. Assembly (needed named argument that allows the Handler to know what DLL it needs to use for this): CustomTransformWorkItemTasks
  2. Type (needed named argument that tells the Handler which is the class from our DLL that is handling this): CustomTransformWorkItemTasks.TransformTaskHandler
  3. A 3rd unnamed argument, which is actually the 1st argument that we are passing to our method (CustomTransformWorkItemTasks.TransformTaskHandler) which is a simple string for which we are manually checking if it is set in our method: Service

In the DLL we are developing for this, we are defining our Handler like this:

  • note that the (main) namespace is called CustomTransformWorkItemTasks like our DLL will be called as well
  • also notice, that we define a class in this namespace called TransformTaskHandler that extends the (SM build-in Microsoft.EnterpriseManagement.UI.SdkDataAccess. ConsoleCommand class)
 namespace CustomTransformWorkItemTasks
{
    public class TransformTaskHandler : ConsoleCommand
    {

The build-in method ExecuteCommand of the ConsoleCommand class from which we are inheriting from, which will get executed on initialization, is called , so we have to define that in our class and handle the arguments (in this example the only one argument with the value: Service) we pass from the MP there:

 public override void ExecuteCommand(IList nodes, NavigationModelNodeTask task, ICollection parameters)
{

In this method, we decide what we do based on what argument we have sent from the Task (in the MP) - in this case we only pass the argument Service:

 if (parameters.Contains("Service"))
{
      // do stuff here - the actual work you need to be done in your task when this argument is passed by the Task
      // in this case, I am setting some variables that decide what I will create later in the actual processing based on the WorkItem type (check the entire source code on GitHub)  
}

This is the part does the actual magic. Notice the foreach block, we are going through all the selected Incidents here, that were selected when running the Task in the Console by enumerating through the nodes list passed as argument to the ExecuteCommand method:

  • notice that we are using EnterpriseManagementObjectProjection  with Class Type constructor in order to be able to create Class & Relationship objects together
  • we are just adding the Relationships we create to the class object, by adding it to the EnterpriseManagementObjectProjection  class using the Add(...)  Method
  • in order to "really" save/create the Class & Relationship objects of this projection into the CMDB, which we are currently only having in memory, we now need to call Overwrite() Method on the projection
  • when we create the AnalystCommentLog, we are using  CreatableEnterpriseManagementObject because it's an object that does not exist yet and we are just creating it, but we don't need to use a projection ( EnterpriseManagementObjectProjection ) because we will not create & add any Relationship to it (we are just adding the newly object itself as a related object to the new Service Request)
  • we need to make sure to set the correct Status for the new WorkItem in order for the internal workflows to correctly process it in order for it to get in progress (Status will be New or Active depending on the WorkItem type)
  • it is also very important to set the Prefixes for each Activity (depending on the type) that exists in the WorkItem Template we apply (which in this case is the default Template for each WorkItem type)
  • another special case we need to worry about, is when we are cloning the Relationships - if the RelationshipType is derived from System.Membership, it is very important to create a brand new Target object copied from the existing one (new BaseManagedEntityId) because Relationships of this type (Membership) have a Target object which is bound to it's source (it cannot have a Relationship to 2 different objects)
  • all the MP, Class, etc. names are defined in a separate CS file called Constants when different classes are defined - check the actual source code on GitHub to understand those
  • check the comments for more detailed explanations
  // here is the code that does the actual work<br>// we wrap this around a try/catch block to handle any exception that may happen and display it in case of failure

try
{
     // getting the types we need based on the string variables we have filled earlier based on the WorkItem type we need to create

    ManagementPack workItemMp = emg.GetManagementPack(workItemMpName, Constants.mpKeyTocken, Constants.mpSMR2Version);
    ManagementPack mpSettings = emg.GetManagementPack(workItemSettingMpName, Constants.mpKeyTocken, Constants.mpSMR2Version);
    ManagementPack knowledgeLibraryMp = emg.GetManagementPack(ManagementPacks.knowledgeLibrary, Constants.mpKeyTocken, Constants.mpSMR2Version);

    ManagementPackClass workItemClass = emg.EntityTypes.GetClass(workItemClassName, workItemMp);
    ManagementPackClass workItemClassSetting = emg.EntityTypes.GetClass(workItemSettingClassName, mpSettings);

    EnterpriseManagementObject generalSetting = emg.EntityObjects.GetObject<EnterpriseManagementObject>(workItemClassSetting.Id, ObjectQueryOptions.Default);

     // here is the foreach loop that processes each class-object (in this case Incident) that was multi-selected in the view before executing the Task

    foreach (NavigationModelNodeBase node in nodes)
    {
         // we need to setup an IList which contains only 1 GUID that correspons to the Incident we are currently working on (node["Id"])<br>        // this is needed because we are using the IObjectProjectionReader.GetData(...) which gets an IList<Guid> as parameter in order to retreive the class-objects we want from the db

        IList<Guid> bmeIdsList = new List<Guid>();
        bmeIdsList.Add(new Guid(node[Constants.nodePropertyId].ToString()));

         // we are setting up the ObjectProjectionCriteria using the "System.WorkItem.Incident.ProjectionType" Type Projection as we need to get all the Relationships of the Incident<br>        // we will use ObjectRetrievalOptions.Buffered so that we don't get any data from the db which we don't need - we will only get the data one we call the IObjectProjectionReader.GetData(...) method<br>        // we are getting the data reader object using GetObjectProjectionReader(...) and setting its PageSize to 1 because we only need 1 object retrieved here

        ObjectProjectionCriteria incidentObjectProjection = new ObjectProjectionCriteria(incidentProjection);
        ObjectQueryOptions queryOptions = new ObjectQueryOptions(ObjectPropertyRetrievalBehavior.All);
        queryOptions.ObjectRetrievalMode = ObjectRetrievalOptions.Buffered;
        IObjectProjectionReader<EnterpriseManagementObject> incidentReader = emg.EntityObjects.GetObjectProjectionReader<EnterpriseManagementObject>(incidentObjectProjection, queryOptions);
        incidentReader.PageSize = 1;

         // we are using EnterpriseManagementObjectProjection for the Incident we are getting from the db instead of EnterpriseManagementObject<br>        // this is because we are getting a (Type) Projection object (class-object together with its Relationships & relationship class-objects

        EnterpriseManagementObjectProjection incident = incidentReader.GetData(bmeIdsList).FirstOrDefault();

         // we are doing the same for the new WorkItem class-object we are creating because we want to add Relationships (with their relationship class-objects from the Incident) here as well<br>        // if we would only have created the new WorkItem class and nothing else with it (no Relationships), we could have used the CreatableEnterpriseManagementObject class (which needs to be used when a new class-object is getting created) 

        EnterpriseManagementObjectProjection workItem = new EnterpriseManagementObjectProjection(emg, workItemClass);

         // now we need to assign some Template to the new WorkItem (if a default/standard Template exists) in order to already have some Activities created<br>        // the Activities and all other Properties of the new WorkItem can be adjusted by modifying the default/standard Template for each WorkItem type

        if (!string.IsNullOrEmpty(workItemTemplateName))
        {
            ManagementPackObjectTemplateCriteria templateCriteria = new ManagementPackObjectTemplateCriteria(string.Format("Name = '{0}'", workItemTemplateName));
            ManagementPackObjectTemplate template = emg.Templates.GetObjectTemplates(templateCriteria).FirstOrDefault();

            if(template != null)
            {
                 // if a Template with this name exists, we apply it to the new WorkItem by calling ApplyTemplate(...) on it

                workItem.ApplyTemplate(template);

                 // if we have a Template, we also need to process each Activity that it contains in order to set the Prefix for each Activity (based on its type)<br>                // we are using the activityPrefixMapping variable we defined above in oder to map each Prefix based on each Activity class-type

                ManagementPack activityManagementMp = emg.GetManagementPack(ManagementPacks.activityManagementLibrary, Constants.mpKeyTocken, Constants.mpSMR2Version);
                ManagementPackRelationship workItemContainsActivityRelationshipClass = emg.EntityTypes.GetRelationshipClass(RelationshipTypes.workItemContainsActivity, activityLibMp);
                ManagementPackClass activitySettingsClass = emg.EntityTypes.GetClass(ClassTypes.activitySettings, activityManagementMp);

                EnterpriseManagementObject activitySettings = emg.EntityObjects.GetObject<EnterpriseManagementObject>(activitySettingsClass.Id, ObjectQueryOptions.Default);

                 // for each Activity that exists in the Template we applied, we are going to get its Prefix setting and apply it to its ID in the format: "PREFIX{0}"<br>                // "{0}" is the string pattern we need to set for any new WorkItem (including Activity) class-object we are creating as this will be replaced by the next ID available for the new WorkItem

                foreach (IComposableProjection activity in workItem[workItemContainsActivityRelationshipClass.Target])
                {
                    ManagementPackClass activityClass = activity.Object.GetClasses(BaseClassTraversalDepth.None).FirstOrDefault();
                    string prefix = activitySettings[null, activityPrefixMapping[activityClass.Name]].Value as string;
                    activity.Object[null, ActivityProperties.Id].Value = string.Format("{0}{1}", prefix, Constants.workItemPrefixPattern);
                }
            }
        }

         // we are setting the Properties for the new WorkItem class-object here from some Properties of the inital Incident (add more as needed)<br>        // it is of highest importance that we also set its status to New/Active (depending on WorkItem class-type) in order for it to be properly processed by the internal workflows on creation<br>        // if we don't set the the correct "creation" Status here, it will never be able to progress into a working state and will remain stuck in a "pending" state

        ManagementPackEnumerationCriteria workItemStatusNewEnumCriteria = new ManagementPackEnumerationCriteria(string.Format("Name = '{0}'", workItemStatusName));
        ManagementPackEnumeration workItemStatusNew = emg.EntityTypes.GetEnumerations(workItemStatusNewEnumCriteria).FirstOrDefault();

        workItem.Object[workItemClass, WorkItemProperties.Id].Value = string.Format("{0}{1}", generalSetting[workItemClassSetting, workItemSettingPrefixName], Constants.workItemPrefixPattern);
        workItem.Object[workItemClass, WorkItemProperties.Title].Value = string.Format("{0} ({1})", incident.Object[incidentClass, WorkItemProperties.Title].Value, incident.Object[incidentClass, WorkItemProperties.Id].Value);
        workItem.Object[workItemClass, WorkItemProperties.Description].Value = incident.Object[incidentClass, WorkItemProperties.Description].Value;
        workItem.Object[workItemClass, WorkItemProperties.Status].Value = workItemStatusNew.Id;

         // due to the fact that the Problem WorkItem does not have any Template we can use to create it, we need to handle this special case<br>        // we need to populate all the required fields when creating the Problem WorkItem, or creating it will fail (Urgency, Impact, Category) 

        if (!string.IsNullOrEmpty(workItemUrgencyName))
        {
            ManagementPackEnumerationCriteria workItemUrgencyEnumCriteria = new ManagementPackEnumerationCriteria(string.Format("Name = '{0}'", workItemUrgencyName));
            ManagementPackEnumeration workItemUrgency = emg.EntityTypes.GetEnumerations(workItemUrgencyEnumCriteria).FirstOrDefault();
            workItem.Object[workItemClass, WorkItemProperties.Urgency].Value = workItemUrgency.Id;
        }

        if (!string.IsNullOrEmpty(workItemImpactName))
        {
            ManagementPackEnumerationCriteria workItemImpactEnumCriteria = new ManagementPackEnumerationCriteria(string.Format("Name = '{0}'", workItemImpactName));
            ManagementPackEnumeration workItemImpact = emg.EntityTypes.GetEnumerations(workItemImpactEnumCriteria).FirstOrDefault();
            workItem.Object[workItemClass, WorkItemProperties.Impact].Value = workItemImpact.Id;
        }

        if (!string.IsNullOrEmpty(workItemCategoryName))
        {
            ManagementPackEnumerationCriteria workItemCategoryEnumCriteria = new ManagementPackEnumerationCriteria(string.Format("Name = '{0}'", workItemCategoryName));
            ManagementPackEnumeration workItemCategory = emg.EntityTypes.GetEnumerations(workItemCategoryEnumCriteria).FirstOrDefault();
            workItem.Object[workItemClass, WorkItemProperties.Category].Value = workItemCategory.Id;
        }

         // we are adding the initial Incident to this new WorkItem as related WorkItem (System.WorkItemRelatesToWorkItem) 

        ManagementPackRelationship workItemToWorkItemRelationshipClass = emg.EntityTypes.GetRelationshipClass(RelationshipTypes.workItemRelatesToWorkItem, wiLibraryMp);
        workItem.Add(incident.Object, workItemToWorkItemRelationshipClass.Target);

         // we are closing the current Incident by setting its Status to Closed and setting a closed date

        incident.Object[incidentClass, IncidentProperties.Status].Value = incidentClosedStatus.Id;
        incident.Object[incidentClass, IncidentProperties.ClosedDate].Value = DateTime.Now.ToUniversalTime();

         // we create a new (analyst) comment (System.WorkItem.TroubleTicket.AnalystCommentLog) and we add it to the Incident in oder to comment the fact that it was closed tue to this WorkItem Transfomr Task

        CreatableEnterpriseManagementObject analystComment = new CreatableEnterpriseManagementObject(emg, analystCommentClass);
        analystComment[analystCommentClass, AnalystCommentProperties.Id].Value = Guid.NewGuid().ToString();
        analystComment[analystCommentClass, AnalystCommentProperties.Comment].Value = string.Format(Constants.incidentClosedComment, workItemClass.Name, workItem.Object.Id.ToString());
        analystComment[analystCommentClass, AnalystCommentProperties.EnteredBy].Value = EnterpriseManagementGroup.CurrentUserName;
        analystComment[analystCommentClass, AnalystCommentProperties.EnteredDate].Value = DateTime.Now.ToUniversalTime();

        ManagementPackRelationship incidentHasAnalystCommentRelationshipClass = emg.EntityTypes.GetRelationshipClass(RelationshipTypes.workItemHasAnalystComment, wiLibraryMp);
        incident.Add(analystComment, incidentHasAnalystCommentRelationshipClass.Target);

         // we create an IList of RelationshipTypes we want to transfer from the Incident to the new WorkItem<br>        // this is the place we can add any new/custom RelationshipTypes which we want to transfer<br>        // just make sure that the RelationshipType can be transfered from an Incident to any other WorkItem class-type

        IList<ManagementPackRelationship> relationshipsToAddList = new List<ManagementPackRelationship>()
        {
            workItemToWorkItemRelationshipClass,
            emg.EntityTypes.GetRelationshipClass(RelationshipTypes.createdByUser, wiLibraryMp),
            emg.EntityTypes.GetRelationshipClass(RelationshipTypes.affectedUser, wiLibraryMp),
            emg.EntityTypes.GetRelationshipClass(RelationshipTypes.assignedToUser, wiLibraryMp),
            emg.EntityTypes.GetRelationshipClass(RelationshipTypes.workItemHasAttachment, wiLibraryMp),
            emg.EntityTypes.GetRelationshipClass(RelationshipTypes.workItemAboutConfigItem, wiLibraryMp),
            emg.EntityTypes.GetRelationshipClass(RelationshipTypes.workItemRelatesToConfigItem, wiLibraryMp),
            emg.EntityTypes.GetRelationshipClass(RelationshipTypes.entityToArticle, knowledgeLibraryMp),
            emg.EntityTypes.GetRelationshipClass(RelationshipTypes.workItemHasCommentLog, wiLibraryMp),
        };

         // we are getting an instance of the "System.Membership" RelationshipType as we need to handle RelationshipTypes derived from it as a special case<br>        // the reason for this, is that Target class-objects of the "System.Membership" RelationshipType are bound to their Source class-objects<br>        // being bound, means that Target class-objects of membership relationships cannot belong to 2 different (Source) class-objects<br>        // because of this, we need to make a copy (using "CreatableEnterpriseManagementObject" to create a new object and copying the Properties) of the existing Target class-object<br>        // and add that to the new WorkItem instead of adding the already existing Target class-object

        ManagementPack systemLibraryMp = emg.GetManagementPack(ManagementPacks.systemLibrary, Constants.mpKeyTocken, Constants.mpSMR2Version);
        ManagementPackRelationship membershipRelationshipClass = emg.EntityTypes.GetRelationshipClass(RelationshipTypes.membership, systemLibraryMp);

         // we are going through each Target & Source Relationships of the Incident as defined in the relationshipsToAddList variable and adding them to the new WorkItem<br>        // we are handling the Target RelationshipTypes which are derived from "System.Membership" as a special case as explained above<br>        // notice that we are also removing these Relationships from the Incident by calling Remove()<br>        // we are removing the Relationships from the Incident for performance purposes - in order to have less Relationships (less data) in the db<br>        // comment the "itemProjection.Remove();" in order to keep the Relationships to the Incident as well if needed for some reason

        foreach (ManagementPackRelationship relationship in relationshipsToAddList)
        {
            if (incident[relationship.Target].Any())
            {
                foreach (IComposableProjection itemProjection in incident[relationship.Target])
                {
                     // create a new Target class-object (CreatableEnterpriseManagementObject) and add it to the projection as it is a member of a Membership RelationshipType (as explained above)<br>                    // notice that we DON'T remove such a Target class-object Relationship because it will also remove the class-object itself (because it is a Membership RelationshipType object and it cannot exist without this Relationship)<br>                    // we need it to exist because we are copying data from it and it needs to still exist in the db (ex. Attachments - we still need the binary data to exist in the db when we create the new Attachment object)<br>                    // we could of course delete it after we create the new WorkItem with its Relationships when calling "workItem.Overwrite()", but I chose not to do it

                    if (relationship.IsSubtypeOf(membershipRelationshipClass))
                    {
                        CreatableEnterpriseManagementObject instance = new CreatableEnterpriseManagementObject(emg, itemProjection.Object.GetClasses(BaseClassTraversalDepth.None).FirstOrDefault());
                        foreach (ManagementPackProperty property in itemProjection.Object.GetProperties())
                        {
                            instance[property.Id].Value = itemProjection.Object[property.Id].Value;
                        }

                        instance[null, Constants.entityId].Value = Guid.NewGuid().ToString();

                        workItem.Add(instance, relationship.Target);
                    }

                     // just add the existing Target object-class as it is not a member of a Membership RelationshipType (as explained above) 

                    else
                    {
                        workItem.Add(itemProjection.Object, relationship.Target);
                        itemProjection.Remove();
                    }
                }
            }

            if(incident[relationship.Source].Any())
            {
                 // we just create the new Relationship of the Source class-object to the new WorkItem because this is not affected by the Membership RelationshipType

                foreach (IComposableProjection itemProjection in incident[relationship.Source])
                {
                    workItem.Add(itemProjection.Object, relationship.Source);
                    itemProjection.Remove();
                }
            }
        }

         // this is where we actually save (write) the new data to the db, when calling "Overwrite()" - here saving the Incident we modified (set Status to Closed & deleted Relationships)<br>        // before we have just created the new objects and relationships in-memory<br>        // this is also the point when almost all of the code validation is being done<br>        // if there are any issues really creating/editing/adding these objects/realtionships, this is where we would get the errors

        incident.Overwrite();

         // we are want to handle the error here of saving the new WorkItem and its Relationships because we want to re-open the Incident in case there is an issue when creating the new WorkItem

        try
        {
             // this is where we actually save (write) the new data to the db, when calling "Overwrite()" - here saving the new WorkItem we created with its Relationships we added (from the Incident) 

            workItem.Overwrite();
        }
        catch (Exception ex)
        {
             // if we faild to create the new WorkItem with its Relationships, we want to revert to setting the Incident to an Active Status (we re-open the Incident) 

            ManagementPackEnumerationCriteria incidentActiveEnumCriteria = new ManagementPackEnumerationCriteria(string.Format("Name = '{0}'", EnumTypes.incidentStatusActive));
            ManagementPackEnumeration incidentActiveStatus = emg.EntityTypes.GetEnumerations(incidentActiveEnumCriteria).FirstOrDefault();

            incident.Object[incidentClass, IncidentProperties.Status].Value = incidentActiveStatus.Id;
            incident.Object[incidentClass, IncidentProperties.ClosedDate].Value = null;

             // again, after applying the new modifications in memory, we need to actually write them to the db using "Overwrite()" 

            incident.Overwrite();

             // no need to show this because we are just passing it (throwing) to the wrapped try/catch block so it will be displayed and handled there

            throw ex;
        }
    }

     // if everything succeeds, we want to refresh the View (in this case, some View that shows Incidents as this is where we are calling our Task from)<br>    // we want to refresh the view to show the new Status of the Incient (as Closed in this case) - if the View only shows non-Closed Incidents, it will dissapear from the View

    RequestViewRefresh();
}
catch(Exception ex)
{
     // we want to handle all Exceptions here so that the Console does not crash<br>    // we also want to show a MessageBox window with the Exception details for troubleshooting purposes

    MessageBox.Show(string.Format("Error: {0}: {1}\n\n{2}", ex.GetType().ToString(), ex.Message, ex.StackTrace));
}

 

Have fun coding! :D

Comments

  • Anonymous
    October 28, 2016
    Great stuff! Looking forward to give it a spin. Thanks Mihai!
  • Anonymous
    March 17, 2017
    AWESOME WORK! Thank you.