Walkthrough: Developing PSI Applications Using WCF

Applies to: Office 2010 | Project 2010 | Project Server 2010 | SharePoint Server 2010

In this article
Creating a Console Application for WCF
Adding Service References
Programming with the PSI
Using the Project Server Queuing Service
Complete Code Example

Microsoft Project Server 2010 uses the Windows Communication Foundation (WCF) for access to both the WCF service and ASMX Web service interfaces of the Project Server Interface (PSI). This article shows how to develop a WCF application for Project Server and how to use the Project Server Queuing Service. You can create the WCF bindings and endpoints entirely in code, or use the WCF Service Configuration Editor to help modify the app.config file for the application. Using an app.config file enables the application to be reconfigured without recompiling, for example to change the transport protocol from HTTP to direct TCP/IP. (Sample code in this article was adapted from a test application by Tiger Wang, Microsoft Corporation.)

This article includes the following sections:

  • Creating a Console Application for WCF

  • Adding Service References

    • Configuring the Services Programmatically

    • Configuring the Services with app.config

  • Programming with the PSI

  • Using the Project Server Queuing Service

  • Complete Code Example

For an introduction to the concepts of developing Project Server applications by using WCF, see Overview of WCF and the PSI. For general procedures to use in development, including tips on setting up Intellisense descriptions for the PSI and details about the three ways to add a service reference, see Prerequisites for WCF-Based Code Samples. See also Beginner's Guide to Windows Communication Foundation.

The Project 2010 SDK download includes two Microsoft Visual Studio projects for the WCFHelloProject application.

  • The WCFHelloProject_Prog directory in the download is the project for programmatic configuration of endpoints, by using Microsoft Visual Studio 2008 SP1. The solution does not use an app.config file. The references to the Project and QueueSystem services are set directly within Visual Studio.

    Warning

    As explained in Prerequisites for WCF-Based Code Samples, directly setting a service reference in the post-beta builds of Project Server 2010 requires temporarily replacing the web.config file for the Project Server service application. We recommend using a proxy assembly for the PSI services or adding a proxy source code file for each service the application uses. The proxy source code files are created when you run the script to create the proxy assembly. The Project 2010 SDK includes the proxy source code files.

  • The WCFHelloProject_vs10_CfgEd directory uses Microsoft Visual Studio 2010 and an app.config file. The references to the Project and QueueSystem services use the wcf.Project.cs and wcfQueueSystem.cs proxy source code files created by the SvcUtil.exe command in the script that creates the PSI proxy assembly.

Prerequisites

Developing WCF applications requires Microsoft Visual Studio 2008 SP1 or Visual Studio 2010. Applications must target the Microsoft .NET Framework 3.5. The procedures in this article use Microsoft Visual C#.

For Project Server applications, target the .NET Framework 3.5 instead of the .NET Framework 3.5 Client Profile, unless you are also including a client sandbox solution for Microsoft SharePoint Server 2010. For more information about SharePoint client development, see What's New: Client Object Model in the SharePoint Foundation 2010 SDK.

Steps in the procedures use Visual Studio running on a test installation of a Project Server computer.

Warning

You should develop Project Server applications on a test installation, rather than on a production installation, of Project Server.

Note

When you run the application on a separate computer, you must install Microsoft.Office.Project.Server.Library.dll with the application. The Project Server 2010 SDK download includes a license for redistribution of the Microsoft.Office.Project.Server.Library.dll assembly.

Claims-based authentication   All types of authentication for applications built on the SharePoint 2010 platform are claims-based. The WCFHelloProject example is designed for a Project Server installation that uses only Windows authentication. If Project Server is configured for multi-authentication (both Windows and Forms-based authentication), all calls to PSI methods must be enclosed in an OperationContextScope section, which adds a header to the outgoing Web request that blocks Forms-based authentication. For more information, see Prerequisites for WCF-Based Code Samples.

Creating a Console Application for WCF

The WCFHelloProject console application is a sample that can be reused for testing simple Project Server applications. Code in the Main method, the ParseCommandLine method, and the Usage method can be adapted for a wide variety of applications.

Procedure 1. To create a WCF console application for the PSI

  1. Run Visual Studio as an administrator, and then create a console application. For example, name the application WCFHelloProject. In the drop-down list at the top of the New Project dialog box, select .NET Framework 3.5 as the target framework.

  2. Add the following references:

    • System.Runtime.Serialization is used in the WCF service proxy code (Reference.cs) that is generated when you set a service reference.

    • System.ServiceModel is required for configuring WCF services. For a Web application, you would also need System.ServiceModel.Web. If you add ASMX Web services, you need System.Web.Services.

    • Microsoft.Office.Project.Server.Library is in the [Program Files]\Microsoft Office Servers\14.0\Bin\Microsoft.Office.Project.Server.Library.dll assembly. If the application will use Project Server events, also add a reference to the Microsoft.Office.Project.Server.Events.Receivers.dll assembly in the same directory.

  3. In the Program.cs file, add the following Program class variables. The last two variables are specific to the WCFHelloProject application.

    static private Uri pwaUri;          // URI of Project Web App.
    static private string pwaUrl;       // URL of Project Web App.
    static private string argError;     // Contains the argument that is in error.
    
    static private int numProjects;     // Number of projects to create.
    static private bool deleteProjects; // Specifies whether to delete projects.
    
  4. Create the ParseCommandLine method, which parses the command line and sets argError if an argument error exists. For sample code of all the methods in the Program.cs file, see the Complete Code Example.

  5. Create the Usage method, which displays the command usage.

  6. In the Main method, initialize the class variables. After you write the CreateProjects class (Procedure 2), add the code that runs when the ParseCommandLine method returns true.

    static void Main(string[] args)
    {
        pwaUrl = string.Empty;
        pwaUri = null;
        numProjects = 2;
        deleteProjects = true;
        argError = null;
    
        if (args.Length == 0)
        {
            Usage(argError);
        }
        else if (ParseCommandLine(args))
        {
            // TODO: Add code to instantiate the CreateProjects class and run its methods.
        }
        else
        {
            Usage(argError);
        }
        // Keep the Command Prompt window open for debugging.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey(true);
    }
    

Although you can add classes for the rest of the application to the Program.cs file, it is easier for code reuse to create separate class files for the main part of the application.

Adding Service References

To use the WCF interface of the PSI, you must add and configure service references. Procedure 2 shows how to add the service references. Procedure 3 shows how to configure the services programmatically, or you can optionally use Procedure 4 to configure the services by using an app.config file.

Namespace names that you give to service references are arbitrary, but it helps to maintain a convention for naming WCF service and ASMX Web service references. For example, the Project 2010 SDK generally uses Svc[ServiceName] for WCF services (such as SvcProject). The Visual Studio Intellisense file (ProjectServerServices.xml) in the SDK download for the PSI proxy assembly also uses the Svc prefix.

Tip

This article shows how to add service references directly within Visual Studio. Alternately, we recommend that you use the ProjectServerServices.dll proxy assembly in the Project 2010 SDK download, and set a reference to that assembly. The Prerequisites for WCF-Based Code Samples article includes detailed instructions for creating a service reference, as well as instructions for using the proxy assembly or using proxy source code files. One advantage of setting a reference to the ProjectServerServices.dll proxy assembly is that you can use the ProjectServerServices.xml file in the same directory so that Intellisense shows type and member descriptions.

Procedure 2. To add WCF service references

  1. Temporarily replace the web.config file in the back-end Project Service virtual directory, as described in How to: Create a Proxy Assembly for WCF Services. Run iisreset.

  2. In Visual Studio Solution Explorer, right-click the References node, and then click Add Service Reference. In the Address field of the Add Service Reference dialog box, type or paste the URL of the PSI service that you need. For the Project service, paste the following:

    https://localhost:32843/[GUID]/PSI/Project.svc
    

    The GUID is the name of the Project Server Services virtual directory in the SharePoint Web Services application (see the Internet Information Services (IIS) Manager). The virtual directory name is also the ID of the Project Server Service application in SharePoint. For more information about the service URL, see the Using the WCF and ASMX Interfaces section in Overview of WCF and the PSI.

    Tip

    To quickly find the GUID of the Project Server Service application, use a Windows PowerShell command that is installed with SharePoint Server 2010. On the Start menu, click All Programs, click Microsoft SharePoint 2010 Products, and then click SharePoint 2010 Management Shell. Following are the commands and the results (your GUID will be different).

    PS> $serviceName = "Project Server Service Application"

    PS> get-SPServiceApplication -name $serviceName | select Name, Id

    Name                                    Id

    Project Server Service Application      51125047-6279-4ae8-890a-b67a5daec75e

    The directory name is 5112504762794ae8890ab67a5daec75e (without the dashes).

  3. Type a name for the Project service namespace. For example, type SvcProject, and then click OK (Figure 1).

    Figure 1. Adding a WCF service reference

    Adding a WCF service reference

    When you add a service reference, Visual Studio also adds an app.config file that contains two default bindings and endpoints for the service. You can exclude or delete the app.config file if you use it only for WCF and configure the services programmatically (Procedure 3), or you can edit the app.config file by using the WCF Service Configuration Editor (Procedure 4).

  4. Similarly, add a reference to the QueueSystem service, and name it SvcQueueSystem.

  5. Before configuring the services, add a class file for the main application code to the WCFHelloProject project in Visual Studio. For example, in the Add New Item – WCFHelloProject dialog box, click the Class item, and then name the file CreateProjects.cs. Visual Studio adds the CreateProjects class.

  6. Add the following using statements to the default statements in the CreateProjects.cs file. The backendProject namespace alias and the backendQueueSystem namespace alias are optional. They are a reminder in the WCFHelloProject example that the Project and QueueSystem service references are for the local (back-end) Project Server services, and cannot be directly accessed from an external computer.

    using System.Net;
    using System.Security.Principal; // Required for the TokenImpersonationLevel enumeration.
    using System.ServiceModel;
    
    using PSLibrary = Microsoft.Office.Project.Server.Library;
    using backendProject = WCFHelloProject.SvcProject;
    using backendQueueSystem = WCFHelloProject.SvcQueueSystem;
    

    Instead of setting direct references to the Project and QueueSystem services, if you use the PSI proxy assembly or add the proxy source code files, the service namespaces are not children of the WCFHelloProject namespace. For example, the following statements set aliases for namespaces in the proxy source files:

    using backendProject = SvcProject;          // The wcfProject.cs file contains the SvcProject namespace.
    using backendQueueSystem = SvcQueueSystem;  // The wcf.QueueSystem.cs file contains the SvcQueueSystem namespace.
    
  7. Add class variables for data that the constructor initializes, so that the data is accessible to all methods in the class. In Procedure 3 or Procedure 4, you set the projectClient variable and the queueSystemClient variable to the endpoint address of the front-end (public) ProjectServer.svc router in Project Web App, to enable access to the back-end PSI service references.

    private static backendProject.ProjectClient projectClient;
    private static backendQueueSystem.QueueSystemClient queueSystemClient;
    private static string pwaUrl;
    private static int numProjects;
    private static bool deleteProjects;
    

    Notice that Intellisense shows ProjectClient and other interfaces and classes in the backendProject variable (which points to the back-end Project service).

    Note

    The client classes such as ProjectClient are present only in the WCF interface. If you add a front-end ASMX reference to the Project Web service, or set a reference to the ProjectServerServices.dll assembly that is built with the GenASMXProxyAssembly.cmd script in the SDK download, or add a proxy source code file such as wsdl.Project.cs, the ProjectClient class is not present.

  8. Add a constructor to initialize the CreateProjects class and a method to configure the service references. The CreateProjects class constructor uses the Uri parameter for the Project Web App URI, to enable programmatic access to all parts of the URL. The num parameter is the number of projects to create, and the delete parameter specifies whether to delete the projects after creating them.

    public CreateProjects(Uri uri, int num, bool delete)
    {
        numProjects = num;
        deleteProjects = delete;
    
        SetClientEndpoints(uri);
    }
    
    private static void SetClientEndpoints(Uri pwaUri)
    {
    }
    
  9. When you are finished setting service references, replace the original web.config file in the back-end Project Service virtual directory, as described in How to: Create a Proxy Assembly for WCF Services. Run iisreset.

You can use either Procedure 3 or Procedure 4 to add code to the SetClientEndpoints method and configure the services.

Configuring the Services Programmatically

One advantage to configuring the WCF services programmatically is that it is generally quicker and easier to paste a code example into the SetClientEndpoints method when you are learning to use WCF. Programmatic configuration can also be used when creating an application that modifies the Project Web App ribbon, where the app.config file should not be modified, such as described in Walkthrough: Customizing the PWA Ribbon and Accessing the JS Grid. Another feature of programmatic configuration (whether it is an advantage depends on the application requirements) is that it cannot be changed unless you modify and recompile the application.

If you need to change or add transport mechanisms, security settings, timeouts, and other settings for the application without recompiling, it is better to use the app.config file (Procedure 4).

Procedure 3. To configure the WCF services programmatically

  1. If it is used only for WCF configuration, exclude the app.config file that Visual Studio created when you set references to the PSI services.

  2. To create a binding for the client endpoints, add the following code to the SetClientEndpoints method.

     const int MAXSIZE = 500000000;
    
    // Set the final part of the URL address of the 
    // front-end ProjectServer.svc router.
    const string svcRouter = "_vti_bin/PSI/ProjectServer.svc";
    
    pwaUrl = pwaUri.Scheme + Uri.SchemeDelimiter + pwaUri.Host + ":"
        + pwaUri.Port + pwaUri.AbsolutePath;
    Console.WriteLine("URL: {0}", pwaUrl);
    
    // Create a basic binding that can be used for HTTP or HTTPS.
    BasicHttpBinding binding = null;
    
    if (pwaUri.Scheme.Equals(Uri.UriSchemeHttps))
    {
        // Initialize the HTTPS binding.
        binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
    }
    else
    {
        // Initialize the HTTP binding.
        binding = new BasicHttpBinding(
            BasicHttpSecurityMode.TransportCredentialOnly);
    }
    
  3. Set properties of the binding to enable use by the PSI-based application. The SendTimeout property should be a large enough time span to handle latency in accessing Project Server. The maximum message size property and the name table character count property should be large enough to handle very large datasets in Project Server. The MessageEncoding property must be set for text data, and the ClientCredentialType property must be set for NTLM credentials.

    binding.Name = "basicHttpConf";
    binding.SendTimeout = TimeSpan.MaxValue;
    binding.MaxReceivedMessageSize = MAXSIZE;
    binding.ReaderQuotas.MaxNameTableCharCount = MAXSIZE;
    binding.MessageEncoding = WSMessageEncoding.Text;
    binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Ntlm;
    
  4. Create an endpoint address that points to the front-end ProjectService.svc router in Project Web App. Finally, initialize each of the client variables with the WCF binding and endpoint address. The channel factory includes security and other properties of the underlying transport mechanism of the WCF service model.

    // The endpoint address is the ProjectServer.svc router for all public PSI calls.
    EndpointAddress address = new EndpointAddress(pwaUrl + svcRouter);
    
    projectClient = new backendProject.ProjectClient(binding, address);
    projectClient.ChannelFactory.Credentials.Windows.AllowedImpersonationLevel
        = TokenImpersonationLevel.Impersonation;
    projectClient.ChannelFactory.Credentials.Windows.AllowNtlm = true;
    
    queueSystemClient = new backendQueueSystem.QueueSystemClient(binding, address);
    queueSystemClient.ChannelFactory.Credentials.Windows.AllowedImpersonationLevel
        = TokenImpersonationLevel.Impersonation;
    queueSystemClient.ChannelFactory.Credentials.Windows.AllowNtlm = true;
    
  5. Add code in the Program.Main method to initialize the CreateProjects object. Leave calls to the Create method and the DisposeClients method commented out, until you write those methods.

    . . .
    else if (ParseCommandLine(args))
    {
        // The command is valid, so instantiate the CreateProjects object.
        CreateProjects projectCreator = 
            new CreateProjects(pwaUri, numProjects, deleteProjects);
    
        //projectCreator.Create();
        //projectCreator.DisposeClients();
    }
    . . .
    
  6. Test the application, to determine whether the CreateProjects object is initialized correctly:

    1. In the WCFHelloProject Properties pane, click the Debug tab, and then add the following to the Command line arguments text box (assuming your Project Web App instance is named pwa): -pwaUrl https://localhost/pwa

    2. Set a breakpoint on the end bracket ("}") of the SetClientEndpoints method, and then run the application.

    3. Check the values of some WCF settings. For example, check the value of projectClient.ChannelFactory. It should be: {System.ServiceModel.ChannelFactory<WCFHelloProject.SvcProject.Project>}.

    4. Check the value of projectClient.Endpoint. It should be: Address={https://localhost/pwa/_vti_bin/PSI/ProjectServer.svc}.

    5. When you step to the end of the application, the Command Prompt window shows the following:

      URL: https://localhost:80/pwa/
      Press any key to exit.
      

Configuring the Services with app.config

One advantage of configuring WCF services by using an app.config or web.config file is that you can change endpoint bindings without recompiling the application. For example, you can change the transport from HTTP to TCP. Another advantage is that you can use the WCF Service Configuration Editor to add or modify bindings and endpoints. A disadvantage of using app.config and the WCF Service Configuration Editor is that there are many settings to learn so that the binding works correctly with Project Server.

The default bindings and endpoints that Visual Studio creates in the app.config file do not work with Project Server. Procedure 4 shows how to create new bindings and endpoints, rather than modify the defaults.

Procedure 4. To configure the WCF services with app.config

  1. Close the WCFHelloProject solution in Visual Studio, copy the WCFHelloProject directory to another location, and then open the WCFHelloProject.sln file in the new location.

  2. If you excluded the app.config file from the project, add it back in. Right-click the WCFHelloProject project in Solution Explorer, click Add, click Existing Item, and then navigate to the app.config file in the new project location.

    If you deleted the app.config file in Procedure 3, rather than just excluding it from the project, create a new app.config file. If you used the ProjectServerServices.dll proxy assembly or the wcf.Project.cs and wcf.QueueSystem.cs files in the Project 2010 SDK download, you must also create an app.config file. For information about how to create an app.config file, see the Adding a Service Configuration File section in Prerequisites for WCF-Based Code Samples.

  3. In Visual Studio, on the Tools menu, click WCF Service Configuration Editor. In the Microsoft Service Configuration Editor application window, on the File menu, click Open, click Config File, and then navigate to the app.config file for the solution.

    Tip

    After you open the app.config file the first time, you can right-click the file in Solution Explorer, and then click Edit WCF Configuration.

    If Microsoft Service Configuration Editor shows an error such as "Unrecognized attribute 'decompressionEnabled' …", click OK, and then edit the app.config file in Visual Studio to remove the unsupported attribute in all locations. Save app.config, and then restart Microsoft Service Configuration Editor.

    When you directly set a service reference, Visual Studio creates two default bindings for each WCF service in your application (Figure 2). Visual Studio also creates two default endpoints for each service, and these endpoints use the default bindings. For example, the CustomBinding_Project endpoint uses the HTTPS protocol and the CustomBinding_Project1 endpoint uses the NET.TCP protocol.

    Figure 2. Default endpoints and bindings in app.config, using Visual Studio 2010 with .NET Framework 3.5

    Default endpoints and bindings in app.config

  4. In the Microsoft Service Configuration Editor window, in the Configuration pane, right-click Bindings, and then click New Binding Configuration. In the Create a New Binding dialog box, click basicHttpBinding, and then click OK. Rename the binding in the basicHttpBinding pane. For example, type basicHttpConf in the Name text box.

    Change the general properties in the Binding tab, as in Table 1.

    Table 1. Binding properties

    Property

    Value

    Comment

    MaxBufferSize

    500000000

    MaxReceivedMessageSize

    500000000

    SendTimeout

    10675199.02:48:05.4775807 is the maximum possible. Try 01:00:00.

    The large value corresponds to the TimeSpan.MaxValue property, which is 10675199 days, 2 hours, 48 minutes, and approximately 5 seconds—which is longer than 29,000 years. You can reduce the value, for example, to a more reasonable 01:00:00 (one hour).

    MaxArrayLength

    16384

    MaxBytesPerRead

    4096

    MaxDepth

    32

    MaxNameTableCharCount

    500000000

    MaxStringContentLength

    8192

  5. On the basicHttpBinding pane, click the Security tab, and then change the property values as in Table 2.

    Table 2. Security properties

    Property

    Value

    Comment

    Mode

    TransportCredentialOnly

    The TransportCredentialOnly mode passes user credentials without encrypting or signing the messages. If the Realm URI uses the HTTPS protocol, set the Mode attribute to Transport. For more information, see Programming WCF Security.

    Realm

    [http://SecurityDomain]

    Security domain, such as https://microsoft.com. The default is an empty string. For more information, see Claims-Based Architectures.

    TransportClientCredentialType

    Ntlm

    For information about security bindings, see Transport Security Overview.

  6. In the Configuration pane, expand the Advanced node, click Endpoint Behaviors, and then click New Endpoint Behavior Configuration. In the Behavior pane, change the Name field to basicHttpBehavior.

  7. In the Behavior: basicHttpBehavior pane, click Add. In the Adding Behavior Element Extension Sections dialog box, click clientCredentials, and then click Add.

  8. In the Configuration pane, under basicHttpBehavior, click the clientCredentials child node, and then change the AllowedImpersonationLevel to Impersonation. Leave the default values for the other properties (SupportInteractive = True, ImpersonationLevel = Identification, and AllowNtlm = True). Leave the child nodes under clientCredentials with the default values.

  9. Create an endpoint that uses the basicHttpBinding, for each WCF service in the application.

    Procedure 4a. To create client endpoints

    1. In the Configuration pane, right-click the Endpoints node, and then click New Client Endpoint. In the Client Endpoint pane, type a name for the endpoint; for example, type basicHttp_Project for the Project service.

    2. In the Address field, type the URL of the ProjectServer.svc router in Project Web App, for example, https://ServerName/ProjectServerName/_vti_bin/PSI/ProjectServer.svc.

    3. In the BehaviorConfiguration field, in the drop-down list, select the name of the custom endpoint behavior that you previously created. For example, select basicHttpBehavior.

    4. In the Binding field, select the type of binding. In this case, select basicHttpBinding.

    5. In the BindingConfiguration field, select the binding that you previously created. In this case, select basicHttpConf.

    6. Click the Contract field, and then click the button to browse to the executable that contains the WCF contract. In the Contract Type Browser dialog box, navigate to the executable file in the \bin\Debug subdirectory of the WCFHelloProject directory, click the WCFHelloProject.exe file, and then click Open. The Contract Type Browser shows the service classes in the executable (Figure 3); click SvcProject.Project, and then click Open.

      Note

      Visual Studio 2010 shows an error dialog with the message, "Could not load file or assembly …" You can also set the service type directly in the Contract field by typing the namespace and service class name. For example, type SvcProject.Project.

      You can also see the interface name for the WCF contract in the Visual Studio Object Browser. In the Object Browser tab, expand the WCFHelloProject node. The SvcProject namespace, for example, contains the Project interface.

      Figure 3. Using the Contract Type Browser (with Visual Studio 2008)

      Using the Contract Type browser

    7. In the same way, create an endpoint for the QueueSystem service. For example, name the endpoint basicHttp_QueueSystem, and set the Contract field to SvcQueueSystem.QueueSystem. The other fields in the endpoint are the same as for the Project endpoint.

    8. In the Microsoft Service Configuration Editor, on the File menu, click Save, and then close the editor.

  10. Check the contents of the behaviors, bindings, and endpoints elements in the app.config file.

    <behaviors>
      <endpointBehaviors>
        <behavior name="basicHttpBehavior">
          <clientCredentials>
            <windows allowedImpersonationLevel="Impersonation" />
          </clientCredentials>
        </behavior>
        <!-- (More behaviors.) -->
      </endpointBehaviors>
    </behaviors>
     . . .
    <bindings>
      <basicHttpBinding>
        <binding name="basicHttpConf" sendTimeout="01:00:00" maxBufferSize="500000000"
            maxReceivedMessageSize="500000000">
          <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
              maxBytesPerRead="4096" maxNameTableCharCount="500000000" />
          <security mode="TransportCredentialOnly">
            <transport clientCredentialType="Ntlm" realm="http//SecurityDomain" />
          </security>
        </binding>
      </basicHttpBinding>
      <!-- (More bindings.) -->
    </bindings>
    . . .
    <client>
      <endpoint address="https://ServerName/ProjectServerName/_vti_bin/PSI/ProjectServer.svc"
          behaviorConfiguration="basicHttpBehavior" binding="basicHttpBinding"
          bindingConfiguration="basicHttpConf" contract="SvcProject.Project"
          name="basicHttp_Project" kind="" endpointConfiguration="" />
      <endpoint address="https://ServerName/ProjectServerName/_vti_bin/PSI/ProjectServer.svc"
          behaviorConfiguration="basicHttpBehavior" binding="basicHttpBinding"
          bindingConfiguration="basicHttpConf" contract="SvcQueueSystem.QueueSystem"
          name="basicHttp_QueueSystem" />
      <!-- (More client endpoints.)-->
    </client>
    

    Tip

    After you create the set of behaviors, bindings, and endpoints that work for one group of WCF services, you can use those sections in the app.config file as a template to create similar sets, without going through the process using the Microsoft Service Configuration Editor.

  11. In the CreateProjects.cs file, initialize the projectClient object and the queueSystemClient object with the endpoint names specified in app.config.

    Note

    The SetClientEndpoints method does not need the pwaUri parameter because the WCF settings are in app.config, not set programmatically as in Procedure 3.

    private static void SetClientEndpoints()
    {
        projectClient = new backendProject.ProjectClient("basicHttp_Project");
        queueSystemClient = new backendQueueSystem.QueueSystemClient("basicHttp_QueueSystem");
    }
    
  12. Set the command-line arguments for debugging, as in Procedure 3, Step 6. Set a breakpoint in the SetClientEndpoints method, step through the statements, and then check the values of some WCF settings, such as the projectClient.ChannelFactory property value and the projectClient.Endpoint property value.

When the application runs and the client endpoints are set correctly, you can use the projectClient object and the queueSystemClient object to call methods in the Project services and the QueueSystem services of the PSI.

Programming with the PSI

You can use either Procedure 3 (programmatic configuration) or Procedure 4 (using app.config) to initialize the projectClient object and the queueSystemClient object with endpoints that go through the public ProjectServer.svc router in Project Web App. Procedure 5 shows how to add two methods that use PSI calls to create and delete projects.

Procedure 5. To develop the Create method

  1. Prepare the data required by the PSI calls. The Create method in the CreateProjects class iterates over the QueueCreateProject method for the number of projects to create. The QueueCreateProject parameters are jobUid (for the Project Server Queue job GUID), dataset (which is a ProjectDataSet object with one ProjectRow for each project to create), and validateOnly (which specifies whether to only validate the data instead of creating the project).

    The projDs object is instantiated from the ProjectDataSet type definition in the backendProject namespace alias (you can find the type definition in the reference.cs file in the SvcProject service reference). The projRow object of type ProjectRow must be created by the NewProjectRow method of the ProjectDataTable in the projDs object, because the row of project data belongs in that specific ProjectDataSet object.

    At least three properties are necessary to create a project: project type, project GUID, and project name. After those properties are set in the project row, the AddProjectRow method adds the row to the projDs object.

    public void Create()
    {
        // Prepare the data.
        Int32 projectType = Convert.ToInt32(PSLibrary.Project.ProjectType.Project);
        Guid[] projectUids = new Guid[numProjects];
        backendProject.ProjectDataSet[] projDs = new backendProject.ProjectDataSet[numProjects];
    
        for (int i = 0; i < numProjects; i++)
        {
            projectUids[i] = Guid.NewGuid();
            projDs[i] = new backendProject.ProjectDataSet();
            backendProject.ProjectDataSet.ProjectRow projRow = projDs[i].Project.NewProjectRow();
    
            projRow.PROJ_TYPE = projectType;
            projRow.PROJ_UID = projectUids[i];
            projRow.PROJ_NAME = "WCFTEST_" + projectUids[i].ToString();
    
            projDs[i].Project.AddProjectRow(projRow);
        }
        . . .
    }
    
  2. Call the QueueCreateProject method of the projectClient object (that is, the object whose endpoint is to the public ProjectServer.svc service) for each ProjectDataSet. The console output in the following code is for the WCFHelloProject test application to show how long it takes to make the number of calls specified.

    // Create the projects.
    Console.ForegroundColor = ConsoleColor.Yellow;
    Console.WriteLine(string.Format("Creating {0} projects via WCF.",
                                    numProjects.ToString()));
    Console.ResetColor();
    
    DateTime startTime = DateTime.Now;
    
    for (int i = 0; i < numProjects; i++)
    {
        projectClient.QueueCreateProject((projectUids[i], projDs[i], false);
    }
    
    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine("Time: {0} ms",
        (new TimeSpan((DateTime.Now.Ticks - startTime.Ticks))).TotalMilliseconds);
    Console.ResetColor();
    
  3. For the remaining lines of the Create method, the following code uses the Helpers.WaitForQueue method, which waits for the Project Server Queuing service to finish creating the projects (see Procedure 6). The CreateProjects.Delete method is explained in the next step.

    Helpers.WaitForQueue(backendQueueSystem.QueueMsgType.ProjectCreate,
                         numProjects, queueSystemClient, startTime);
    
    if (deleteProjects) Delete(projectUids);
    
  4. Create the Delete method in the CreateProjects class. Delete writes information to the console about the number of projects it is deleting, calls the QueueDeleteProjects method, and then writes the number of milliseconds elapsed for Project Server to delete the projects.

    public void Delete(Guid[] projUids)
    {
        Console.Write("Deleting {0} project(s)...", numProjects);
    
        DateTime startTime = DateTime.Now;
        Guid jobUid = Guid.NewGuid();
    
        projectClient.QueueDeleteProjects(jobUid, true, projUids, true);
    
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine("\nTime: {0} ms",
            (new TimeSpan((DateTime.Now.Ticks - startTime.Ticks))).TotalMilliseconds);
        Console.ResetColor();
    
        Helpers.WaitForQueue(backendQueueSystem.QueueMsgType.ProjectDelete,
                             numProjects, queueSystemClient, startTime);
    }
    

Before you try to test the application, write the Helpers class with the WaitForQueue method.

Using the Project Server Queuing Service

The ReadMyJobStatus method in the QueueSystem service enables an application to read the status of specified job types in the Project Server Queuing Service and wait for the jobs to complete. If you run the WCFHelloProject application without waiting for the queue, it tries to delete the projects before all of them are created, and raises an exception.

Procedure 6. To use the QueueSystem service

  1. Add a class to the WCFHelloProject application that has a utility method for using the QueueSystem service. For example, in the CreateProjects.cs file, add the Helpers class with a WaitForQueue method. WaitForQueue will call the ReadMyJobStatus method in the QueueSystem service, which has parameters that require the job types to check, number of jobs, and start time. Calling ReadMyJobStatus requires the queueSystemClient object to access the QueueSystem service through the front-end ProjectServer.svc router.

    class Helpers
    {
        public static bool WaitForQueue(backendQueueSystem.QueueMsgType jobType, int numJobs,
                                        backendQueueSystem.QueueSystemClient queueSystemClient,
                                        DateTime startTime)
        {
            . . .
        }
    }
    
  2. In the WaitForQueue method, add the following code. The maxSeconds2Wait constant enables the application to bail out if there is a problem or if the Project Server Queuing Service takes too long. The ReadMyJobStatus method returns a QueueStatusDataSet in the queueStatusDs object.

    While the number of jobs processed is less than the total number, and the elapsed time is less than the specified maximum, call ReadMyJobStatus every second (1000 milliseconds). Sort the results in the queueStatusDs object by the QueuePosition column and in the last sort order used.

    WaitForQueue returns true when all the queue jobs are processed or if wait time exceeds the maximum time.

    const int maxSeconds2Wait = 50;
    backendQueueSystem.QueueStatusDataSet queueStatusDs = new backendQueueSystem.QueueStatusDataSet();
    
    int timeout = 0;    // Number of seconds waited.
    Console.Write("Waiting for job " + jobType.ToString());
    
    backendQueueSystem.QueueMsgType[] messageTypes = { jobType };
    backendQueueSystem.JobState[] jobStates = { backendQueueSystem.JobState.Success };
    
    while ((timeout < maxSeconds2Wait) && (queueStatusDs.Status.Count < numJobs))
    {
        System.Threading.Thread.Sleep(1000);
    
        queueStatusDs = queueSystemClient.ReadMyJobStatus(
            messageTypes,
            jobStates,
            startTime,
            DateTime.Now,
            numJobs,
            true,
            backendQueueSystem.SortColumn.QueuePosition,
            backendQueueSystem.SortOrder.LastOrder);
    
        timeout++;
        Console.Write(".");
    }
    Console.WriteLine();
    
    if (queueStatusDs.Status.Count == numJobs)
    {
        return true;
    }
    return false;
    
  3. Uncomment the following two lines in the Program.Main class, to run and test the application.

    projectCreator.Create();
    projectCreator.DisposeClients();
    
  4. Debugging: To more easily check the contents of a DataSet, use the DataSet Visualizer tool. For example, set a breakpoint on the if (queStatusDS.Status.Count == numJobs) line in the WaitForQueue method. When the application hits the breakpoint, hover the mouse pointer over queueStatusDs, and then click the small magnifying glass icon in the pop-up debug value. Figure 4 shows the DataSet Visualizer dialog box. You can visually examine all of the properties of the rows in any table of the DataSet, or copy the datarows into a text editor. The QueueStatusDataSet contains only the Status table. Because the default execution of WCFHelloProject creates two projects, the Project Server Queuing Service runs only two queue jobs of MessageType = 22 for the application user. The QueueConstants.QueueMessageType enumeration in the Microsoft.Office.Project.Server.Library namespace (QueueConstants.QueueMsgType) shows that the message type value 22 is ProjectCreate.

    Figure 4. Using the DataSet Visualizer for debugging

    Using the DataSet Visualizer

If you run the WCFHelloProject application several times in quick succession, the queue job times typically decrease for several runs, and then remain close in value to the previous run after about three or four runs. For example, the following output from five runs shows a great decrease in time after the initial run. That is because an algorithm in the Project Server Queuing System maintains an internal record of types and frequency of queue jobs and adjusts the queue to process similar jobs more quickly, if they come in rapid succession. Thus Project Server can more efficiently handle a rapid influx of similar jobs, such as timesheet submissions. For more information about the QueueSystem service, see How to: Use the QueueSystem Service.

URL: https://localhost:80/pwa/
Creating 2 projects via WCF.
Time: 57004.164 ms
Waiting for job ProjectCreate.......
Deleting 2 project(s)...
Time: 852.4845 ms
Waiting for job ProjectDelete.
_________________________________
URL: https://localhost:80/pwa/
Creating 2 projects via WCF.
Time: 3779.055 ms
Waiting for job ProjectCreate.
Deleting 2 project(s)...
Time: 1267.497 ms
Waiting for job ProjectDelete.
_________________________________
URL: https://localhost:80/pwa/
Creating 2 projects via WCF.
Time: 2144.394 ms
Waiting for job ProjectCreate.
Deleting 2 project(s)...
Time: 350.5635 ms
Waiting for job ProjectDelete.
_________________________________
URL: https://localhost:80/pwa/
Creating 2 projects via WCF.
Time: 1719.6165 ms
Waiting for job ProjectCreate.
Deleting 2 project(s)...
Time: 335.916 ms
Waiting for job ProjectDelete..
_________________________________
URL: https://localhost:80/pwa/
Creating 2 projects via WCF.
Time: 2259.621 ms
Waiting for job ProjectCreate.
Deleting 2 project(s)...
Time: 361.305 ms
Waiting for job ProjectDelete.

The WCFHelloProject application uses calls to the PSI through WCF to create a specified number of projects in the Drafts database of Project Server, and then shows the time it takes to create and then to delete the projects. You can set endpoints for the PSI client objects by programmatic configuration or by using app.config and the Microsoft Service Configuration Editor in Visual Studio. Figure 5 shows a screenshot of the console window for the running application.

Figure 5. Console window output of WCFHelloProject.exe

Console window output of WCFHelloProject

Complete Code Example

Following is the complete code in the Program.cs file. The ParseCommandLine method in the Program class validates the command-line arguments. The Main method instantiates the CreateProjects object.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace WCFHelloProject
{
    class Program
    {
        static private Uri pwaUri;          // URI of Project Web App.
        static private string pwaUrl;       // URL of Project Web App.
        static private string argError;     // Contains the argument that is in error.

        static private int numProjects;     // Number of projects to create.
        static private bool deleteProjects; // Specifies whether to delete projects.

        static void Main(string[] args)
        {
            pwaUrl = string.Empty;
            pwaUri = null;
            numProjects = 2;
            deleteProjects = true;
            argError = null;

            if (args.Length == 0)
            {
                Usage(argError);
            }
            else if (ParseCommandLine(args))
            {
                // The command is valid, so instantiate the CreateProjects object.
                CreateProjects projectCreator = 
                    new CreateProjects(pwaUri, numProjects, deleteProjects);

                projectCreator.Create();
                projectCreator.DisposeClients();
            }
            else
            {
                Usage(argError);
            }
            // Keep the Command Prompt window open for debugging.
            Console.WriteLine("Press any key to exit.");
            Console.ReadKey(true);
        }

        // Parse the command line.
        static private bool ParseCommandLine(string[] args)
        {
            const int MAXNUM = 20;
            bool error = false;
            bool argErr = false;

            int argsLength = args.Length;

            for (int i = 0; i < args.Length; ++i)
            {
                if (error) break;

                switch (args[i].ToLower())
                {
                    case "/pwaurl":
                    case "-pwaurl":
                        i++;
                        if (i >= argsLength) return false;
                        pwaUrl = args[i];

                        if (pwaUrl.ToLower().StartsWith("http"))
                        {
                            // Add a trailing slash, if it is not present.
                            if (pwaUrl.LastIndexOf("/") != pwaUrl.Length - 1)
                                pwaUrl += "/";

                            // Convert to a URI, for easier access to properties.
                            pwaUri = new Uri(pwaUrl);
                        }
                        else
                            argErr = true;
                        break;

                    case "/numprojects":
                    case "-numprojects":
                        i++;
                        if (i >= argsLength) return false;
                        numProjects = Convert.ToInt32(args[i]);

                        if (numProjects < 0 || numProjects > MAXNUM) argErr = true;
                        break;

                    case "/delete":
                    case "-delete":
                        i++;
                        if (i >= argsLength) return false;

                        string theArg = args[i].ToLower();

                        switch (theArg)
                        {
                            case "t":
                            case "true":
                            case "1":
                                deleteProjects = true;
                                break;
                            case "f":
                            case "false":
                            case "0":
                                deleteProjects = false;
                                break;
                            default:
                                argErr = true;
                                break;
                        }
                        break;

                    case "/?":
                    case "-?":
                        error = true;
                        break;

                    default:
                        argError = args[i];
                        error = true;
                        break;
                }
            }
            if (pwaUrl == string.Empty) error = true;
            if (argErr) error = true;

            return !error;
        }

        // Show the command usage.
        static private void Usage(String errorInfo)
        {
            if (errorInfo != null)
            {
                // A command-line argument error occurred. Report it to the user.
                Console.WriteLine("Error: {0} is not an argument.\n", errorInfo);
            }

            Console.WriteLine(string.Format(
                "Usage:  [/?] -{0} {1} [-{2} {3}] [-{4} {5}]",
                "pwaUrl", @"""<URL>""",
                "numProjects", "<0 - 20>",
                "delete", "<T | F>"));

            Console.WriteLine(
                "\n\tpwaUrl:\t\tExample: https://ServerName/pwa"
                + "\n\n\tnumProjects:\tOptional. Number of projects to create."
                + "\n\t\t\tThe default is 2; maximum is 20."
                + "\n\n\tdelete:\t\tOptional. Delete projects after creating them."
                + "\n\t\t\tThe default is true.");
        }   
    }
}

Following is the complete code for the CreateProjects class and the Helpers class in the CreateProjects.cs file. The SetClientEndpoints method sets client endpoints programmatically, rather than with app.config. For the code sample that uses app.config, see the WCFHelloProject_vs10_CfgEd directory in the Project 2010 SDK download.

using System;
using System.Net;
using System.ServiceModel;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PSLibrary = Microsoft.Office.Project.Server.Library;
using backendProject = WCFHelloProject.SvcProject;
using backendQueueSystem = WCFHelloProject.SvcQueueSystem;

namespace WCFHelloProject
{
    class CreateProjects
    {
        private static backendProject.ProjectClient projectClient;
        private static backendQueueSystem.QueueSystemClient queueSystemClient;
        private static string pwaUrl;
        private static int numProjects;
        private static bool deleteProjects;

        public CreateProjects(Uri uri, int num, bool delete)
        {
            numProjects = num;
            deleteProjects = delete;
 
           SetClientEndpoints(uri);
        }

        private static void SetClientEndpoints(Uri pwaUri)
        {
            const int MAXSIZE = 500000000;
            const string svcRouter = "_vti_bin/PSI/ProjectServer.svc";

            pwaUrl = pwaUri.Scheme + Uri.SchemeDelimiter + pwaUri.Host + ":"
                + pwaUri.Port + pwaUri.AbsolutePath;
            Console.WriteLine("URL: {0}", pwaUrl);

            // Create a binding for HTTP.

            BasicHttpBinding binding = null;

            if (pwaUri.Scheme.Equals(Uri.UriSchemeHttps))
            {
                // Create binding for HTTPS.
                binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
            }
            else
            {
                // Create binding for HTTP.
                binding = new BasicHttpBinding(BasicHttpSecurityMode.TransportCredentialOnly);
            }

            binding.Name = "basicHttpConf";
            binding.SendTimeout = TimeSpan.MaxValue;
            Console.WriteLine("SendTimeout value:\n\t{0} days,\n\t{1} hours,\n\t{2} minutes,\n\t{3} seconds",
                binding.SendTimeout.Days.ToString(), binding.SendTimeout.Hours.ToString(),
                binding.SendTimeout.Minutes.ToString(), binding.SendTimeout.Seconds.ToString());

            binding.MaxReceivedMessageSize = MAXSIZE;
            binding.ReaderQuotas.MaxNameTableCharCount = MAXSIZE;
            binding.MessageEncoding = WSMessageEncoding.Text;
            binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Ntlm;

            // The endpoint address is the ProjectServer.svc router for all public PSI calls.
            EndpointAddress address = new EndpointAddress(pwaUrl + svcRouter);

            projectClient = new backendProject.ProjectClient(binding, address);
            projectClient.ChannelFactory.Credentials.Windows.AllowedImpersonationLevel
                = TokenImpersonationLevel.Impersonation;
            projectClient.ChannelFactory.Credentials.Windows.AllowNtlm = true;

            queueSystemClient = new backendQueueSystem.QueueSystemClient(binding, address);
            queueSystemClient.ChannelFactory.Credentials.Windows.AllowedImpersonationLevel
                = TokenImpersonationLevel.Impersonation;
            queueSystemClient.ChannelFactory.Credentials.Windows.AllowNtlm = true;
        }

        public void DisposeClients()
        {
            projectClient.Close();
            queueSystemClient.Close();
        }

        public void Create()
        {
            // Prepare the data.
            Int32 projectType = Convert.ToInt32(PSLibrary.Project.ProjectType.Project);
            Guid[] projectUids = new Guid[numProjects];
            backendProject.ProjectDataSet[] projDs = new backendProject.ProjectDataSet[numProjects];

            for (int i = 0; i < numProjects; i++)
            {
                projectUids[i] = Guid.NewGuid();
                projDs[i] = new backendProject.ProjectDataSet();
                backendProject.ProjectDataSet.ProjectRow projRow = projDs[i].Project.NewProjectRow();

                projRow.PROJ_TYPE = projectType;
                projRow.PROJ_UID = projectUids[i];
                projRow.PROJ_NAME = "WCFTEST_" + projectUids[i].ToString();

                projDs[i].Project.AddProjectRow(projRow);
            }

            // Create the projects.
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine(string.Format("Creating {0} projects via WCF.",
                                            numProjects.ToString()));
            Console.ResetColor();

            DateTime startTime = DateTime.Now;

            for (int i = 0; i < numProjects; i++)
            {
                projectClient.QueueCreateProject(projectUids[i], projDs[i], false);
            }

            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("Time: {0} ms",
                (new TimeSpan((DateTime.Now.Ticks - startTime.Ticks))).TotalMilliseconds);
            Console.ResetColor();

            Helpers.WaitForQueue(backendQueueSystem.QueueMsgType.ProjectCreate,
                                 numProjects, queueSystemClient, startTime);

            if (deleteProjects) Delete(projectUids);
        }

        public void Delete(Guid[] projUids)
        {
            Console.Write("Deleting {0} project(s)...", numProjects);

            DateTime startTime = DateTime.Now;
            Guid jobUid = Guid.NewGuid();

            projectClient.QueueDeleteProjects(jobUid, true, projUids, true);

            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("\nTime: {0} ms",
                (new TimeSpan((DateTime.Now.Ticks - startTime.Ticks))).TotalMilliseconds);
            Console.ResetColor();

            Helpers.WaitForQueue(backendQueueSystem.QueueMsgType.ProjectDelete,
                                 numProjects, queueSystemClient, startTime);
        }
    }

    class Helpers
    {
        public static bool WaitForQueue(backendQueueSystem.QueueMsgType jobType, int numJobs,
                                        backendQueueSystem.QueueSystemClient queueSystemClient,
                                        DateTime startTime)
        {
          const int maxSeconds2Wait = 50;
          backendQueueSystem.QueueStatusDataSet queueStatusDs = new backendQueueSystem.QueueStatusDataSet();

            int timeout = 0;    // Number of seconds waited.
            Console.Write("Waiting for job " + jobType.ToString());

            backendQueueSystem.QueueMsgType[] messageTypes = { jobType };
            backendQueueSystem.JobState[] jobStates = { backendQueueSystem.JobState.Success };

            while ((timeout < maxSeconds2Wait) && (queueStatusDs.Status.Count < numJobs))
            {
                System.Threading.Thread.Sleep(1000);

                queueStatusDs = queueSystemClient.ReadMyJobStatus(
                    messageTypes,
                    jobStates,
                    startTime,
                    DateTime.Now,
                    numJobs,
                    true,
                    backendQueueSystem.SortColumn.QueuePosition,
                    backendQueueSystem.SortOrder.LastOrder);

                timeout++;
                Console.Write(".");
            }
            Console.WriteLine();

            if (queueStatusDs.Status.Count == numJobs)
            {
                return true;
            }
            return false;
        }
    }
}

Next Steps

The code in the WCFHelloProject example does not include Try … Catch statements, to help keep the code easier to follow for the discussion. The sample code is provided only as a test application to run on a test installation of Project Server, but should have exception handlers around the sections that use the PSI. Typical exceptions for WCF calls to the PSI are EndpointNotFoundException and CommunicationException.

The sample code uses a basic HTTP transport for communication. You could also create client endpoints that use the TCP network transport, and then compare the speed when running the application on an external computer. The external computer must have the .NET Framework 3.5 SP1 installed.

See Also

Tasks

Walkthrough: Customizing the PWA Ribbon and Accessing the JS Grid

How to: Use the QueueSystem Service

Concepts

Overview of WCF and the PSI

Prerequisites for WCF-Based Code Samples

Other Resources

Programming WCF Security

Claims-Based Architectures

Transport Security Overview

Beginner's Guide to Windows Communication Foundation

What's New: Client Object Model