I’ve had this request quite a few times over the last year to be able to copy an incident and I wanted to see how hard it would be to do. I started by trying to do it with SMLets and got about 95% of the way there but no matter which way I tried I couldn’t quite get all the way there. I’m either just not enough of a PowerShell expert or there are some limitations in SMLets/SDK that I couldn’t work around. So – I went back to my trusty C#. Don’t get me wrong. I LOVE PowerShell, but there is just something very comforting about C# for me. It just works the way I expect it to. C# is my first love.
Creating a New Object and Copying Object Properties from the Original to the Copy
So – for this project I set it up so that it was a more generic solution that could be used to copy any kind of object. It was pretty straightforward to just loop through all the properties of an object and copy them to a new object. The reality though is that there are subtleties depending on which class of object that you are working with that need to be taken into consideration. For example, if an incident is currently in a Status = Resolved state do you really want to copy that to the new incident record? Probably not. You want it to start out in the active state. You also can’t blindly copy the incident work item ID property over to the new object since that is the key property and you don’t want to end up with two incidents with the same ID (that’s technically not even possible).
On the other hand you want to be able to copy over extended class properties that I don’t even know about right now. Given that requirement, I can’t hardcode which properties to copy over since it would necessarily not include extended class properties.
What to do?
Simple – assume that all extended class properties should be copied over and simply exclude certain properties which I know right now should not be copied over.
So – I created a library of common functions which can be called for copying any kind of object. The first method in that Common class is a function to copy all of the properties from an old object to a new object except a list of properties that are passed in that should not be.
public static CreatableEnterpriseManagementObject CreateNewObjectFromExistingObject(EnterpriseManagementObject emoToBeCopied, ManagementPackClass mpc, string[] strPropertiesToExclude, EnterpriseManagementGroup emg) { //Create a new object to copy property values into CreatableEnterpriseManagementObject cemo = new CreatableEnterpriseManagementObject(emg, mpc); //For each property copy the property value into the new object foreach (ManagementPackProperty property in emoToBeCopied.GetProperties()) { //.. unless it is in the list of properties to NOT copy over if (Array.IndexOf(strPropertiesToExclude, property.Name) == -1) { cemo[property.Id].Value = emoToBeCopied[property.Id].Value; } } return cemo; }
Then right before I call this method I create a list of properties to exclude and pass it in:
string[] strPropertiesToExclude = new string[] { Constants.strPropertyId, Constants.strPropertyCreatedDate, Constants.strPropertyStatus, Constants.strPropertyTargetResolutionTime, Constants.strPropertyResolutionCategory, Constants.strPropertyResolutionDescription, Constants.strPropertyClosedDate }; EnterpriseManagementObjectProjection emopNewIncident = Common.CreateNewObjectProjectionFromExistingObjectProjection(emopIncident, mpcIncident, strPropertiesToExclude , this.EMG);
As you can see there are quite a few properties that should not be copied over for an incident.
Copying Relationships
The next issue to deal with was whether or not to copy over a given relationship. Some of the relationships for an incident are maxcardinality = 1 relationship types like Assigned To User, Affected User, etc. and some are maxcardinality > 1 like Affected Configuration Items.
I ended up treating each of these the same way actually. I basically just took every existing related object and created a new relationship from the new incident to that object like this:
foreach (IComposableProjection icpAffectedUser in emopIncident[mprAffectedUser.Target]) { emopNewIncident.Add(icpAffectedUser.Object, mprAffectedUser.Target); }
In this case it will only loop at most once because there is only one affected user.
In the case of config items, it will loop through for as many configuration items as there are.
foreach (IComposableProjection icpAffectedConfigItem in emopIncident[mprAboutConfigItem.Target]) { emopNewIncident.Add(icpAffectedConfigItem.Object, mprAboutConfigItem.Target); }
After I wrote the code that way though I realized that I really should just create a generic way of handling relationships. So, I created a common method similar to the CreateNewObjectProjectionFromExistingObjectProjection that takes a list of type projection component aliases to ignore. For example – here is the type projection XML for the incident type projection:
<Component Path="$Target/Path[Relationship='CoreIncident!System.WorkItem.IncidentPrimaryOwner']$" Alias="PrimaryOwner" /> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItemAffectedUser']$" Alias="AffectedUser" /> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItemAssignedToUser']$" Alias="AssignedUser" /> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItemCreatedByUser']$" Alias="CreatedByUser" /> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItem.TroubleTicketClosedByUser']$" Alias="ClosedByUser" /> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItem.TroubleTicketResolvedByUser']$" Alias="ResolvedByUser" /> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItem.TroubleTicketHasActionLog']$" Alias="ActionLogs" /> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItem.TroubleTicketHasUserComment']$" Alias="UserComments" /> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItem.TroubleTicketHasAnalystComment']$" Alias="AnalystComments" /> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItem.TroubleTicketHasNotificationLog' TypeConstraint='WorkItem!System.WorkItem.TroubleTicket.SmtpNotificationLog']$" Alias="SMTPNotifications" /> <Component Path="$Target/Path[Relationship='CoreActivity!System.WorkItemContainsActivity' TypeConstraint='CoreActivity!System.WorkItem.Activity.ManualActivity']$" Alias="Activities"> <Component Path="$Target/Path[Relationship='CoreActivity!System.WorkItemContainsActivity' SeedRole='Target']$" Alias="ParentWorkItem" /> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItemCreatedByUser']$" Alias="ActivityCreatedBy" /> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItemAssignedToUser']$" Alias="ActivityAssignedTo" /> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItemAboutConfigItem']$" Alias="ActivityAboutConfigItem" /> </Component> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItemRelatesToWorkItem']$" Alias="RelatedWorkItems"> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItemAffectedUser']$" Alias="RWIAffectedUser" /> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItemAssignedToUser']$" Alias="RWIAssignedUser" /> </Component> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItemRelatesToWorkItem' SeedRole='Target']$" Alias="RelatedWorkItemsSource"> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItemAffectedUser']$" Alias="RWIAffectedUser" /> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItemAssignedToUser']$" Alias="RWIAssignedUser" /> </Component> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItemAboutConfigItem']$" Alias="AffectedConfigItems" /> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItemRelatesToConfigItem']$" Alias="RelatedConfigItems" /> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItemAboutConfigItem' TypeConstraint='System!System.Service']$" Alias="RelatedServiceRequests" /> <Component Path="$Target/Path[Relationship='CoreKnowledge!System.EntityLinksToKnowledgeDocument']$" Alias="RelatedKnowledgeArticles" /> <Component Path="$Target/Path[Relationship='WorkItem!System.WorkItemHasFileAttachment']$" Alias="FileAttachments"> <Component Path="$Target/Path[Relationship='SupportingItem!System.FileAttachmentAddedByUser']$" Alias="FileAttachmentAddedBy" /> </Component>
Notice how each component has an Alias name? Well – there are some relationships that don’t make sense to be copied over to the new object so we can make an array of those that don’t make sense:
//A list of Relationships to exclude was not passed in so we are going to go with a default list. this.RelationshipAliasesToExclude = new string[] { Constants.strAliasCreatedByUser, Constants.strAliasClosedByUser, Constants.strAliasResolvedByUser, Constants.strAliasActionLogs, Constants.strAliasUserComments, Constants.strAliasAnalystComments, Constants.strAliasSMTPNotifications, Constants.strAliasActivities, Constants.strAliasFileAttachments };
Then we can pass those aliases in to our CopyRelationships method:
//Copy all the relationships defined in the type projection, except for those specified Common.CopyRelationships(emopIncident, ref emopNewIncident, mptpIncident, this.RelationshipAliasesToExclude);
And here is what the CopyRelationships method does. It loops through each component of the type projection. If it is not in the list of aliases to exclude then it loops through each related object and creates a new relationship to the same object in the new projection object.
public static void CopyRelationships(EnterpriseManagementObjectProjection emopToBeCopiedFrom, ref EnterpriseManagementObjectProjection emopToBeCopiedTo, ManagementPackTypeProjection mptp, string[] strAliasesToExclude) { foreach (ManagementPackTypeProjectionComponent mptpc in mptp.ComponentCollection) { if (Array.IndexOf(strAliasesToExclude, mptpc.Alias) == -1) { foreach (IComposableProjection icp in emopToBeCopiedFrom[mptpc.TargetEndpoint]) { emopToBeCopiedTo.Add(icp.Object, mptpc.TargetEndpoint); } } } }Sweet!
Setting the Created By User and Relationship Between the Original Incident and the Incident Copy
Now, here is an interesting case… what to do about the created by user relationship. We don’t want to copy that over since we want to set it to whoever the current user is that is doing the copying not to who created the incident that we are copying from right?
So – to do that I created a common helper method to get the current user and return it as an EnterpriseManagementObject:
public static EnterpriseManagementObject GetLoggedInUserAsObject(EnterpriseManagementGroup emg) { EnterpriseManagementObject emoUserToReturn = null; string strUserName = System.Environment.UserName; string strDomain = System.Environment.UserDomainName; string strUserByUserNameAndDomainCriteria = string.Format("{0} = '{1}' AND {2} = '{3}'", Constants.strPropertyUserName, strUserName, Constants.strPropertyDomain ,strDomain); ManagementPackClassCriteria mpccUser = new ManagementPackClassCriteria(String.Format("{0} = '{1}'", Constants.strMPAttributeName, Constants.strClassUser)); ManagementPackClass mpcUser = null; foreach(ManagementPackClass mpc in emg.EntityTypes.GetClasses(mpccUser)) { //There should be only one mpcUser = mpc; } EnterpriseManagementObjectCriteria emocUserByUserNameAndDomain = new EnterpriseManagementObjectCriteria(strUserByUserNameAndDomainCriteria, mpcUser); IObjectReader<EnterpriseManagementObject> emoUsers = emg.EntityObjects.GetObjectReader<EnterpriseManagementObject>(emocUserByUserNameAndDomain, ObjectQueryOptions.Default); foreach (EnterpriseManagementObject emoUser in emoUsers) { //There will be only one if any emoUserToReturn = emoUser; } return emoUserToReturn; }
Then I just add the returned user EnterpriseManagementObject in the same way I did with the other relationships:
//Set CreatedByUser to be the user that is logged in EnterpriseManagementObject emoCreatedByUser = Common.GetLoggedInUserAsObject(this.EMG); if (emoCreatedByUser != null) { emopNewIncident.Add(emoCreatedByUser, mprCreatedByUser.Target); }
The last thing I did is just wrote a one liner to add the incident that we are copying to as a related work item to the incident that we are creating:
//Relate the original incident to the new one
emopNewIncident.Add(emopIncident.Object, mprWorkItemRelatesToWorkItem.Target);
That’s pretty much it. At the end, the new incident object projection is committed to the database like this:
emopNewIncident.Commit();
Using the Solution in a Console Task or on the Command Line
Now, what I did is wrap up all this logic in a C# code class called Incident. That can then be instantiated either from a simple command line program or from a console task.
Command line example:
Incident incident = new Incident(); incident.IDToCopy = args[0]; incident.EMG = Common.GetManagementGroupConnectionFromRegistry(); string strWorkItemID = incident.Copy(); if (strWorkItemID != null) { Console.WriteLine(String.Format("{0} copied to {1}", args[0], strWorkItemID)); }
Console task example:
public override void ExecuteCommand(IList<NavigationModelNodeBase> nodes, NavigationModelNodeTask task, ICollection<string> parameters) { foreach (NavigationModelNodeBase node in nodes) { if(parameters.Contains("Incident")) { Incident incident = new Incident(); incident.IDToCopy= node["Id"].ToString(); incident.EMG = incident.EMG = Common.GetManagementGroupConnectionFromRegistry(); string strWorkItemID = incident.Copy(); } } }
So – now I can use this from the command line like this:
or in the UI like this:
I’ve already discussed how to create console tasks like this before so I wont go into detail on that here, but I did have someone ask me how to create console task icons. I’ll explain that next.
Creating Console Task Icons
First you need to find or create a 16x16 .pixel png file. Then you need so save that file in the same folder as your MP .xml file.
Then you need to declare the image file in your MP in the Resources section at the very end of the management pack (after the LanguagePacks section even):
<Resources> <Assembly ID="ConsoleTaskAssembly" Accessibility="Public" FileName="CopyObject.dll" HasNullStream="false" QualifiedName="CopyObject" /> <Image ID="Copy" Accessibility="Public" FileName="Copy_16x16.png" HasNullStream="false" /> </Resources> </ManagementPack>
You just need to give your image an ID and put the file name in FileName.
Then you need to make an association in your MP XML file between the console task and the image in the Presentation section of the MP XML:
<Presentation> <ConsoleTasks> <ConsoleTask ID="CopyIncident" Accessibility="Public" Enabled="true" Target="IncidentLibrary!System.WorkItem.Incident" RequireOutput="false"> <Assembly>Console!SdkDataAccessAssembly</Assembly> <Handler>Microsoft.EnterpriseManagement.UI.SdkDataAccess.ConsoleTaskHandler</Handler> <Parameters> <Argument Name="Assembly">CopyObject</Argument> <Argument Name="Type">CopyObject.ConsoleTask</Argument> <Argument>Incident</Argument> </Parameters> </ConsoleTask> </ConsoleTasks> <ImageReferences> <ImageReference ElementID="CopyIncident" ImageID="Copy" /> </ImageReferences> </Presentation>
Notice how the ElementID corresponds to the ConsoleTask ID attribute value and the ImageID corresponds to the Image ID attribute value.
Making Tasks Multi-Select Capable
Just for fun I also made sure that this task supports multi-select. To do that I needed to do two things:
1) Add a Category in the management pack so that the task will show up when multiple objects are selected in a view:
<Category ID="Category.CopyIncidentCommandMultiselect" Target="CopyIncident" Value="Console!Microsoft.EnterpriseManagement.ServiceManager.UI.Console.MultiSelectTask" />This category simply needs to target the console task ID and point to the Microsoft.EnterpriseManagement.ServiceManager.UI.Console.MultiSelectTask EnumerationValue in the Microsoft.EnterpriseManagement.ServiceManager.UI.Console
management pack.
2) In the console task handler ExecuteCommand override I looped through each of the NavigationalModelNodeBase objects in the nodes collection. This is highlighted above.
Lastly you need to create a management pack bundle. For more information on that see this blog post:
Where to get Copy Object
I have uploaded this solution to CodePlex as a reference implementation. You are more than welcome to use the solution in your own environment, but please be aware that this is not something that is officially supported by Microsoft support. You can file feature requests (including other classes that you would like to see copy support for) and bugs on the CodePlex site on the Issue Tracker section and I will do what I can to address them.