Programming eDiscovery in SharePoint Server 2013

Recently I needed to get a grip on how to work with the eDiscovery tools in SharePoint 2013 from the server-side object model. There's not much information out there on how to do this (and some of the information out there is plain wrong), so I built a proof-of-concept console app to work through the key features, including:

  • Programmatically retrieving an eDiscovery case.
  • Creating a new source.
  • Creating a custodian.
  • Creating a new source.
  • Creating a new eDiscovery set.
  • Using queries and exports.
  • Applying in-place holds to eDiscovery sets.

I'll keep the explanation to a minimum, as I'm hoping the code largely speaks for itself. I'll assume you know the basic concepts of eDiscovery in SharePoint 2013, including how to work with cases, sources, sets, queries, and exports through the UI. To use the code, you'll need assembly references to Microsoft.SharePoint.dll and Microsoft.Office.Policy.dll. All the eDiscovery classes you need are in the Microsoft.Office.Server.Discovery namespace.

Our first task is to retrieve a case. An eDiscovery case is an individual site (SPWeb) within your eDiscovery Center site collection. Individual eDiscovery cases are represented by the Case class. The Case class provides the entry point for all eDiscovery operations in code. To retrieve a case, you need to call the Case constructor and pass the corresponding SPWeb instance as a parameter:

using (SPSite site = new SPSite("http://sp2013/sites/ediscovery/"))
using (SPWeb webCase = site.OpenWeb("case1"))
{
Case myCase = new Case(webCase);

Next, let's use the Case instance to create a new custodian. Essentially all this does is create a new list item in the Custodians list in the case web. Later, you'll see how we can assign custodians to sources to indicate who's responsible for the source:

//Create a custodian
Custodian newCustodian = myCase.CreateCustodian();
newCustodian.Name = "Legal Eagle";
newCustodian.LoginName = "JASON\\bob"; // i.e. domain username
newCustodian.Update();

Next let's create a new source. Essentially, SharePoint 2013 eDiscovery sources are either SharePoint webs or Exchange mailboxes. In both cases, the source is represented by the Source class. To create a new source from a SharePoint web, you call the Case.CreateLocation method. To create a new source from an Exchange mailbox, you call the Case.CreateMailbox method. I'm going to focus on creating a source from a SharePoint web. Once you've created the new Source instance, if you want it to work properly, you need to set three key properties:
  • Set the Source.WebId property to the ID of the SPWeb for which you want to define a source.
  • Set the Source.SiteId property to the ID of the SPSite containing the web for which you want to define a source.
  • Set the Source.FederationId property to the ID of the search result source (e.g. "Local SharePoint Results") from which you want to retrieve content.
Getting the ID of webs and sites is straightforward. Getting the ID of the search result source is a little more complex (credit). The code looks like this:


Guid targetSiteId;
Guid targetWebId;
Guid resultSourceId;

// Get the site ID and the web ID for the source
using(SPSite targetSite = new SPSite("http://team.jason.net/"))
{
targetSiteId = targetSite.ID;
var targetWeb = targetSite.RootWeb;
targetWebId = targetWeb.ID;
}

// Get a result source ID from the search service application
SearchQueryAndSiteSettingsServiceProxy searchSettingsProxy =
SPFarm.Local.ServiceProxies
    .GetValue<SearchQueryAndSiteSettingsServiceProxy>();
SearchServiceApplicationProxy searchProxy =
searchSettingsProxy.ApplicationProxies
    .GetValue<SearchServiceApplicationProxy>("Search Service Application");
    SearchObjectOwner serviceApplicationOwner = new      
    SearchObjectOwner(SearchObjectLevel.Ssa);
SourceRecord serviceApplicationResultSource = searchProxy.GetResultSourceByName("Local SharePoint Results", serviceApplicationOwner);
resultSourceID = serviceApplicationResultSource.Id;

// Create the new eDiscovery source
Microsoft.Office.Server.Discovery.Source newSource = myCase.CreateLocation();
newSource.Name = "Team Site";
newSource.DisplayId = "http://team.jason.net";
newSource.FederationId = resultSourceID;
newSource.SiteId = targetSiteId;
newSource.WebId = targetWebId;
newSource.AddCustodian(newCustodian);
newSource.Update();

Now that we've created a source, we can use it in eDiscovery sets or queries. In the object model, eDiscovery sets are represented by the SourceGroup class. Let's create a new one and add our source to it:

// Create a new eDiscovery Set
SourceGroup newSet = myCase.CreateSourceGroup();
newSet.Name = "Team Site Leaflets";
newSet.AddSource(newSource);
newSet.DateStartFilter = new DateTime(2015, 1, 1);
newSet.Query = "Leaflet";

newSet.Update();

If you want to apply an in-place hold to the set, you simply set the SourceGroup.Preserve property to true and then call the Update method:

// Put the set on hold
newSet.Preserve = true;

newSet.Update();

Note that this requests an in-place hold. The hold won't actually take effect until the eDiscovery In-Place Hold Processing timer job runs.

Next, let's look at how to create a query. In the object model, queries are represented by the SavedSearch class:

// Create a new query based on the discovery set we created earlier
SavedSearch newQuery = myCase.CreateSavedSearch();
newQuery.Name = "Coded Query";
newQuery.SourceManagement = SourceManagementType.SourceGroup;
newQuery.SourceGroupIds.Add(1) // SPListItem ID of the discovery set
newQuery.Deduplication = true;
newQuery.SPFilters = "Created<=2/15/2015";
newQuery.Update();

When you create a query through the UI, you can choose whether to include eDiscovery sets (each consisting of one or more sources with filter criteria) or sources (no filter criteria). In the object model, you specify query scope by setting the SavedSearch.SourceManagement property to one of the following SourceManagementType enumeration values:

  • SourceManagementType.AllContent. This corresponds to the All case content option in the UI. The query contains all the sources defined in the case, with any eDiscovery set filters applied.
  • SourceManagementType.SourceGroup. This corresponds to the Select eDiscovery sets option in the UI. The query contains the sources defined in the discovery sets you select, with any discovery set filters applied.
  • SourceManagementType.Source. This corresponds to the Select sources option in the UI. The query contains the sources you select, and no discovery set filters are applied.
Once you've configured this property, you can either add sources or discovery sets to the query by adding integer IDs to the SourceIds or SourceGroupIds collections respectively. In this example, I've specified the SourceManagementType.SourceGroup option and then added the integer ID of my discovery set to the SourceGroupIds collection.

The SavedSearch class also allows you to specify string filters and refiners for the query. You can use the SPFilters and SPRefinements properties to specify filters and refiners for SharePoint sources, and you can use the EXFilters and EXRefinements properties to specify filters and refiners for Exchange mailboxes. A word of warning, however - the syntax required for the refinement properties is not user friendly. For example, if you create a query with refiners that match a file extension of "txt" and an author of "Administrator", the SPRefinements property ends up looking like this:

[{"n":"Author","t":["Administrator"],"o":"and","k":false,"m":null},"n":"FileExtension","t":["\"??747874\""],"o":"OR","k":false,"m":{"\"??747874\"":"txt"}}]

So it's probably fair to say that our ability to create query refiners in code is limited.

Finally, let's take a look at how to create an export in code. When you create an export, you're essentially just adding an item to the Exports list in the case web. However, the Export class does expose a ResultLink property that enables you to get the exported and packaged discovery content.

Export myExport = myCase.CreateExport(true, true, true);
myExport.Name = "Coded Export";
myExport.AddSearch(newQuery);
Console.WriteLine(String.Format("Download link: {0}", myExport.ResultLink));
}

That's it - I think that pretty much covers all the key bits of using SharePoint eDiscovery in code. Hope it helps.

Comments

  1. Property or indexer 'Microsoft.Office.Server.Discovery.SavedSearch.SPFilters' cannot be assigned to -- it is read only

    ReplyDelete
  2. SPFilters is read only. So now what else we can do for putting filters ?

    ReplyDelete
  3. Hi Prashant - you're absolutely right. SavedSearch.SPRefinements is writeable but SavedSearch.SPFilters is not. I'll figure out a workaround and update the post when I can. You might be able to do it through the string-based SavedSearch.Query property.

    ReplyDelete
  4. Hey Jason - Thanks for this. I will wait for your next update :)

    ReplyDelete
  5. This comment has been removed by a blog administrator.

    ReplyDelete

Post a Comment

Popular posts from this blog

Server-side activities have been updated

The target principal name is incorrect. Cannot generate SSPI context.

Custom Workflow Activity for Creating a SharePoint Site