The Challange
So you may ask now, why would we like to access PSI functionality from PSContext at all? Let me explain our situation.
Recently we had to extend an existing server side code in our custom Project Server solution to enable delegate users (that means ones acting for another users at the given time) to access the same custom functionality as the user they acting for.
Note: You can manage delegation via PWA Settings / Manage Delegates (either in the Personal Setting or in the Security section), see the page http://YourProjectServer/PWA/_layouts/15/pwa/userdelegation/ManageDelegations.aspx?mgm=true.
To make the things event worse, we had to check the delegation information in code running in the SharePoint web context, and in code that runs as a simple server side process without web context. It is actually not a timer job, but an automated server process scheduled by Windows Task Scheduler for the sake of simplicity, that perform some kind of housekeeping and reporting tasks for the custom application.However, in the case of a time job, an asynchronous event receiver, or a workflow step running without web context, you would have the same issue. Why these two cases (with or without SharePoint web context) differ from each other, will be discussed later in more details.
You probably know, that although the server side object model of the Project Server (available via the Microsoft.ProjectServer.PSContext) covers quite a lot of the functionality of the PSI, there is still a plenty of features that are simply inaccessible via the objects available through PSContext. For example, there is no way to access the delegation information. That means for us, that we should read this information via PSI, as its Resource class provides a ReadDelegations method just for this purpose. If you are not new in Project Server development, you should have already your own experience with PSI. If you have not any experience with that yet, I can say you, that it is rather complex comparing to the server side OM provided by PSContext. You should work with various DataSet and DataTable objects, but the greatest problem in our case that it requires a lot of configuration, either by config files (see the Configuring the Services with app.config section of the Walkthrough: Developing PSI Applications Using WCF article for example) , or via code (see section 11 of the Walkthrough: Developing PSI Applications Using WCF article, or the Configuring the Services Programmatically section of the Walkthrough: Developing PSI Applications Using WCF article for example), just to be able to start using the PSI web services. If you have more applications (in our case one with, and another one without SharePoint web context), multiple environments (development, test, and production, one with HTTP, another one with HTTPS), and multiple servers in the environments, creating and maintaining the configuration information is typically not a trivial task.
Why could not we simply access the PSI endpoints via the PSContext? It would be great, wouldn’t it? Are you surprised if I say, it is possible? Well, actually it is. It requires an amount of hacking, and probably not a supported solution, but it is technically possible, and it made our life easier. If you take the risk on your own, you can try it as well, however there is no guarantee, that it works for you, or that once it works, a new Project Server patch could not break the functionality. So be warned.
After this introduction you should have already an overview of the problem and hopefully you are ready to read about the technical stuff. If you are not interested in such details, and need only the result of the analysis, you can skip the next two sections.
Connection between the Project Server client side object model and the server side object model
If one dig into the assemblies implementing the server side object model (Microsoft.Project.dll), and the client side object model (Microsoft.Project.Client.dll) of Project Server, then it turns out, that Microsoft has not re-implemented the functionality of PSI, these libraries still utilize the good-old PSI infrastructure either directly (in case of the server side OM) or indirectly (in case of the client side OM, that calls PSI via the server side OM).
How the client side OM invokes the server side OM is not Project Server specific, the infrastructure is inherited from SharePoint. Although there were changes between the SharePoint versions 2010 and 2013, the main concepts remain the same. Both the server side and client side of that bridge were deeply researched and analyzed in my posts five years ago. Those posts should give you enough insight into the internal functionality of the client side OM, this topic is outside of scope of the current post.
It is more exciting (at least, for now), how the Project Server server side OM invokes the PSI infrastructure. Exactly that will I describe in the next section.
Connection between the Project Server server side object model and PSI
The Microsoft.Office.Project.PWA namespace contains a public class called PSI (implemented in assembly Microsoft.Office.Project.Server.PWA). This class exposes more than 20 public properties with postfix “WebService”, each of them returns the WCF proxy of the corresponding PSI channel. For example, the ResourceWebService property returns a reference for a Resource object (namespace: Microsoft.Office.Project.Server.WebServiceProxy, assembly: Microsoft.Office.Project.Server.PWA), inherited from WcfProxyBase<IResource>. Once created in the “WebService” property getters, these WCF proxy objects are stored in the private field _proxyContainer (of type ProxyContainer, implemented as a nested class) of the PSI class for any further access.
For the purpose of our solution, it is irrelevant how the WCF proxy objects (like the Resource object returned by the ResourceWebService property of the PSI class) know, what configuration they should use and how they can access the WCF endpoint. Probably I write about sometimes later in an other post.
The internal PJClientCallableContext class (Microsoft.ProjectServer namespace, Microsoft.ProjectServer assembly) contains a static read-only property called PJContext. It returns an instance of the PJContext class (Microsoft.Office.Project.PWA namespace, Microsoft.Office.Project.Server.PWA assembly) using the generic private static RetrieveValue method. If the process runs in the HTTP context (HttpContext.Current is not null) the RetrieveValue method invokes the GetContext(bool isWebServiceCall, bool ignoreCachedContext) method of the PJContext class. Otherwise (without HTTP context), the lazy-initialized value of the _pjcontext field will be returned by RetrieveValue method. The lazy-initialization of this field can be found in the Initialize method of the PJClientCallableContext class, the static GetObjectModelContext method of the PJContext class is invoked using the SPWeb object instance passed to the Initialize method as parameter. This is the same SPWeb object, that you passed to the constructor of the PSContext object (or it is the RootWeb of the site if you used the PSContext constructor with the SPSite object; or the RootWeb of the site corresponding to the URL if you used the PSContext constructor with the Uri object), as in the PSContext(SPWeb web) constructor an instance of the PJClientCallableContext class is created using the web instance as parameter.
Side note: On the other hand, using the PSContext constructors with parameters in a HTTP context may result you do no get the expected outcome, but it is beyond our scope again, more on that in a later post.
After this theoretical explanation let’s see some functional code.
The Code
My goal was to access the static PJContext property of the internal PJClientCallableContext class via Reflection, and expose its PSI property to my code as an extension method.
The solution described below requires adding the following assembly references:
- Microsoft.Project.dll
- Microsoft.Office.Project.Server.Library.dll
- Microsoft.Office.Project.Server.PWA.dll
- Microsoft.Office.Project.Schema.dll
- Microsoft.Office.Project.Server.Administration.dll
The extension method is rather simple, and requires this using directive to work:
using Microsoft.Office.Project.PWA;
- public static PSI GetPSI(this PSContext dummyContext)
- {
- PSI result = null;
- Assembly psAssembly = typeof(PSContext).Assembly;
-
- // get internal type
- Type type_PJClientCallableContext = psAssembly.GetType("Microsoft.ProjectServer.PJClientCallableContext");
- PropertyInfo pi_PJContext = type_PJClientCallableContext.GetProperty("PJContext");
- PJContext pjContext = pi_PJContext.GetValue(null) as PJContext;
-
- if (pjContext != null)
- {
- result = pjContext.PSI;
- }
-
- return result;
- }
Note: As you can see, in the code above we don’t use the PSContext object passed as parameter to the extension method at all. It is only to enable attaching the functionality to a PSContext instance via the extension method, and so enforcing the creation of a PSContext instance first. If you have an application without HTTP context (like a console application), and you call this code without creating a PSContext instance first, like:
PSI psi = Extensions.GetPSI(null);
an InvalidOperationException will be thrown by the RetrieveValue method of the PJClientCallableContext class:
You must wrap your server OM code in a PJClientCallableContext when outside of an HttpContext.
In the case of a web application you don’t have such problem, as the PJContext instance is created using the current HTTP context.
Below I illustrate the usage of the extension method, by querying the delegation information of an enterprise resource in Project Server. To achieve that, I invoke the ReadDelegations method of the Resource class. See the available values of the DelegationFilter enumeration in the documentation.
The code requires the following using directives:
using Microsoft.Project;
using Microsoft.Office.Project.PWA;
using schema = Microsoft.Office.Project.Server.Schema;
using lib = Microsoft.Office.Project.Server.Library;
- using (PSContext projectContext = PSContext.GetContext(new Uri(pwaUrl)))
- {
- PSI psi = projectContext.GetPSI();
-
- lib.UserDelegationConsts.DelegationFilter filter = lib.UserDelegationConsts.DelegationFilter.All;
-
- // replace the GUID with an ID of a resource in your environment
- // whose delegation information you would like to display
- Guid resId = Guid.Parse("087ade95-281e-e411-9568-005056b45654");
-
- using (schema.UserDelegationDataSet delegationDS = psi.ResourceWebService.ReadDelegations(filter, resId))
- {
- foreach (schema.UserDelegationDataSet.ResourceDelegationsRow resDelegation in delegationDS.ResourceDelegations)
- {
- Console.WriteLine("Resource is substituted by '{0}' from '{1}' to '{2}'", resDelegation.DELEGATE_NAME, resDelegation.DELEGATION_START, resDelegation.DELEGATION_FINISH);
- }
- }
- }
Using the approach above you don’t have to bother with the configuration of the PSI proxy you need in your application, and still access such features in your server side code, that are available in PSI.
Note: There is a major restriction using this method. Although the PSI web services are available from a client computer, or from a remote server as well, you can not access remote servers using the code above. You can access only the PSI resources exposed by the local farm.