Changing Components’ Schemas With Core Service

Besides making new extensions and applications to extend the features of Tridion, sometimes the Core Service API is a great tool for scripting something that would otherwise take a long time to do manually. A colleague of mine is in need of such a script, and has never worked with the Core Service API before, so I thought I would write this tutorial to help him get on the right track.

The project he’s in has the need to change the schema for a bunch of multi-media components. The GUI does not provide a way to change the schema, as changing schemas for components could lead to data loss. For example, if your metadata values have different names than the schema that you are switching to, data in those fields would be gone. Fortunately for my colleague, there is no metadata on the old schemas.

As my colleague is somewhat new to the world of .NET as well, this tutorial may explain some of the basics of .NET and Visual Studio as well.

The Project Setup

To start, create a new .NET Console Application project in Visual Studio (for this tutorial, I am using Visual Studio 2010 Professional Edition). Lets call it “Examples.ComponentSchemaChanger”. Visual Studio will create a new project with a Program class that contains a static void Main(string[] args) method. This Main method is the entry point for your console application. The args parameter is arguments that you can pass to your application from the command line.

Next, create a folder in Windows in the Solution folder that was created called “Resources”. Here we’ll include all referenced assemblies that our project will use. Since this is a fairly simple script, only one will be needed.

On a Tridion server (or VM), from the /bin/client/ directory of the location where Tridion is installed. Copy “Tridion.ContentManager.CoreService.Client.dll.config” and “Tridion.ContentManager.CoreService.Client.dll” from this location to the Resources folder you created in the previous step. The DLL contains the Core Service API that you will use for this script, and the config contains the configuration bindings that your application will use to connect to Tridion.

Next, lets add the reference to the DLL. Right click the References folder in your Visual Studio project, and select “Add Reference”. Select the “Browse” tab, and then browse to your Resources folder. Select “Tridion.ContentManager.CoreService.Client.dll” and click “Ok”.

You’ll also need to add several of other references. Right click and add references, but this time select the “.NET” tab and add references to “System.Configuration”, “System.ServiceModel” and “System.Runtime.Serialization”.

Now that we have referenced the Core Service library, lets modify the configuration. Right click the project, and select “Add -> New Item”. Under the General section, select “Application Configuration File”. Next copy the contents of “Tridion.ContentManager.CoreService.Client.dll.config” to your “App.config” folder. Right underneath the <configuration> element in your App.config file, add the following:

  <appSettings>
    <add key="CoreServiceEndpoint" value="netTcp_2011" />
    <add key="TridionUsername" value="YOUR TRIDION USERNAME" />
    <add key="TridionPassword" value="YOUR TRIDION PASSWORD"/>
  </appSettings>

The above will let you set the username and password that your script will use to authenticate with the Core Service, as well as which endpoint name to use (more on that later).

The SchemaChanger Class

Let’s add a new class to the project called “SchemaChanger”. Right click the project, click “Add -> Class” and then give it the name “SchemaChanger”.

Now add the following field to your new class:

private SessionAwareCoreServiceClient _client;

You’ll see an error squiggly line and a message of “The type or namespace name ‘SessionAwareCoreServiceClient’ could not be found (are you missing a using directive or an assembly reference?)”. This is because we have not referenced the namespace in a using statement. Let’s let Visual Studio do this automatically for us. Click the squiggly lined “SessionAwareCoreServiceClient”, and when the down arrow appear, click that, and then “using Tridion.ContentManager.CoreService.Client;“. You’ll see the using statement automatically added to the top, and your error message has gone away. For the rest of this tutorial, do the same to any similar errors you encounter to automatically add the the namespaces with the using directive.

Next, let’s add a property to our class that will use a getter accessor for getting our client.

public SessionAwareCoreServiceClient Client
{
    get
    {
        if (_client == null)
        {
            string endpointName = ConfigurationManager.AppSettings["CoreServiceEndpoint"];
            if (String.IsNullOrEmpty(endpointName))
            {
                throw new ConfigurationErrorsException("CoreServiceEndpoint missing from appSettings");
            }

            _client = new SessionAwareCoreServiceClient(endpointName);

            string username = ConfigurationManager.AppSettings["TridionUsername"];
            string password = ConfigurationManager.AppSettings["TridionPassword"];

            if (!String.IsNullOrEmpty(username) && !String.IsNullOrEmpty(password))
            {
                _client.ClientCredentials.Windows.ClientCredential = new NetworkCredential(username, password);
            }
        }
        return _client;
    }
}

What the above is doing is, if our _client variable is null, to create a new instance of SessionAwareCoreSerivceClient using the endpoint that we supply in our appSettings. If _client has already been instanciated, it’ll return it without creating a new instance. We’ll also be using the TridionUsername and TridionPassword appSettings that we set up earlier. If the appConfig does not have these settings, or if they are empty, the application will attempt to use the user that will be running the script. It also checks to make sure the CoreServiceEndpoint appSetting exists, and throws an error if its missing.

Next, lets add a method that’s going to be doing the work for us. Let’s call this method “ChangeSchemasForComponentsInFolder”, and allow it to take 4 arguments: one for the TcmUri of the folder we want to search in, one to allow the operation to happen recursively, the third one for the TcmUri of the schema that we want to change from, and the forth one for the TcmUri of the schema that we want to change the components to.

public void ChangeSchemasForComponentsInFolder(string folderUri, bool recursive, string fromSchemaUri, string schemaUriToChangeTo)
{

}

We can only modify components that aren’t already checked out, so lets create a variable that will keep track of components that we are not able to edit.

    List<ComponentData> failedItems = new List<ComponentData>();

Next we’ll want to open the folder that we are going to search in. We can use the CoreService client’s Read method for this.

    FolderData folder = Client.Read(folderUri, null) as FolderData;

We’ll also want to grab the Schema that we’ll be switching to, and the namespace of that schema.

    SchemaData schema = Client.Read(schemaUriToChangeTo, null) as SchemaData;
    XNamespace ns = schema.NamespaceUri;

Now we’ll create a filter to actually query for multimedia items. We’ll want to make sure to only grab Components, and to only grab components of a Multimedia type. We’ll also want to check for components recursively if we’ve supplied to do so in the passed argument.

    OrganizationalItemItemsFilterData filter = new OrganizationalItemItemsFilterData();
    filter.ItemTypes = new ItemType[] { ItemType.Component };
    filter.ComponentTypes = new ComponentType[] { ComponentType.Multimedia };
    filter.Recursive = recursive;

And finally the actual work of switching the schema of the component. We’ll open up the component in read mode first, and only check it out to modify if the component’s current schema ID matches the schema we want to change from. If the component doesn’t match, we’ll attempt to check it out. If we are successful on checking it out, we’ll change its schema, save, and check back in. If we weren’t successful on checking the item out, we’ll make note of that item to report at the end.

    XElement items = Client.GetListXml(folder.Id, filter);
    foreach (XElement item in items.Elements())
    {
        ComponentData component = Client.Read(item.Attribute("ID").Value, null) as ComponentData;

        if (!component.Schema.IdRef.Equals(fromSchemaUri))
        {
            // If the component is not of the schmea that we want to change from, do nothing...
            continue;
        }

        component = Client.TryCheckOut(component.Id, new ReadOptions()) as ComponentData;

        if (component.IsEditable.Value)
        {
            component.Schema.IdRef = schemaUriToChangeTo;
            component.Metadata = new XElement(ns + "Metadata").ToString();
            Client.Save(component, null);
            Client.CheckIn(component.Id, null);
            Console.WriteLine(String.Format(" - changed schema for {0} ({1}) ", component.Title, component.Id));
        }
        else
        {
            failedItems.Add(component);
        }
    }

This is where the namespace of the schema comes into play with the component.Metadata = new XElement(ns + "Metadata").ToString() Here we are setting up an empty metadata section using the namespace from the Schema that we are switching to. Without this line, you might see an error like “Root element must be in namespace”.

What about that failedItems variable that we were keeping track of? Good catch. Let’s go ahead and report the items that we weren’t able to change by adding the following to the end of our method.

    if (failedItems.Count > 0)
    {
        Console.WriteLine();
        Console.WriteLine("The following items could not be converted. Please have them checked in and try again.");
        foreach (ComponentData component in failedItems)
        {
            Console.WriteLine(component.Id + " :" + component.LocationInfo.WebDavUrl);
        }
    }

Our SchemaChanger class is now complete, and should look something like the following:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Net;
using System.Xml.Linq;
using Tridion.ContentManager.CoreService.Client;

namespace Examples.ComponentSchemaChanger
{
    /// <summary>
    /// Script example of changing schemas for multi media components.
    /// </summary>
    class SchemaChanger
    {
        /// <summary>
        /// The session aware core service client.
        /// </summary>
        private SessionAwareCoreServiceClient _client;

        /// <summary>
        /// Gets the SessionAware Core Service client.
        /// </summary>
        public SessionAwareCoreServiceClient Client
        {
            get
            {
                if (_client == null)
                {
                    string endpointName = ConfigurationManager.AppSettings["CoreServiceEndpoint"];
                    if (String.IsNullOrEmpty(endpointName))
                    {
                        throw new ConfigurationErrorsException("CoreServiceEndpoint missing from appSettings");
                    }

                    _client = new SessionAwareCoreServiceClient(endpointName);

                    string username = ConfigurationManager.AppSettings["TridionUsername"];
                    string password = ConfigurationManager.AppSettings["TridionPassword"];

                    if (!String.IsNullOrEmpty(username) && !String.IsNullOrEmpty(password))
                    {
                        _client.ClientCredentials.Windows.ClientCredential = new NetworkCredential(username, password);
                    }
                }
                return _client;
            }
        }

        /// <summary>
        /// Changes schemas for multimedia components located in a specific folder.
        /// </summary>
        /// <param name="folderUri">The tcm uri of the folder to change the items in.</param>
        /// <param name="recursive">Whether or not to perform this recursively on child folders.</param>
        /// <param name="fromSchemaUri">The tcm uri of the schema that we are changing from.</param>
        /// <param name="schemaUriToChangeTo">The tcm uri of the schema to change the components to.</param>
        public void ChangeSchemasForComponentsInFolder(string folderUri, bool recursive, string fromSchemaUri, string schemaUriToChangeTo)
        {
            // Let's keep track of items that couldn't be checked out and report at the end.
            List<ComponentData> failedItems = new List<ComponentData>();

            // Open the folder to check
            FolderData folder = Client.Read(folderUri, null) as FolderData;

            // Open up the schema that we will be changing to.
            SchemaData schema = Client.Read(schemaUriToChangeTo, null) as SchemaData;
            XNamespace ns = schema.NamespaceUri;

            // Create a filter to get all multi-media components.
            OrganizationalItemItemsFilterData filter = new OrganizationalItemItemsFilterData();
            filter.ItemTypes = new ItemType[] { ItemType.Component };
            filter.ComponentTypes = new ComponentType[] { ComponentType.Multimedia };
            filter.Recursive = recursive;

            XElement items = Client.GetListXml(folder.Id, filter);
            foreach (XElement item in items.Elements())
            {
                ComponentData component = Client.Read(item.Attribute("ID").Value, null) as ComponentData;

                if (!component.Schema.IdRef.Equals(fromSchemaUri))
                {
                    // If the component is not of the schmea that we want to change from, do nothing...
                    continue;
                }

                if (component.Schema.IdRef.Equals(schema.Id))
                {
                    // If the component already has this schema, don't do anything.
                    continue;
                }

                component = Client.TryCheckOut(component.Id, new ReadOptions()) as ComponentData;

                if (component.IsEditable.Value)
                {
                    component.Schema.IdRef = schemaUriToChangeTo;
                    component.Metadata = new XElement(ns + "Metadata").ToString();
                    Client.Save(component, null);
                    Client.CheckIn(component.Id, null);
                    Console.WriteLine(String.Format(" - changed schema for {0} ({1}) ", component.Title, component.Id));
                }
                else
                {
                    failedItems.Add(component);
                }
            }

            if (failedItems.Count > 0)
            {
                Console.WriteLine();
                Console.WriteLine("The following items could not be converted. Please have them checked in and try again.");
                foreach (ComponentData component in failedItems)
                {
                    Console.WriteLine(component.Id + " :" + component.LocationInfo.WebDavUrl);
                }
            }
        }
    }
}

The Program Class

Now lets go back to the Program class’ Main method and put our code to work. We’ll allow the user to pass into the command line the arguments for the folder’s tcm uri, whether or not to perform this recursively, the schema uri of the components we want to change from, and the schema uri of the schema to change the components to. We’ll also display some a usage message when an incorrect # of arguments is supplied, a message showing the error if one pops up, and finally a message letting the user know that the script has completed.

static void Main(string[] args)
{
    if (args.Length < 4)
    {
        Console.WriteLine("Usage: Examples.ComponentSchemaChanger  <y/n for recursive>  ");
        Console.WriteLine("Example:");
        Console.WriteLine("Examples.ComponentSchemaChanger tcm:100-12345 y tcm:100-987-8 tcm:100-1234-8");
    }
    else
    {
        string folderUri = args[0];
        string recursive = args[1].ToLower();
        string schemaUriFrom = args[2];
        string schemaUriTo = args[3];

        try
        {
            SchemaChanger changer = new SchemaChanger();
            changer.ChangeSchemasForComponentsInFolder(folderUri, recursive.Equals("y"), schemaUriFrom, schemaUriTo);
        }
        catch (Exception ex)
        {
            Console.WriteLine("There was an error:");
            Console.WriteLine(ex);
        }
    }

    Console.WriteLine();
    Console.WriteLine(" press <ENTER> to continue");
    Console.ReadLine();
}

Building and Deploying

Right click the project and select “Build”. This will compile and build the project for you, putting the files you need in %Project Directory%\bin\Debug\ (or %Project Directory%\bin\Release\ if you are targeting Release). The 3 files that you’ll need are Examples.ComponentSchemaChanger.exe, Examples.ComponentSchemaChanger.exe.config (this is the file containing your app settings), and Tridion.ContentManager.CoreService.Client.dll.

Executing Our Script

Our script is setup to work either on the CMS Server or remotely with a simple configuration change. Remember that CoreServiceEndpoint application config setting that we added? When this setting is “wsHttp_2011″, you’ll be able to run your script remotely (as long as you have access to contact the CMS server from your location), and when this setting is “netTcp_2011″, you’ll be able to run locally on the CMS Server. You can actually run the wsHttp_2011 binding from the CMS Server, but the netTcp binding will perform faster for you.

Whether deployed locally or remotely, open up the command prompt, navigate to the folder you deployed to, and enter the following command:
Examples.ComponentSchemaChanger tcm:12-3456-2 y tcm:12-1000-8 tcm:12-1234

Sit back, and watch magic happen. :)

Again a Warning

Remember as mentioned, changing a component’s schema can lead you to lose data from fields if the field definitions are different. Before trying out the following code, make sure the components you are changing doesn’t contain any such fields that will be lost.

Checking If An Item Is In Workflow With Core Service

During your quest of making custom applications using Tridion’s Core Service API, you may have come across the need to check whether or not an item is currently in a Workflow Process. Luckily, unlike some other tasks you may need to do with the Core Service API, this task is easily done by checking the WorkflowInfo property that is included in the PageData and ComponentData classes.

// Open a page in Core Service.
PageData page = client.Read(yourPageUri, new ReadOptions()) as PageData;

if (page.WorkflowInfo != null)
{
    // Ladies and gentlemen, WorkflowInfo property was not null, this page is in workflow
}
else
{
    // WorkflowInfo property is null, the page is not in workflow
}

Great, but now that you know that your item is in workflow, you’ll probably be wanting to grab some Workflow related items. The good news is, the WorkflowInfo class contains most of the data you’ll need to interact with the Workflow for that item. Here are some common things you’ll probably do with this property:

// Get the ProcessInstanceData from the WorkflowInfo property
ProcessInstanceData processInstance = 
    (ProcessInstanceData)client.Read(page.WorkflowInfo.ProcessInstance.IdRef, null);

// Get the ProcessDefinitionData from the process instnace
ProcessDefinitionData processDefinition =
    (ProcessDefinitionData)client.Read(processInstance.ProcessDefinition.IdRef, null);

// Getting the ActivityInstanceData from the WorkflowInfo property...
ActivityInstanceData activityInstance = 
    (ActivityInstanceData)client.Read(page.WorkflowInfo.ActivityInstance.IdRef, null);

// Get the ActivityDefinitionData from the activity instance
ActivityDefinitionData activityDefinition =
    (ActivityDefinitionData)client.Read(activityInstance.ActivityDefinition.IdRef, null);

// Note that if you only wanted to get the ActivityDefinition's Description, you can get that from the WorkflowInfo's ActivityDefinitionDescription property.
if (page.WorkflowInfo.ActivityDefinitionDescription.Equals(activityDefinition.Description))
{
    // true of course...
}

// Check how long its been since the activity was started...
if (page.WorkflowInfo.StartDate.HasValue)
{
    TimeSpan timeSinceStart = DateTime.Now - page.WorkflowInfo.StartDate.Value;
}

Other Useful Properties of WorkflowInfo

Here are the other properties of WorkflowInfo that wasn’t included in the samples above:

ActivityState: The enum value of the activity’s current state.
Assignee: The Link<TrusteeData> of the assignee.
CreationDate: The Nullable<DateTime> that the activity was created.
FinishDate: The Nullable<DateTime> that the previsou activity instance was finished.
Performer: The Link<UserData> of the activity’s performer.
PreviousMessage: The FinishMessage of the previous Activity.

Getting Using and Used Items With Core Services

A while back I posted about how to get Using and Used Items from Tridion using the TOM.NET API. Today I thought I would share again those same examples, only this time using the Core Service API. For those of you who may not know, the TOM.NET API is read only when used in Template Building Blocks, and read/write when used in the Tridion Event System. But, when you want to create custom applications or processes, you’ll want to create them using the Core Service API.

One of the major differences if you’re comparing this article with the TOM.NET one, is that the Core Service API doesn’t actually have a GetWhereUsed() or GetWhereUsing() method on object classes. Instead, we’ll be retrieving XML using the Core Service Client’s GetListXml(string identifier, SubjectRelatedListFileterData filter) method.

You may notice there’s a GetList(id, filter) method too and may be tempted to bypass working with the XML returned from GetListXml… however, as of Tridion 2011 SP1, if you look at the API’s documentation, currently the only filter supported by this method is the OrganizationalItemAncestorsFilterData, so trying to use that method with our UsingItemsFilterData or UsedItemsFilterData from the examples below will fail, and you’ll probably get an error stating “Unexpected List Type”.

Get Using Items (UsingItemsFilterData)

To get using items with Core Services, you just need to pass the GetListXml(id, filter) a filter of type UsingItemsFilterData, and the tcm uri of the item that you want to get the using items for.

GETTING ALL PAGES USED BY A COMPONENT:

SessionAwareCoreServiceClient client = new SessionAwareCoreServiceClient("netTcp_2011");

UsingItemsFilterData filter = new UsingItemsFilterData();
filter.ItemTypes = new [] { ItemType.Page };
filter.IncludedVersions = VersionCondition.OnlyLatestVersions;

XElement pages = client.GetListXml("tcm:12-3456", filter); // Pass in a Component ID

XML Sample Returned By GetListXml:

<tcm:ListUsingItems xmlns:tcm="http://www.tridion.com/ContentManager/5.0">
    <tcm:Item ID="tcm:17-384-64" Title="Some Page Title" Type="64" OrgItemID="tcm:17-107-4" Path="\040 Web Publication\Root\030 - Work" Icon="T64L1P0" Publication="040 Web Publication"></tcm:Item>
    <tcm:Item ID="tcm:31-871-64" Title="Another Page Title" Type="64" OrgItemID="tcm:31-206-4" Path="\050 Another Web\Root\030 - Work" Icon="T64L1P1" Publication="050 Another Web"></tcm:Item>
</tcm:ListUsingItems>

GETTING ALL COMPONENTS USED BY A SCHEMA:

SessionAwareCoreServiceClient client = new SessionAwareCoreServiceClient("netTcp_2011");
                
UsingItemsFilterData filter = new UsingItemsFilterData();
filter.ItemTypes = new ItemType[] { ItemType.Component };
filter.IncludedVersions = VersionCondition.OnlyLatestVersions;
filter.InRepository = new LinkToRepositoryData { IdRef = "tcm:0-31-1" };

XElement components = client.GetListXml("tcm:2-572-8", filter); // Pass in a Schema ID

GETTING ALL SCHEMAS THAT USED A GIVEN EMBEDDED SCHEMA:

UsingItemsFilterData filter = new UsingItemsFilterData
{
    ItemTypes = new ItemType[] { ItemType.Schema },
    IncludedVersions = VersionCondition.OnlyLatestVersions
};

XElement schemas = client.GetListXml("tcm:2-345-8", filter); // Pass in a Schema ID

GETTING ALL COMPONENTS USING A GIVEN KEYWORD:

UsingItemsFilterData filter = new UsingItemsFilterData
{
    ItemTypes = new ItemType[] { ItemType.Component },
    IncludedVersions = VersionCondition.OnlyLatestVersions
};

XElement components = client.GetListXml("tcm:2-213-1024", filter); // Pass in a Keyword ID

Note that the above could have just been retrieved using ClassifiedItemsFilterData.

XElement components = client.GetListXml("tcm:2-213-1024", new ClassifiedItemsFilterData());

Get Used Items (UsedItemsDataFilter)

Getting Used items from Core Service works in the same manner as getting Using items, except that you pass in a UsedItemsDataFilter as the filter instance.

GET THE EMBEDDED SCHEMAS USED IN A GIVEN SCHEMA:

SessionAwareCoreServiceClient client = new SessionAwareCoreServiceClient("netTcp_2011");
XElement schemas = client.GetListXml("tcm:2-184-8", new UsedItemsFilterData { ItemTypes = new[] { ItemType.Schema } });

GET COMPONENTS WITHIN COMPONENTLINKS OF A GIVEN COMPONENT:

UsedItemsFilterData filter = new UsedItemsFilterData
{
    ItemTypes = new ItemType[] { ItemType.Component }
};

XElement components = client.GetListXml("tcm:21-543", filter);

Happy coding everyone!