How to make PSI Extensions in Project Server 2010 - Part I

In the SDK that accompanied Project Server 2007, one of the samples explains how to extend the Project Server Interface (PSI). The main point of PSI extensibility is the ability to deploy additional content (in terms of services) that can aggregate PSI methods and enhance Project Server functionality. Further, PSI extensions enable the same addressability and Project Server security as do the built-in PSI methods (PSIs). If you know the URL of the PSIs, you also have the URL of the extension – there is no other complicated URL to remember.

So, why I am writing this all over again? Well, the answer is that things have changed since the release of Office 2007. I will not go into all details, but two changes stand out: claims authentication and WCF. Over the course of the Microsoft SharePoint Foundation 2010 and Project Server 2010 release, the internal authentication infrastructure was changed to claims. A whole slew of very cool scenarios are now enabled by claims authentication. For Project Server, this means that all back-end services –the PSIs – are now placed in an access control list (ACL) by using claims. Checking the ACL is done by an entity referred to as “the gatekeeper”. The gatekeeper inspects the set of claims that travels along with the request, and if satisfied the gatekeeper allows the service invocation to occur. If not, the service invocation request is rejected. Secondly, using claims as the authentication mechanism mandates that WCF is the transport infrastructure as it is impossible to associate a claim with a regular ASMX web service call.

The primary change from Project Server 2007 and Project Server 2010 is mandated by the introduction of claims: Since all services on the backend are now ACL’ed using a claim (via the gatekeeper), if you were to call a service on the backend directly, you’d have to obtain a set of claims describing you the calling entity. This is certainly doable, but not trivial: It requires you to make a call to a suited STS and obtain the produced claim and associate it with the request you are about to make to the backend. So, to make this easier for you, we have changed the way you deploy PSI extensions. In this multi-part post, I will explain how to make a PSI extension service that lives and breathes under Project Server 2010. In this first part, I will get the skeleton service up and running and in the next part, I’ll try to actually do something useful with the extension service and also get a client working. Finally, I’ll try to illustrate what the changes are between a 2007 extension and a 2010 extension and hopefully give some guidance on what needs to change for a 2007 extension to be migrated to a 2010 extension.

PSI Extension Skeleton using Project Server 2010 – Part I

Because of the changes in the authentication and authorization infrastructure that occurred during the development of Office 2010, PSI extensions now are deployed on the front-end. Since the extension service will be a WCF service, a couple of things (configuration items) have to be accomplished in order for your service to be functional as a WCF service. The mantra to use when developing WCF services is A-B-C, or Address, Binding and Contract. In short, the address designates where the service is reachable, the binding determines transport properties like transport type, message size etc., and the contract defines what a request and a suitable response for the actual service operation looks like.

One of the really cool features of WCF is that many properties of your service are governed by configuration files so that you can focus on writing your business logic without having to worry about the infrastructure that makes it all possible. To that end, we will take care of the address and the binding in the configuration file that governs the front-end. Finding that particular configuration file can be accomplished in several ways – I have chosen to use PowerShell this time. The following script gets you the exact location of the configuration file we are interested in:

 

[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Web.Administration") > $null

$serverManager = New-Object -TypeName Microsoft.Web.Administration.ServerManager

$site = $serverManager.Sites["SharePoint - 80"]

foreach($app in $site.Applications)

{

   foreach($vdir in $app.VirtualDirectories)

   {

      $outputPath = $vdir.PhysicalPath + "\PSI\web.config"

      if([System.IO.File]::Exists($outputPath))

      {

         Write-Host $outputPath

      }

   }

}

$serverManager.Dispose()

$serverManager = $null

If all works out well, the previous script (you may have to run it in a PowerShell prompt with elevated privileges) should output the path to the configuration file that governs the PWA front-end. Note, you will most likely have to replace the site name (“SharePoint – 80” in my case) with what you have called your default SharePoint site.

Now, open the web.config file of the site, and make a few changes to the file. First, you’ll need to define what bindings your extension service will be using. Keep in mind that this will have to be compatible with the bindings configured for the SharePoint site that hosts your PWA. For the sake of this example, the binding entries, which go into the <system.serviceModel> section, look like this:

<bindings>

  <basicHttpBinding>

    <binding name="extensionBasicHttpConf"

             closeTimeout="00:01:00"

             openTimeout="00:01:00"

             receiveTimeout="00:10:00"

             sendTimeout="00:01:00"

             allowCookies="true"

             maxBufferSize="4194304"

             maxReceivedMessageSize="500000000"

             messageEncoding="Text"

             transferMode="StreamedResponse">

      <security mode="TransportCredentialOnly">

        <transport clientCredentialType="Windows" proxyCredentialType="Windows" realm="" />

      </security>

    </binding>

    <binding name="mexHttpBinding">

      <security mode="TransportCredentialOnly">

        <transport clientCredentialType="Windows" proxyCredentialType="Windows" realm="" />

      </security>

    </binding>

  </basicHttpBinding>

</bindings>

Note that you may not need to be using streamed responses and your timeouts should probably be a little more aggressive than what I have shown above. While developing the extension service, it may also be useful to you to have a service behavior that allows exception details to be populated into any fault that may occur. The following XML in the <system.serviceModel> section shows how to implement the fault behavior:

<behaviors>

<serviceBehaviors>

<behavior name="PSIExtensionServiceBehavior">

<serviceDebug includeExceptionDetailInFaults="true" />

<serviceMetadata httpGetEnabled="true" />

</behavior>

</serviceBehaviors>

</behaviors>

Finally, you’ll have to declare the extension service in terms of address. Here is the XML (again in the <system.serviceModel> section):

<services>

<service name="Project.Server.Extensions.ExtensionPSIService"

behaviorConfiguration="PSIExtensionServiceBehavior">

<endpoint address=""

binding="basicHttpBinding"

bindingConfiguration="extensionBasicHttpConf"

contract="Project.Server.Extensions.IExtensionPSIService" />

<endpoint address="mex"

binding="basicHttpBinding"

bindingConfiguration="mexHttpBinding"

name="mex"

contract="IMetadataExchange" />

</service>

</services>

With this addition to the config file, we have now declared the address that the extension service will be listening to, we have declared what binding will be used and finally we have the contract exposed by the service. Additionally, we have a metadata exchange (MEX) endpoint – useful for creating clients. This is something that you may or may not want in a production environment.

The next thing to do is to create the activation file. This is the file, when requested will initiate the activation of your service. By default, this file is called <service name>.svc and is placed in the same location as the web.config file that holds the configuration entries for the service. Note that the name you chose for the activation file is what you will be putting in the request URL when you try to access the service. I have named mine Service.svc, with the following content:

<%@ServiceHost language="C#"

               service="<Fully qualified type name>, Version=1.0.0.0, Culture=neutral, PublicKeyToken=<PublicKeyToken of your assembly>" %>

PSIExtensions is the name of the assembly that holds the service implementation. As you can see, I have strongly signed the assembly and put it in the GAC.

With all the configuration work behind us, we can now write the actual service. This will be the topic of the next part of this post, but for now I will explain how to get the skeleton up and running. In Visual Studio 2008 or Visual Studio 2010, create a new class library (you may also use the template to create a WCF service library – I typically do not because it creates more files than I need, and I like to be in total control as to what goes where).

Add a reference to System.ServiceModel for your class library, and add a new code file that will have the interface definition. It should, for now, look like this:

namespace Project.Server.Extensions

{

   [ServiceContract]

   internal interface IExtensionPSIService

   {

      [OperationContract]

      string EchoHello();

   }

}

Now, all we need is to have the actual service implementation. For that, add a new class to your assembly with the following content:

namespace Project.Server.Extensions

{

   [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]

   internal class ExtensionPSIService : IExtensionPSIService

   {

      #region IExtensionPSIService Members

      public string EchoHello()

      {

         return "Hello";

      }

      #endregion

   }

}

The AspNetCompatibilityRequirements attribute is needed as the web application that hosts your extension service (and the Project Server front-end service) requires ASP.NET compatibility – if you omit this attribute declaration, your service cannot be activated.

You should now be able to compile, register in the GAC, and activate the service. Open a browser and target the service activation file that you created. In my case, since I have deployed PWA in a “sub-site” called PWA, the URL looks like this: https://<servername>/pwa/_vti_bin/PSI/Service.svc (assuming that you have your PWA instance in an app called “pwa” – please update your URLs accordingly if that is not the case).

You should be getting the well-known “You have created a service.” page.