
Until Office 2007, Microsoft hadn’t offered any reliable Office suite automation capabilities with the exception of client-side mail merge. This often meant companies/developers had to hand-craft Word documents at byte level, or invest in costly commercial products that did so for them. With Office 2007, Microsoft’s introduced Open XML document formats and an OpenXML SDK for developers, but it still had some short comings when working with templates or converting between file formats.
NOTE: this article was written before Office 365 / Microsoft 365 was released. A more modern approach to updating documents or converting between file types is available using Microsoft Graph and SharePoint Online.
On this page:
Preamble
Until Office 2007 was released, Microsoft strongly advised against any kind of server-side automation using their Office suite, since it was focused very specifically on the client and didn’t have any alternatives to modal dialogue boxes when imparting information or displaying errors. This meant that it could just wait indefinitely in a headless server side environment if it ‘displayed a prompt’ when the application (e.g. Word) was loaded from a Windows Service in an out-of-session process (i.e. no user session.) Even with the advent of the Open XML SDK, there were a few missing pieces to the automation puzzle – most notably the ability to update document fields automatically, and the ability to use Office’s ‘Save As’ functionality (to save to different file formats.)
Microsoft itself refer to SharePoint Word Automation Services (WAS) as the missing ‘Save As’ functionality in server-side Word document automation.
In SharePoint 2010, WAS only supports interaction via the API. There aren’t any web services exposing this capability and no out-of-the-box activities via workflows either.
You are likely going to be developing against WAS in one of two ways: (i) by developing custom activities that allow WAS to be integrated into workflows; or (ii) by developing your own custom web services that expose WAS to the wider world (i.e. not just to SharePoint workflows.)
This article considers both approaches.
One final thing to note is that, generally speaking, SharePoint 2010 development needs to be performed on the server itself (well, hopefully a development server similar to the production system!) Projects must target .NET Framework 3.5 SP1 and must be configured to build to x64 or Any CPU (since SharePoint 2010 is a 64-bit only product.)
Exposing Word Automation Via Web Services
Before going any further it is worth mentioning that the worked examples below do not go into much detail from a WCF hosting perspective. It is assumed you already know how to do this. If you want a simplified configuration option with regards to WCF see my other article on defining WCF behaviours in code instead of via configuration file sections, here.
Anyone who’s done much in the way of WCF programming will be well aware of the ‘fun’ that can be had trying to get endpoints to talk to each other based on service/client configuration settings in application configuration files. After many painful hours attempting to debug these sorts of issues, I decided to investigate a better way to go. I now tend to define the service configuration in code in a shared WCF utilities assembly, so that both server and consumer are using exactly the same configuration and the work involved in establishing or using the web services is simplified. See the related article here.
By exposing WAS as one or more web services, you have the opportunity to consume its services within your wider codebase – not just within SharePoint itself. This worked example uses Windows Communications Foundation (WCF) to expose SharePoint WAS. For the approach described below to work, the web services must run on the SharePoint Server machine itself so that elevation of privileges can be performed.
This solution requires a minimum of two projects: an assembly containing the definitions (contracts, etc.) and another for the Windows Service that hosts the capability.
Create a new Visual Studio Class Library project. Give it a sensible name, such as My.SharePoint.WordAutomation. First, we’ll define two web service contracts…
using System.Runtime.Serialization;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.ServiceModel;
namespace My.SharePoint.WordAutomation
{
/// <summary>
/// Represents the output format using for document conversion.
/// </summary>
public enum DocFormat
{
/// <summary>
/// Output format determined automatically based on output filename extension.
/// </summary>
Auto = 0,
/// <summary>Open XML document format (.DOCX)</summary>
Doc = 1,
/// <summary>Open XML macro-enabled document format (.DOCM)</summary>
MacroDoc = 2,
/// <summary>Legacy document format (.DOC)</summary>
LegacyDoc = 3,
/// <summary>Open XML template format (.DOTX)</summary>
Template = 4,
/// <summary>Open XML macro-enabled template format (.DOTM)</summary>
MacroTemplate = 5,
/// <summary>Legacy document template format (.DOT)</summary>
LegacyTemplate = 6,
/// <summary>MHTML Single File Web Page (.MHT)</summary>
Web = 7,
/// <summary>Portable document format (.PDF)</summary>
Pdf = 8,
/// <summary>Rich text format (.RTF)</summary>
RichText = 9,
/// <summary>Microsoft Word XML document format (.XML)</summary>
Xml = 10,
/// <summary>XML Paper Specification document format (.XPS)</summary>
XmlPaperSpec = 11
}
/// <summary>
/// Represents the type of markup that will be included in the output document.
/// </summary>
[Flags]
public enum DocMarkupTypes
{
/// <summary>Include comments in the output document.</summary>
Comments = 1,
/// <summary>Include ink markup in the output document.</summary>
Ink = 2,
/// <summary>
/// Include revisions (insertions, deletions, etc.) in the output document.
/// </summary>
Text = 4,
/// <summary>Include formatting revisions in the output document.</summary>
Formatting = 8,
/// <summary>Include all markup types.</summary>
All = Comments | Ink | Text | Formatting
}
/// <summary>
/// Represents the review status of the output document.
/// </summary>
public enum RevisionType
{
/// <summary>
/// Final view, with revisions accepted and comments removed.
/// </summary>
Final = 0,
/// <summary>
/// Original view, with revisions rejected and comments removed.
/// </summary>
Original = 1,
/// <summary>
/// Final view, but retaining all revisions and comments information.
/// </summary>
FinalWithMarkup = 2,
/// <summary>
/// Original view, but retaining all revisions and comments information.
/// </summary>
OriginalWithMarkup = 3
}
/// <summary>
/// The behaviour to adopt when saving the converted document.
/// </summary>
public enum SaveMode
{
/// <summary>
/// Try to append the existing file first (if present and versioning
/// is enabled), or overwrite in all other cases.
/// </summary>
AppendIfPossible = 0,
/// <summary>
/// Always overwrite the existing file (even if versioning is enabled.)
/// </summary>
AlwaysOverwrite = 1,
/// <summary>
/// Append the file as a new version.
/// If versioning disabled, the conversion will fail.
/// </summary>
AppendOnly = 2,
/// <summary>Fail the conversion if the output file already exists.</summary>
NeverOverwrite = 3
}
/// <summary>
/// The compatibility mode to use during conversion.
/// </summary>
public enum WordCompatibilityMode
{
/// <summary>
/// Use the newest compatibility mode supported by the WAS service.
/// </summary>
Current = -1,
/// <summary>Use the mode specified by the input file.</summary>
UseFile = 0,
/// <summary>Word 97-2003 compatibility mode.</summary>
LegacyWord = 11,
/// <summary>Word 2007 compatibility mode.</summary>
Word2007 = 12,
/// <summary>Word 2010 compatibility mode.</summary>
Word2010 = 14
}
/// <summary>
/// Provides properties that control conversion behaviour.
/// </summary>
[DataContract]
public class ConversionOptions
{
/// <summary>
/// Gets or sets a value controlling if a preview thumbnail should be
/// created for the new file. (Defaults to false.)
/// </summary>
[DataMember]
public bool AddThumb { get; set; }
/// <summary>
/// Gets or sets a value controlling if the original file is deleted
/// after processing (for document conversion jobs only.)
/// </summary>
[DataMember]
public bool DeleteOriginal { get; set; }
/// <summary>
/// Gets or sets a value controlling if fonts used in the output document
/// are embedded in it. (Defaults to false.)
/// </summary>
[DataMember]
public bool EmbedFonts { get; set; }
/// <summary>
/// Gets or sets a value controlling if system fonts are embedded in the
/// output document (ignored if the EmbedFonts property isn't asserted.)
/// (Defaults to false.)
/// </summary>
[DataMember]
public bool EmbedSystemFonts { get; set; }
/// <summary>
/// Gets or sets a value controlling the types of markup to include in
/// the output document. (Defaults to 'All'.)
/// </summary>
[DataMember]
public DocMarkupTypes MarkupView { get; set; } = DocMarkupTypes.All;
/// <summary>
/// Gets or sets the review status to set when saving the output document.
/// (Defaults to 'Final'.)
/// </summary>
[DataMember]
public RevisionType ReviewStatus { get; set; } = RevisionType.Final;
/// <summary>
/// Gets or sets the behaviour to adopt when attempting to save the
/// output document. (Defaults to 'AppendIfPossible'.)
/// </summary>
[DataMember]
public SaveMode SaveAs { get; set; } = SaveMode.AppendIfPossible;
/// <summary>
/// Gets or sets a value controlling if only those characters used in
/// the output
/// document are included in the embedded fonts. (Defaults to false.)
/// </summary>
[DataMember]
public bool SubsetFonts { get; set; }
/// <summary>
/// Gets or sets the output format being targeted. (Defaults to 'Auto'.)
/// </summary>
[DataMember]
public DocFormat TargetFormat { get; set; } = DocFormat.Auto;
/// <summary>
/// Gets or sets a value controlling if document fields are updated
/// during conversion. (Defaults to false.)
/// </summary>
[DataMember]
public bool UpdateFields { get; set; }
/// <summary>
/// Gets or sets the file compatibility mode WAS should use.
/// (Defaults to 'Current'.)
/// </summary>
[DataMember]
public WordCompatibilityMode WordMode { get; set; } =
WordCompatibilityMode.Current;
}
/// <summary>
/// Provides the file conversion settings.
/// </summary>
[DataContract]
public class ConversionSettings
{
/// <summary>
/// Gets or sets the name of the document library the source file is in.
/// </summary>
[DataMember]
public string Library { get; set; } = "";
/// <summary>
/// Gets or sets the conversion options that control behaviour.
/// </summary>
[DataMember]
public ConversionOptions Options { get; set; } = new();
/// <summary>
/// Gets or sets the full URL of the SharePoint site being targeted.
/// </summary>
[DataMember]
public string Site { get; set; } = "";
/// <summary>
/// Gets or sets the filename of the source file.
/// </summary>
[DataMember]
public string SourceFile { get; set; } = "";
/// <summary>
/// Gets or sets the filename of the output file.
/// </summary>
[DataMember]
public string TargetFile { get; set; } = "";
}
/// <summary>
/// Provides details about a SharePoint Word Automation Services
/// processing failure.
/// </summary>
[DataContract]
public sealed class JobFailure
{
/// <summary>
/// Creates a new instance of the <see cref="JobFailure"/> class.
/// </summary>
public JobFailure()
: this("", "", "")
{
}
/// <summary>
/// Creates a new instance of the <see cref="JobFailure"/> class,
/// initialising all properties.
/// </summary>
/// <param name="inputFile">The name of the input file.</param>
/// <param name="outputFile">The name of the output file.</param>
/// <param name="failMessage">The error message or reason for failure.</param>
public JobFailure(string inputFile, string outputFile, string failMessage)
{
Input = inputFile;
Output = outputFile;
Message = failMessage;
}
/// <summary>
/// Gets the name of the input file.
/// </summary>
[DataMember]
public string Input { get; private set; }
/// <summary>
/// Gets the error message or reason for failure.
/// </summary>
[DataMember]
public string Message { get; private set; }
/// <summary>
/// Gets the name of the output file.
/// </summary>
[DataMember]
public string Output { get; private set; }
}
/// <summary>
/// Defines a contract between client and service endpoints for consuming
/// SharePoint Word Automation Services capability.
/// </summary>
[ServiceContract]
[ServiceKnownType(typeof(DocFormat))]
[ServiceKnownType(typeof(DocMarkupTypes))]
[ServiceKnownType(typeof(JobFailure))]
[ServiceKnownType(typeof(List<JobFailure>))]
[ServiceKnownType(typeof(RevisionType))]
[ServiceKnownType(typeof(SaveMode))]
[ServiceKnownType(typeof(WordCompatibilityMode))]
[ServiceKnownType(typeof(ConversionOptions))]
[ServiceKnownType(typeof(ConversionSettings))]
[ServiceKnownType(typeof(List<ConversionSettings>))]
public interface IWordAuto
{
/// <summary>
/// Performs a synchronous document update/conversion, optionally returning
/// the raw byte array for the revised file.
/// <para>
/// NOTE: Use of this method is not recommended as SharePoint WAS Timers
/// are set to process the queue periodically, and the timer interval could
/// be many minutes or hours. Implementations should impose a suitable
/// timeout (e.g. 1 hour) to avoid the method running indefinitely.
/// </para>
/// </summary>
/// <param name="settings">The conversion job settings.</param>
/// <param name="getContent">
/// True to return the converted/processed document,
/// and False otherwise (returns a null.)
/// </param>
[OperationContract(Name = "Convert")]
byte[] Convert(ConversionSettings settings, bool getContent);
/// <summary>
/// Performs a synchronous batch document update/conversion,
/// optionally returning the raw byte array for the revised files.
/// <para>
/// NOTE: Use of this method is not recommended as SharePoint WAS Timers
/// are set to process the queue periodically, and the timer interval could
/// be many minutes or hours. Implementations should impose a suitable
/// timeout (e.g. 1 hour) to avoid the method running indefinitely.
/// </para>
/// </summary>
/// <param name="settings">The collection of documents to process.</param>
/// <param name="getContent">
/// True to return the converted/processed documents,
/// and False otherwise (returns an empty collection.)
/// </param>
[OperationContract(Name = "ConvertMultiple")]
List<byte[]> Convert(List<ConversionSettings> settings, bool getContent);
/// <summary>
/// Performs a synchronous document update/conversion, optionally returning
/// the raw byte array for the revised file.
/// <para>
/// NOTE: Use of this method is not recommended as SharePoint WAS Timers
/// are set to process the queue periodically, and the timer interval could
/// be many minutes or hours. Implementations should impose a suitable
/// timeout (e.g. 1 hour) to avoid the method running indefinitely.
/// </para>
/// </summary>
/// <param name="filename">
/// The filename of the source file being converted (that will be
/// temporarily uploaded to SharePoint.)
/// </param>
/// <param name="content">
/// The contents of the source file that will be temporarily uploaded
/// to SharePoint.
/// </param>
/// <param name="settings">The conversion job settings.</param>
/// <param name="getContent">
/// True to return the converted/processed document,
/// and False otherwise (returns a null.)
/// </param>
/// <returns>The raw byte contents of the output document.</returns>
[OperationContract(Name = "ConvertWithTemporaryFiles")]
byte[] Convert(
string filename, byte[] content, ConversionSettings settings,
bool getContent);
/// <summary>
/// Asynchronously queues a conversion job, returning the unique job ID
/// immediately.
/// <para>Use the GetJobStatus method to check on job progress.</para>
/// </summary>
/// <param name="settings">The conversion job settings.</param>
/// <returns>The unique ID of the conversion job that was queued.</returns>
[OperationContract(Name = "ConvertAsync")]
Guid ConvertAsync(ConversionSettings settings);
/// <summary>
/// Asynchronously queues a conversion job, returning the unique job ID
/// immediately.
/// <para>Use the GetJobStatus method to check on job progress.</para>
/// </summary>
/// <param name="settings">The collection of documents to process.</param>
/// <returns>The unique ID of the conversion job that was queued.</returns>
[OperationContract(Name = "ConvertAsyncMultiple")]
Guid ConvertAsync(List<ConversionSettings> settings);
/// <summary>
/// Returns details about any conversions that failed during execution of
/// the job, or an empty collection.
/// </summary>
/// <param name="id">The WAS conversion job's unique ID.</param>
/// <returns>A collection of conversion failure messages.</returns>
[OperationContract]
List<JobFailure> GetJobFailures(Guid id);
/// <summary>
/// Returns the percentage by which the conversion(s) job has progressed.
/// </summary>
/// <param name="id">The WAS conversion job's unique ID.</param>
/// <returns>The job progress, as a percentage.</returns>
[OperationContract]
decimal GetJobStatus(Guid id);
}
/// <summary>
/// Defines a contract between client and service endpoints for interacting with
/// SharePoint document libraries.
/// </summary>
[ServiceContract]
public interface ISharePointDocs
{
/// <summary>
/// Deletes a file from a SharePoint document library.
/// </summary>
/// <param name="site">The URL of the SharePoint site.</param>
/// <param name="library">
/// The name of the document library being targeted.
/// </param>
/// <param name="filename">The name of the file to delete.</param>
[OperationContract(Name = "DeleteByFilename")]
void Delete(string site, string library, string filename);
/// <summary>
/// Deletes a file from SharePoint. (Requires that document IDs have
/// been enabled on the SharePoint Server.)
/// </summary>
/// <param name="id">The unique ID of the document.</param>
[OperationContract(Name = "DeleteByFileId")]
void Delete(int id);
/// <summary>
/// Retrieves the raw content of a file saved in a SharePoint document library.
/// </summary>
/// <param name="site">The URL of the SharePoint site.</param>
/// <param name="library">
/// The name of the document library being targeted.
/// </param>
/// <param name="filename">The name of the file to retrieve.</param>
/// <returns>The raw content of the file.</returns>
[OperationContract]
byte[] Get(string site, string library, string filename);
/// <summary>
/// Retrieves the raw content of a file saved in a SharePoint document library.
/// (Requires that document IDs have been enabled on the SharePoint Server.)
/// </summary>
/// <param name="id">The unique ID of the document on the server.</param>
/// <returns>The raw content of the file.</returns>
[OperationContract]
byte[] Get(int id);
/// <summary>
/// Retrieves a list of all the files in the specified SharePoint
/// document library.
/// </summary>
/// <param name="site">The URL of the SharePoint site.</param>
/// <param name="library">
/// The name of the document library being targeted.
/// </param>
/// <returns>A list of the filenames of the documents in the library.</returns>
[OperationContract]
List<string> ListDocs(string site, string library);
/// <summary>
/// Retrieves a list of the names of all the document libraries on the
/// specified SharePoint site.
/// </summary>
/// <param name="site">The URL of the SharePoint site.</param>
/// <returns>A list of document library names.</returns>
[OperationContract]
List<string> ListLibraries(string site);
/// <summary>
/// Uploads a file to SharePoint.
/// </summary>
/// <param name="site">The URL of the SharePoint site.</param>
/// <param name="library">
/// The name of the document library being targeted.</param>
/// <param name="filename">The filename to save as.</param>
/// <param name="content">The raw file content to save.</param>
/// <param name="overwrite">
/// True to overwrite an existing file with the same name (or new version
/// of an existing file if versioning is enabled), if present, or False
/// to raise an exception if another file with the same name already exists.
/// </param>
[OperationContract]
void Put(
string site, string library, string filename, byte[] content,
bool overwrite);
}
}
Code language: C# (cs)
NOTE: The enumerated type values have been defined specifically so they line-up exactly with the equivalent SharePoint 2010 API declarations. If you are migrating this project code to a SharePoint 2013 implementation, you may want to make sure the declarations or their values are still the same.
I normally split my entities into separate files to improve the readability of the project, but I’ve concatenated them together into the same code fragment here for convenience of presentation.
Now that we’ve defined our WCF contracts, we can get to work developing the services themselves. We’ll need a second project for this (a Windows Service project.) In this case we’re defining both contract implementations within the same class (since the Word Automation one uses some of the document management methods) but they could just as easily have been defined independently. Also, it is worth noting that the Windows Service is likely to need to be configured to run as the SharePoint Administrator account (normally ‘sp_admin’, but it will depend how your IT team have set SharePoint up and whether they’ve named their service accounts according to Microsoft recommendations.)
We’ll need to add references to Microsoft.SharePoint and Microsoft.Office.Word.Server assemblies to our service host project.
using Microsoft.SharePoint;
using System.Collections.Generic;
using System.Configuration;
using System.Security.Principal;
using Microsoft.Office.Word.Server.Conversions;
using My.SharePoint.WordAutomation;
using Microsoft.VisualBasic;
using System.Collections;
using System.ComponentModel;
using System.Security.Principal;
namespace My.SharePoint.WordAutomation.Host
{
internal class DocManagement : IWordAuto, ISharePointDocs
{
#region IWordAuto Members
byte[] IWordAuto.Convert(ConversionSettings settings, bool getContent)
{
IWordAuto automation = this;
byte[] content =
automation.Convert(
new List<ConversionSettings> { settings },
getContent
)[0];
return getContent ? content : null;
}
List<byte[]> IWordAuto.Convert(
List<ConversionSettings> settings, bool getContent)
{
IWordAuto automation = this;
ISharePointDocs docs = this;
Guid jobId = automation.ConvertAsync(settings);
DateTime timeout = DateTime.Now.AddHours(1D);
while (automation.GetJobStatus(jobId) < 100M &&
DateTime.Now < timeout)
{
Thread.Sleep(60000);
}
if (automation.GetJobStatus(jobId) < 100M)
{
throw new Exception(
"Timeout while waiting for the job to complete. Long " +
"running jobs are better handled with asynchronous calls.");
}
if (!getContent)
{
return new List<byte[]>();
}
List<byte[]> contents = new();
foreach (ConversionSettings conversion in settings)
{
contents.Add(
docs.Get(
conversion.Site,
conversion.Library,
conversion.TargetFile)
);
}
return contents;
}
byte[] IWordAuto.Convert(
string filename, byte[] content, ConversionSettings settings,
bool getContent)
{
IWordAuto automation = this;
ISharePointDocs docs = this;
docs.Put(settings.Site, settings.Library, filename, content, false);
settings.SourceFile = filename;
return automation.Convert(
new List<ConversionSettings> { settings }, getContent
)[0];
}
Guid IWordAuto.ConvertAsync(ConversionSettings settings)
{
IWordAuto automation = this;
return automation.ConvertAsync(new List<ConversionSettings> { settings });
}
Guid IWordAuto.ConvertAsync(List<ConversionSettings> settings)
{
if (settings == null || settings.Count == 0)
{
return Guid.Empty;
}
string baselineSite = settings[0].Site;
ConversionOptions baselineOptions = settings[0].Options;
foreach (ConversionSettings entry in settings)
{
if (entry.Site != baselineSite ||
entry.Options.AddThumb != baselineOptions.AddThumb ||
entry.Options.WordMode != baselineOptions.WordMode ||
entry.Options.EmbedSystemFonts !=
baselineOptions.EmbedSystemFonts ||
entry.Options.EmbedFonts != baselineOptions.EmbedFonts ||
entry.Options.MarkupView != baselineOptions.MarkupView ||
entry.Options.TargetFormat != baselineOptions.TargetFormat ||
entry.Options.SaveAs != baselineOptions.SaveAs ||
entry.Options.ReviewStatus != baselineOptions.ReviewStatus ||
entry.Options.SubsetFonts != baselineOptions.SubsetFonts ||
entry.Options.UpdateFields != baselineOptions.UpdateFields)
{
throw new ArgumentException(
"Inconsistent conversion options. All conversions in " +
"a single job must use the same settings.");
}
}
using (WindowsImpersonationContext context =
WindowsIdentity.Impersonate(IntPtr.Zero))
{
using (SPSite site = new SPSite(settings[0].Site))
{
ConversionJob job = new ConversionJob(
ConfigurationManager.AppSettings["AutomationServiceName"]);
job.UserToken = site.UserToken;
job.Settings.AddThumbnail = settings[0].Options.AddThumb;
job.Settings.CompatibilityMode =
(CompatibilityMode)(int)settings[0].Options.WordMode;
job.Settings.DoNotEmbedSystemFonts =
!settings[0].Options.EmbedSystemFonts;
job.Settings.EmbedFonts = settings[0].Options.EmbedFonts;
job.Settings.MarkupView =
(MarkupTypes)(int)settings[0].Options.MarkupView;
job.Settings.OutputFormat =
(SaveFormat)(int)settings[0].Options.TargetFormat;
job.Settings.OutputSaveBehavior =
(SaveBehavior)(int)settings[0].Options.SaveAs;
job.Settings.RevisionState =
(RevisionState)(int)settings[0].Options.ReviewStatus;
job.Settings.SubsetEmbeddedFonts = settings[0].Options.SubsetFonts;
job.Settings.UpdateFields = settings[0].Options.UpdateFields;
string sourceFileUrl = "";
string targetFileUrl = "";
foreach (ConversionSettings entry in settings)
{
sourceFileUrl = string.Format(
"{0}/{1}/{2}",
entry.Site, entry.Library, entry.SourceFile);
targetFileUrl = string.Format(
"{0}/{1}/{2}",
entry.Site, entry.Library, entry.TargetFile);
job.AddFile(sourceFileUrl, targetFileUrl);
}
job.Start();
return job.JobId;
}
}
}
List<JobFailure> IWordAuto.GetJobFailures(Guid id)
{
using (WindowsImpersonationContext context =
WindowsIdentity.Impersonate(IntPtr.Zero))
{
ConversionJobStatus status = new ConversionJobStatus(
ConfigurationManager.AppSettings["AutomationServiceName"],
id,
null);
List<JobFailure> failures = new();
foreach (ConversionItemInfo info in status.GetItems(ItemTypes.Failed))
{
failures.Add(new JobFailure(
info.InputFile, info.OutputFile, info.ErrorMessage));
}
return failures;
}
}
decimal IWordAuto.GetJobStatus(Guid id)
{
using (WindowsImpersonationContext context =
WindowsIdentity.Impersonate(IntPtr.Zero))
{
ConversionJobStatus status = new ConversionJobStatus(
ConfigurationManager.AppSettings["AutomationServiceName"],
id,
null);
decimal percentageComplete = status.Count <= 0
? 100M
: (status.Succeeded + status.Failed) /
(decimal)status.Count * 100M;
return percentageComplete;
}
}
#endregion
#region ISharePointDocs Members
void ISharePointDocs.Delete(string site, string library, string filename)
{
using (WindowsImpersonationContext context =
WindowsIdentity.Impersonate(IntPtr.Zero))
{
using (SPWeb webSite = new SPSite(site).OpenWeb())
{
SPList documentLibrary = webSite.Lists[library];
SPListItem item = null;
foreach (SPListItem file in documentLibrary.Items)
{
if (string.Compare(
file.Name.Trim(), filename.Trim(), true) == 0)
{
item = file;
break;
}
}
if (item == null)
{
// File not present - just return.
return;
}
item.Delete();
}
}
}
void ISharePointDocs.Delete(int id)
{
// Sorry, never got around to implementing the delete functionality.
throw new NotImplementedException();
}
byte[] ISharePointDocs.Get(string site, string library, string filename)
{
using (WindowsImpersonationContext context =
WindowsIdentity.Impersonate(IntPtr.Zero))
{
using (SPWeb webSite = new SPSite(site).OpenWeb())
{
byte[] content = null;
SPList documentLibrary = webSite.Lists[library];
foreach (SPListItem item in documentLibrary.Items)
{
if (string.Compare(
item.Name.Trim(), filename.Trim(), true) == 0)
{
content = item.File.OpenBinary();
break;
}
}
if (content == null)
{
throw new Exception(
"The file does not exist on the server: " +
string.Format("{0}/{1}/{2}", site, library, filename));
}
return content;
}
}
}
byte[] ISharePointDocs.Get(int id)
{
throw new NotImplementedException();
}
List<string> ISharePointDocs.ListDocs(string site, string library)
{
using (WindowsImpersonationContext context =
WindowsIdentity.Impersonate(IntPtr.Zero))
{
using (SPWeb webSite = new SPSite(site).OpenWeb())
{
SPDocumentLibrary docLibrary =
webSite.Lists[library] as SPDocumentLibrary;
if (library == null)
{
throw new ArgumentException(
string.Format(
"List '{1}' does not exist or is not a document " +
"library in website '{0}'",
site, library));
}
List<string> filenames = new();
foreach (SPListItem item in docLibrary.Items)
{
filenames.Add(item.Name);
}
return filenames;
}
}
}
List<string> ISharePointDocs.ListLibraries(string site)
{
using (WindowsImpersonationContext context =
WindowsIdentity.Impersonate(IntPtr.Zero))
{
using (SPWeb webSite = new SPSite(site).OpenWeb())
{
List<string> names = new();
foreach (SPList item in webSite.Lists)
{
if (item is SPDocumentLibrary)
{
names.Add(item.Title);
}
}
return names;
}
}
}
void ISharePointDocs.Put(
string site, string library, string filename, byte[] content,
bool overwrite)
{
using (WindowsImpersonationContext context =
WindowsIdentity.Impersonate(IntPtr.Zero))
{
using (SPWeb webSite = new SPSite(site).OpenWeb())
{
SPList documentLibrary = webSite.Lists[library];
string destinationFileUrl = string.Format(
"{0}/{1}",
documentLibrary.RootFolder.ServerRelativeUrl,
filename);
webSite.AllowUnsafeUpdates = true;
webSite.Files.Add(destinationFileUrl, content, overwrite);
}
}
}
#endregion
}
}
Code language: C# (cs)
All that is left to do now is to instantiate the WCF service hosts in the Windows Service class.
namespace My.SharePoint.WordAutomation.Host
{
public sealed class HostingService : ServiceBase
{
private List<ServiceHost> hosts = new List<ServiceHost>();
protected override void OnStart(string[] args)
{
ServiceModelSectionGroup config =
ConfigurationManager.OpenExeConfiguration(
ConfigurationUserLevel.None)
.GetSectionGroup("system.serviceModel")
as ServiceModelSectionGroup;
foreach (ServiceElement service in config.Services.Services)
{
this.hosts.Add(new ServiceHost(Type.GetType(service.Name)));
}
foreach (ServiceHost host in this.hosts)
{
host.Open();
}
}
protected override void OnStop()
{
foreach (ServiceHost host in this.hosts)
{
try
{
host.Close();
}
catch {
host.Abort();
}
}
}
}
}
Code language: C# (cs)
One final item to note… Depending on your WCF configuration you may have issues hosting the capability as Windows Service – which needs to run as the SharePoint Administrator account to achieve the required permissions. That being the case, you may have security (encrypt and sign) issues with some authentication schemes. If you do, this can be overcome by proxying the public facing service. Essentially, you have a WCF service that runs locally on the SharePoint Server machine, running as the SharePoint Administrator account, and then you develop a separate public facing service that runs on SharePoint Server as well but which runs as the built-in Network Service account. Now, the public facing service passes security checks between machine endpoints and the internal tunnel between the public facing service and the one that interacts with SharePoint won’t have the security issue since they are both running on the same server.