How to create an adapter for the TFS Integration Platform - Part VI: IAnalysisProvider
Note: This post is part of a series and you can find the rest of the parts in the series index.
IAnalysisProvider, has a name that is a bit misleading—or was misleading to me—because for a long time I thought it did some analysis of the environment as a pre-step and then the real work happened elsewhere. The reality is, the _IAnalysisProvider_ is the reader part of your adapter: its goal is to get data from your system and into a format and/or location that the platform can work with.
IServiceProvider
IAnalysisProvider inherits from IServiceProvider, which means you need to implement a method for that: _GetService, which just returns this object.
object IServiceProvider.GetService(Type serviceType)
{
return (IServiceProvider)this;
}
Misc Methods
I am not covering every method you need to implement from IAnalysisProvider, because you seldom need to implement them all. For example, in my implementation of _DetectConflicts_, it just does some logging:
void IAnalysisProvider.DetectConflicts(ChangeGroup changeGroup)
{
TraceManager.TraceInformation("WSSVC:AP:DetectConflicts");
}
InitializeServices
The first method you must care about is _InitializeServices_, this is the first method called by the platform and does five key things in my scenario:
void IAnalysisProvider.InitializeServices(IServiceContainer serviceContainer)
{
TraceManager.TraceInformation("WSSVC:AP:Initialize");
this.analysisServiceContainer = serviceContainer;
supportedContentTypes = new Collection<ContentType>();
supportedContentTypes.Add(WellKnownContentType.VersionControlledFile);
supportedContentTypes.Add(WellKnownContentType.VersionControlledFolder);
SharePointVCChangeActionHandler handler = new SharePointVCChangeActionHandler(this);
supportedChangeActions = new Dictionary<Guid, ChangeActionHandler>();
supportedChangeActions.Add(WellKnownChangeActionId.Add, handler.BasicActionHandler);
supportedChangeActions.Add(WellKnownChangeActionId.Delete, handler.BasicActionHandler);
supportedChangeActions.Add(WellKnownChangeActionId.Edit, handler.BasicActionHandler);
configurationService = (ConfigurationService)analysisServiceContainer.GetService(typeof(ConfigurationService));
highWaterMarkDelta = new HighWaterMark<DateTime>(Constants.HwmDelta);
highWaterMarkChangeset = new HighWaterMark<int>("LastChangeSet");
configurationService.RegisterHighWaterMarkWithSession(highWaterMarkDelta);
configurationService.RegisterHighWaterMarkWithSession(highWaterMarkChangeset);
changeGroupService = (ChangeGroupService)analysisServiceContainer.GetService(typeof(ChangeGroupService));
changeGroupService.RegisterDefaultSourceSerializer(new SharePointVCMigrationItemSerializer());
}
- The first part is setting up what types of content we support (lines 6 to 8). You can see above I only care about files and folders.
- The second part is setting up what actions we support for reading (lines 10 to 14), which in this case is Add, Delete, and Edit. Delete is a bit of a lie—we do not actually support it, but we claim we do.
- The third part is getting the configuration service, which is important since we will use it later (line 16).
- The fourth part is getting the HWM—or high watermark information—(lines 18 to 21), which I will explain soon.
- Lastly, we register the default item serializer (line 23) so that the platform knows how to convert the items.
Registration
We set up the content types and actions we support and then need to register those. The way to do that is with _RegisterSupportedChangeActions. What I did was loop over the actions, then loop over the types, and finally call _RegisterChangeAction. You could do this differently—for example, if you only supported some actions on some types:
void IAnalysisProvider.RegisterSupportedChangeActions(ChangeActionRegistrationService contentActionRegistrationService)
{
TraceManager.TraceInformation("WSSVC:AP:RegisterSupportedChangeActions");
this.changeActionRegistrationService = contentActionRegistrationService;
foreach (KeyValuePair<Guid, ChangeActionHandler> supportedChangeAction in supportedChangeActions)
{
foreach (ContentType contentType in ((IAnalysisProvider)this).SupportedContentTypes)
{
changeActionRegistrationService.RegisterChangeAction(supportedChangeAction.Key, contentType.ReferenceName, supportedChangeAction.Value);
}
}
}
ChangeActionHandler
The _ChangeActionHandler_ class is a separate class used in the _IAnalysisProvider_ during registration, providing the minimal functionality for figuring out how to register a new action (i.e., add a file, update a work item, etc.).
public abstract class ChangeActionHandlers
{
protected ChangeActionHandlers(IAnalysisProvider analysisProvider)
{
}
public virtual void BasicActionHandler(MigrationAction action, ChangeGroup group)
{
if (action == null)
{
throw new ArgumentNullException("action");
}
if (group == null)
{
throw new ArgumentNullException("group");
}
group.CreateAction(action.Action,
action.SourceItem,
action.FromPath,
action.Path,
action.Version,
action.MergeVersionTo,
action.ItemTypeReferenceName,
action.MigrationActionDescription);
}
}
High Watermark
High watermarks are a very interesting feature of the platform, and they let you store a value in the database for the usage of identifying change groups. One aspect I liked about its construction is its use of generics, meaning you can work with the types that make the most sense. So I have int and DateTime, and you associate a name with it.
To get the value from the database, you call the _Reload_ method. To set the value and save, call _Update_.
highWaterMarkDelta.Reload();
highWaterMarkDelta.Update(deltaTableStartTime);
Conflict Types
I mentioned conflict types briefly before and said that my VC adapter does not have a conflict type—which is not 100% true. It does have one, the _GenericConflictType_, which is based on the platform. Below is the code snippet from the WIT adapter, which does have a custom conflict type. The only difference with the VC adapter is that the last line does not exist.
public void RegisterConflictTypes(ConflictManager conflictManager)
{
TraceManager.TraceInformation("WSSWIT:AP:RegisterConflictTypes");
this.conflictManagerService = (ConflictManager)analysisServiceContainer.GetService(typeof(ConflictManager));
this.conflictManagerService.RegisterConflictType(new GenericConflictType());
this.conflictManagerService.RegisterConflictType(new SharePointWITGeneralConflictType(), SyncOrchestrator.ConflictsSyncOrchOptions.Continue);
}
GenerateDeltaTable
The next method to cover—and the second most important—is _GenerateDeltaTable, which is responsible for actually getting the values from the source system. This is done below in two steps: first _GetSharePointUpdates and second _PromoteDeltaToPending_.
void IAnalysisProvider.GenerateDeltaTable()
{
TraceManager.TraceInformation("WSSVC:AP:GenerateDeltaTable");
highWaterMarkDelta.Reload();
TraceManager.TraceInformation("\tWSSVC:AP:Initial HighWaterMark {0} ", highWaterMarkDelta.Value);
deltaTableStartTime = DateTime.Now;
TraceManager.TraceInformation("\tWSSVC:AP:CutOff {0} ", deltaTableStartTime);
GetSharePointUpdates();
highWaterMarkDelta.Update(deltaTableStartTime);
TraceManager.TraceInformation("\tWSSVC:AP:Updated HighWaterMark {0} ", highWaterMarkDelta.Value);
changeGroupService.PromoteDeltaToPending();
}
GetSharePointUpdates
This is a huge method and does the heavy lifting. I will skip covering all the boring details of talking to SharePoint and focus on what you need to do. First, you need to identify what is new, which is done using the HWM and comparing the modified date.
// item has been modified since HWM & before delta table start time
if (item.Modified.CompareTo(highWaterMarkDelta.Value) > 0 && item.Modified.CompareTo(deltaTableStartTime) < 0)
You also need to figure out if the file is new or an update. In my VC adapter, I created a special system called the _ProcessLog_, which was to cater to a situation caused by SharePoint and won’t apply to other systems. Once you’ve done all of that, you can tell the platform about it by creating an action and saving the action. The following code is for VC:
TraceManager.TraceInformation("\tChangeSet:{0} - {1} ({2})", highWaterMarkChangeset.Value, item.Filename, item.AbsoluteURL);
string itemType = item.ItemType.ToWellKnownContentType().ReferenceName;
ChangeGroup cg = CreateChangeGroup(highWaterMarkChangeset.Value, 0);
cg.CreateAction(actionGuid, item, null, item.AbsoluteURL, item.Version, null, itemType, null);
cg.Save();
highWaterMarkChangeset.Update(highWaterMarkChangeset.Value + 1);
And this is the same logic for WIT:
ChangeGroup changeGroup = CreateChangeGroup(highWaterMarkChangeSet.Value, 0);
changeGroup.CreateAction(actionGuid, task, string.Empty, listName, string.Empty, string.Empty,
WellKnownContentType.WorkItem.ReferenceName, CreateFieldRevisionDescriptionDoc(task));
changeGroup.Save();
highWaterMarkChangeSet.Update(highWaterMarkChangeSet.Value + 1);
Revision Description Doc
While the VC adapter is fairly easy—the downloading is done in the _SharePointItem—the WIT adapter doesn’t download anything. What it needs is a special XML file called a revision description document. You are responsible for generating this document as part of the creation of the action (you may have noticed this in the sample above).
This is what makes field mapping possible. If you don’t understand field mapping, you must read Willy-Peter’s post on it. You can see below how I create my document, which is created per SharePoint list item and how I support all the columns, including custom ones:
private static XmlDocument CreateFieldRevisionDescriptionDoc(SharePointListItem task)
{
XElement columns = new XElement("Columns",
new XElement("Column",
new XAttribute("DisplayName", "Author"),
new XAttribute("ReferenceName", "Author"),
new XAttribute("Type", "String"),
new XElement("Value", task.AuthorId)),
new XElement("Column",
new XAttribute("DisplayName", "DisplayName"),
new XAttribute("ReferenceName", "DisplayName"),
new XAttribute("Type", "String"),
new XElement("Value", task.DisplayName)),
new XElement("Column",
new XAttribute("DisplayName", "Id"),
new XAttribute("ReferenceName", "Id"),
new XAttribute("Type", "String"),
new XElement("Value", task.Id.ToString())));
foreach (KeyValuePair<string, object> column in task.Columns)
{
columns.Add(new XElement("Column",
new XAttribute("DisplayName", column.Key),
new XAttribute("ReferenceName", column.Key),
new XAttribute("Type", "String"),
new XElement("Value", column.Value)));
}
XElement descriptionDoc = new XElement("WorkItemChanges",
new XAttribute("Revision", "0"),
new XAttribute("WorkItemType", "SharePointItem"),
new XAttribute("Author", task.AuthorId),
new XAttribute("ChangeDate", task.ModifiedOn.ToString(CultureInfo.CurrentCulture)),
new XAttribute("WorkItemID", task.Id.ToString()),
columns);
XmlDocument doc = new XmlDocument();
doc.LoadXml(descriptionDoc.ToString());
return doc;
}