0
\$\begingroup\$

I have written a small program which has 2 main classes :

  • ServerSideMMS: This can access the SharePoint Managed Metadata Services, but only from the server which is running SharePoint
  • ClientSideMMS: This can access the SharePoint Managed Metadata Services from any client machine

These can both be used to access MMS services and retrieve the data. In my case, I only need to grab the actual set of terms (which is the lowest level of metadata).

Server

    using Microsoft.SharePoint;
    using Microsoft.SharePoint.Taxonomy;

    namespace CallTaxonomyWebService
    {
        /// <summary>
        /// A class used for retrieving MMS data. This must be run on the server
        /// which is hosting the metadata.
        /// </summary>
        class ServerSideMMS
        {
            private const string ManagedMetadataService = "Managed Metadata Service";
            private TaxonomySession Taxonomy;
            private SPSite Site;

            /// <summary>
            /// The constructor assigns the given SPSite and the resulting TaxonomySession
            /// to some private fields. 
            /// </summary>
            /// <param name="spSite"></param>
            public ServerSideMMS(string spSite)
            {
                Site = new SPSite(spSite);
                Taxonomy = new TaxonomySession(Site);
            }

            /// <summary>
            /// Get all the terms which are stored in the given termset under the given group
            /// </summary>
            /// <param name="groupName"></param>
            /// <param name="setName"></param>
            /// <returns></returns>
            public TermCollection GetTermSet(string groupName, string setName)
            {
                var group = GetGroup(groupName);
                var termset = group.TermSets[setName];
                return termset.Terms;
            }

            /// <summary>
            /// Returns an MMS group of the given name
            /// </summary>
            /// <param name="groupName"></param>
            /// <returns></returns>
            public Group GetGroup(string groupName)
            {
                var Mms = Taxonomy.TermStores[ManagedMetadataService];
                return Mms.Groups[groupName];
            }
        }
    }

Client

I wasn't particularly happy with the outcome of this class. As you can see I had to split the TermSet retrieval into 2 classes, because I wasn't sure how to create and return an empty TermCollection object in case something wasn't found, so I resorted to just assigning this to a class field and returning it from the main GetTermSet method :

    using Microsoft.SharePoint.Client;
    using Microsoft.SharePoint.Client.Taxonomy;

    namespace CallTaxonomyWebService
    {
        /// <summary>
        /// A class used for retrieving MMS data. This can be run from a client machine.
        /// Because of the more complicated nature of these queryies (when compared to 
        /// the server-side calls), I have defined a few private fields to store
        /// return data. 
        /// 
        /// For more on what we can do with CSOM and MMS, see https://github.com/OfficeDev/PnP/tree/dev and
        /// https://msdn.microsoft.com/en-us/library/office/dn904534.aspx. 
        /// </summary>
        class ClientSideMMS
        {
            private TaxonomySession Taxonomy;
            private ClientContext Context;
            private TermStore Store;
            private TermGroup Group;
            private TermCollection Collection;

            /// <summary>
            /// The constructor sets and validates the ClientContext before loading
            /// the required elements of each Store, Group, TermSet and Term. This can
            /// be amended if we every want to retrieve more data but for now I am 
            /// just getting the names of each. 
            /// </summary>
            /// <param name="spSite"></param>
            public ClientSideMMS(string spSite)
            {
                Context = new ClientContext(spSite)
                    { AuthenticationMode = ClientAuthenticationMode.Default};
                Taxonomy = TaxonomySession.GetTaxonomySession(Context);
                Store = Taxonomy.GetDefaultSiteCollectionTermStore();
                  Context.Load(Store,
                        store => store.Name,
                        store => store.Groups.Include(
                            group => group.Name,
                            group => group.TermSets.Include(
                                termSet => termSet.Name,
                                termSet => termSet.Terms.Include(
                                    term => term.Name)
                            )
                        )
                );
                Context.ExecuteQuery();
            }

            /// <summary>
            /// This returns the private "Set" field after calling "LoadTermSet" (which assigns the 
            /// requested TermSet to Set)
            /// </summary>
            /// <param name="groupName"></param>
            /// <param name="setName"></param>
            /// <returns></returns>
            public TermCollection GetTermSet(string groupName, string setName)
            {
                LoadTermSet(groupName, setName);
                return Collection;
            }

            /// <summary>
            /// Iterate through each Group until we find the right one (Name = groupName). Then 
            /// find the correct TermSet in that group and assign the Set field to that value.
            /// 
            /// This is structured like this to avoid local "return" variables ... 
            /// </summary>
            /// <param name="groupName"></param>
            /// <param name="setName"></param>
            private void LoadTermSet(string groupName, string setName)
            {
                foreach (var group in Store.Groups)
                {
                    if (group.Name == groupName)
                    {
                        Group = group;

                        foreach (var set in group.TermSets)
                        {
                            if (set.Name == setName)
                            {
                                // I would much rather this function just return the Collection,
                                // but as I can't guaruntee that a collection will be found, this
                                // could cause an error, hence the private field ... 
                                Collection = set.Terms;
                                return;
                            }
                        }
                    }
                }
            }
        }
    }

Calling the classes

    using System.Linq;

    namespace CallTaxonomyWebService
    {
        static class Program
        {
            private const string site = @"http://mysharepointsite/";

            private static void Main(string[] args)
            {
                var mms = new ClientSideMMS(site);
                var termcollection = mms.GetTermSet("People", "Department");
                var localTerms = termcollection.Select(term => term.Name).ToList();

                var remoteMms = new ServerSideMMS(site);
                var remoteCollection = remoteMms.GetTermSet("People", "Department");
                var remoteTerms = remoteCollection.Select(term => term.Name).ToList();
            }
        }
    }

Any feedback is much appreciated, particularly in relation to my use of a private field instead of a return type to try and avoid NullRefernceExceptions in the client side class.

For example, would it be better to try and initialise an empty return value inside the LoadTermSet method and just return that at the end?

\$\endgroup\$
1
\$\begingroup\$
private TaxonomySession Taxonomy;
private SPSite Site;

These fields should be readonly. Also, private fields usually use camelCase, not PascalCase.


/// <param name="groupName"></param>
/// <param name="setName"></param>
/// <returns></returns>

There is no reason to have empty documentation comments. Either fill them in with useful information, or remove them.


var Mms = Taxonomy.TermStores[ManagedMetadataService];

Local variables also usually use camelCase.


As you can see I had to split the TermSet retrieval into 2 [methods], because I wasn't sure how to create and return an empty TermCollection object in case something wasn't found, so I resorted to just assigning this to a class field and returning it from the main GetTermSet method

This approach is wrong. If you call GetTermSet for the second time and nothing is found, it will return the previous result.

If you want to return null in the case when nothing is found (i.e. the outer loop completes without returning), then just do that. There also isn't any reason to assign to the Group field, or even to have it.

public TermCollection GetTermSet(string groupName, string setName)
{
    foreach (var group in store.Groups)
    {
        if (group.Name == groupName)
        {
            foreach (var set in group.TermSets)
            {
                if (set.Name == setName)
                {
                    return set.Terms;
                }
            }
        }
    }

    return null;
}

Or even better, use LINQ:

public TermCollection GetTermSet(string groupName, string setName)
{
    var query = from g in store.Groups
                where g.Name == groupName
                from set in g.TermSets
                where set.Name == setName
                select set.Terms;

    return query.FirstOrDefault();
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.