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 SharePointClientSideMMS
: 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 NullRefernceException
s 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?
1 Answer 1
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 emptyTermCollection
object in case something wasn't found, so I resorted to just assigning this to a class field and returning it from the mainGetTermSet
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 return
ing), 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();
}
Explore related questions
See similar questions with these tags.