Building a Web Service

For our first example, let's suppose that BOTS Consulting has a pretty good relationship with some of its clients including, coincidentally, Woodgrove Bank (the Woodgrove sample that comes with CMS will be modified to complete this code sample). Whenever Woodgrove, or other BOTS partners, posts a job posting on their Web site, BOTS would like to have it show on their Web site as well. If Woodgrove changes their job posting, BOTS would like their site to reflect the modified posting on their Web site. If Woodgrove deletes their posting, BOTS would like their Web site to reflect this deletion.

So, in cooperation with Woodgrove and its other partners, BOTS Consulting wants to create a Web Service that their partners would call to accomplish these tasks. This horizontal, real-time, push Web Service hosted by BOTS will syndicate a redundant copy of each job opportunity sent from BOTS' partners. That makes BOTS the aggregator of job postings for their partners.

First a Woodgrove author will submit a new or changed posting for approval. Then a Woodgrove approver will approve the posting, which will be the catalyst for calling the BOTS Web Service to syndicate the content. Figure 33-8 shows this scenario pictorially.

Figure 33-8. Add or change push syndication

graphics/33fig08.gif

Later a Woodgrove approver will delete the posting, which will be the catalyst for calling the BOTS Web Service to remove the content previously syndicated. Typically, we expect that the posting will simply expire rather than get deleted. The ExpiryDate is syndicated with the content, and the BOTS Web Service will set the syndicated posting to expire at the same time as the Woodgrove posting. If we presume the clocks on the separate servers are somewhat in sync, both postings will expire simultaneously. Figure 33-9 shows this scenario pictorially.

Figure 33-9. Delete push syndication

graphics/33fig09.gif

Create the BOTS Web Service VS.NET Project

We begin by building the BOTS Web Service that Woodgrove will call. It is wise and easier to isolate the Web Service code from the existing BOTS VS.NET project.

Step 1

So, crack open VS.NET and open a new C# MCMS Web Service VS.NET project (Figure 33-10).

Figure 33-10. New C# MCMS Web Service VS.NET project

graphics/33fig10.jpg

Be sure to choose MCMS Web Service from the Visual C# Projects folder under the Content Management Server Projects folder. If, instead, you choose ASP.NET Web Service from the root-level Visual C# Projects folder, a lot of configuration in the Web.config file will be missing, and although CMS will work for most things, when we try to do something other than retrieve data, we will end up with exceptions. The error message will indicate that we need to add the PostingEvents Module to the <httpmodules> section of Web.config with the name CmsPosting. Let's avoid all that by selecting the MCMS Web Service project template from the correct folder. That way, all the appropriate references, assemblies, and httpmodules will have already been added to the project for us.

Step 2

Using the VS.NET Solution Explorer, right-click the project name, CmsJobsWebService, and select Add a Web Service from the Add menu of the resulting pop-up. In the resulting dialog, choose Web Service from the list of templates in the Web Project Items folder, and name the new Service PostJobToBOTS.asmx (Figure 33-11; this time we do not want to use the Content Management Server folder).

Figure 33-11. New Web Service named PostJobToBOTS.asmx

graphics/33fig11.gif

This will be our actual Web Service assembly. However, we want an easy way to communicate multiple parameters via XML to the Web Service. To do this, we are going to create a serializable public class in the next few steps.

Step 3

Again, right-click the project name, CmsJobsWebService, and select Add a Component from the Add menu of the resulting pop-up. This time, in the resulting dialog, choose Class from the list of templates in the Web Project Items folder, and name the new class JobPosting.cs (Figure 33-12; again, we do not want to use the Content Management Server folder).

Figure 33-12. New class named JobPosting.cs

graphics/33fig12.gif

Step 4

Near the top of the code window for JobPosting.cs, change the namespace from CmsJobsWebService to BOTS.CmsJobsWebService. The namespace used here must be the same as the namespace used in the Web Service for the two classes to interact locally. Add a nice comment to the summary area and add the [Serializable] attribute to the class. More on the value of the [Serializable] attribute later. The code should look something like the following:

 using System; namespace BOTS.CmsJobsWebService {   /// <summary>   /// This class provides an efficient way to share information   /// with our partners about an Approved Job Posting. The class   /// is marked as Serializable so we can easily dehydrate its   /// state into XML, pass it to our partner so they can hydrate   /// it back and use it.   /// </summary>   [Serializable]   public class JobPosting   {     //1. Public Properties     public String JobEmployer;     public String JobPostingID;     public String JobPostingName;     public DateTime JobPostingStartDate;     public DateTime JobPostingExpiryDate;     public String JobTitle;     public String JobDescription;     //2. Constructor, required by .NET Framework     public JobPosting()     {     }   } } 
Step 5

The first code in the class defines the public properties for this class. We can hold information in these properties in much the same way that we used structures in the past. If we wanted to, we could have used regular set and get blocks to access private variables, but since no additional processing is needed and to keep the example short, we just make the variable itself public. For jobs that are syndicated to BOTS, these properties represent the data that BOTS anticipates. The syndicate will need to adopt their data to these fields. The first property, JobEmployer, should identify the company that is calling the Web Service. The next four are characteristics about the posting, and the last two are about the job itself. The JobTitle property will be used to populate the DisplayName of each posting that is created/updated. The code after the second comment is the .NET required class constructor. Save your changes.

Step 6

Return to the top of the code window for the PostJobToBOTS assembly (PostJobToBOTS.asmx.cs). We need to add two using statements. The first statement is included for interacting with Publishing objects and the second for working with Placeholders. We also need to change the namespace from CmsJobsWebService to BOTS.CmsJobsWebService (to match the namespace given in the JobPosting class), add a nice comment to the summary area, and add the [WebService] attribute to the class. The code near the top of the file should look something like the following (preceded by several using statements inserted by default):

 using Microsoft.ContentManagement.Publishing; using   Microsoft.ContentManagement.Publishing.Extensions.Placeholders; namespace BOTS.CmsJobsWebService {   /// <summary>   /// Summary description for CmsJobsWebService.PostJobToBOTS.   /// </summary>   [WebService (Namespace="http://botsconsulting.com")]   public class PostJobToBOTS : System.Web.Services.WebService   {     public PostJobToBOTS()     { 

The [WebService] attribute is the key to exposing this class as a Web Service. By adding Namespace="http://botsconsulting.com" to this attribute, we avoid the warning we would get later about using the http://tempuri.org namespace.

Step 7

Uncomment the HelloWorld example provided near the bottom of the code window, build the solution (Ctrl-Shift-B), and then point the browser to our Web Service (should be http://localhost/CmsJobsWebService/PostJobToBOTS.asmx) to make sure that everything is working properly. Since debug is set to true by default in a new MCMS Web Service, right-clicking the PostJob.asmx file in the Solution Explorer, clicking Set As Start Page, and then pressing F5 launches a browser window (Figure 33-13) where we can not only debug but test as well.

Figure 33-13. Testing the Web Service

graphics/33fig13.gif

If you see a message that reads "This Web service is using http://tempuri.org/ as its default namespace" followed by a verbose recommendation, the Namespace property we discussed in step 6 is missing from the [WebService] attribute. Clicking the Service Description will display XML that describes the class and its methods. This XML is called the WSDL, Web Service Description Language, discussed near the beginning of this chapter in Table 33-1. Clicking the HelloWorld link and subsequently on the Invoke button that follows will result in a new window that shows the XML string that would be returned to the caller of the HelloWorld Web Service. Getting this simple example to function properly is a must before continuing. If it is working for you, let's continue. If not, there are dozens of examples on MSDN, on GotDotNet.com, and even in the .NET Framework SDK that should help get you going.

Step 8

Next, replace the entire HelloWorld example and its comment with the following four private functions. We will be using them when we are coding the public method of our Web Service.

The first private function simply logs errors to a file.

 private void LogErrorToFile(string errorMessage) //***************************************************************** //Write Error to File //***************************************************************** {   try   {     //1. Write Error to File     System.IO.StreamWriter logFile =       new System.IO.StreamWriter(<@>"C:<\\>ErrorLog.txt", true);     logFile.WriteLine(errorMessage);     logFile.Close();   }   catch{} } 

The second private function was introduced in Chapter 25 in Listing 25-1. It is used to create an authenticated CMSApplicationContext using the currently logged-on Windows user, typically in Update PublishingMode.

 private CmsApplicationContext GetAuthenticatedCmsApplicationContext   (PublishingMode cmsMode) //***************************************************************** //Adapted from Listing 25 1 Function to Create an Application //Context. Create a new CmsApplicationContext and authenticate it //Pass the created Context back to the calling method. //***************************************************************** {   //1. Declare a Context variable   CmsApplicationContext cmsContextApp = null;   try   {     //2. Grab a new Application Context     cmsContextApp = new CmsApplicationContext();     //3. Assign current Windows User to a WindowsIdentity variable     //   This will only work if IIS is set to Windows     //   Authentication and Guest Access is enabled in the SCA     System.Security.Principal.WindowsIdentity identCurrentUser =       System.Security.Principal.WindowsIdentity.GetCurrent();     //4. Log in to CMS     //   Use the currently authenticated Windows User credentials     //   Put Context into the PublishingMode passed to the function     cmsContextApp.AuthenticateUsingUserHandle(       identCurrentUser.Token,      cmsMode);     //5. Return the Authenticated Context     return cmsContextApp;   }   catch(Exception eError)   {     //6. Write Error to File     LogErrorToFile(eError.Message.ToString());     //7. Return the null Context in the event of an error     return cmsContextApp;   } } 

The third private function was also introduced in Chapter 25, in Listing 25-3. Given a Channel, a Name, and a Template, the function is used to create an uncommitted Posting.

 private Posting CreateNewPosting(   Channel parentChannel, string newPostingName,   Template cmsTemplate) //***************************************************************** //Adapted from Listing 25 3 Function to Create New Posting. //Create a new Posting in the parentChannel using the //newPostingName based upon the cmsTemplate all passed to the //function. Pass the created Posting back to the calling method. //***************************************************************** {   //1. Declare a Posting variable   Posting cmsPosting = null;   try   {     //2. Determine if the user has sufficient rights to create     //   a Posting from the would-be parent Channel     if(parentChannel.CanCreatePostings)     {       //3. Create the Posting using the Template passed       cmsPosting = parentChannel.CreatePosting(cmsTemplate);       //4. Validate successful creation       if(cmsPosting != null)       {         //5. Give it the Name passed to the function         cmsPosting.Name = newPostingName;       }     }     //6. Return the created Posting     return cmsPosting;   }   catch(Exception eError)   {     //7. Write Error to File     LogErrorToFile(eError.Message.ToString());     //8. Return the null Posting in the event of an error     return cmsPosting;   } } 

And finally, the fourth private function simply isolates the process of systematically generating a name from data provided, so that it can consistently be done from several areas of our Web Service. We will concatenate the jobEmployer, a period, and the jobID (without the curly brackets) into a name that will help us relocate the created Posting if it is changed or deleted in the future. By including the jobEmployer value in the name, we ensure uniqueness across partner postings, providing a potentially beneficial sorting mechanism while maintaining the ability to find the created Posting later.

 private string GenerateJobName(string jobEmployer, string jobID) //***************************************************************** //Concatenate JobEmployer and ID to consistently form a name //***************************************************************** {   //1. Declare temp variable to hold jobname   string jobName = "";   try   {     //2. Strip out all spaces (could do any kind of tailoring here)     jobName = jobEmployer.Replace(" ", "");     //3. Concatenate the GUID without the curly brackets to the     //   Employer name separated with a period     //   This is done so that we can identify this Posting later     jobName += "." + jobID.Substring(1,36);     //4. Return concatenated name     return jobName.ToString();   }   catch(Exception eError)   {     //5. Write Error to File     LogErrorToFile(eError.Message.ToString());     //6. Return empty name     return jobName.ToString();   } } 

Next we create our first WebMethod.

Step 9

BOTS will need to provide its partners with a function that they can call (in a Web Service this is called a WebMethod) to add or change a job when it is approved on their Web Property. Making a function available to the outside world via a Web Service is relatively painless in VS.NET. We simply ensure that the function is public and is preceded with the [WebMethod] attribute. The AddChangeJob WebMethod code is explained with comments throughout but will also be discussed after the code is presented.

 [WebMethod] public string AddChangeJob(JobPosting job) {   try   {     //1. Grab an Authenticated Context in Update PublishingMode     //   using the GetAuthenticatedCmsApplicationContext function     //   from Listing 25 1     CmsApplicationContext cmsContextApp =       GetAuthenticatedCmsApplicationContext(PublishingMode.Update);     //2. Grab the jobs Channel in which the Posting will exist     //   Cast the result of the Searches object as a Channel     Channel cmsChannel =       cmsContextApp.Searches.GetByPath(       "/Channels/botsconsulting/careers/jobs/")       as Channel;     //3. Generate a consistent name for the Posting from data     //   provided. The resulting name will look something like:     //   Woodgrove.12345     string jobName = GenerateJobName(job.JobEmployer,       job.JobPostingID);     //4. Grab this Posting for update if it already exists     Posting cmsPosting =       cmsContextApp.Searches.GetByPath(       "/Channels/botsconsulting/careers/jobs/" + jobName)       as Posting;     //5. If no Posting was found, create one     if (cmsPosting == null)     {       //6. Grab the Template upon which to create the Posting       //   Cast the result of the Searches object as a Template       Template cmsTemplate =         cmsContextApp.Searches.GetByPath(         "/Templates/botsconsulting/JobDetail")         as Template;       //7. Create a new Posting       cmsPosting = CreateNewPosting(cmsChannel, jobName,         cmsTemplate);     }     //8. Verify we have a Posting     if (cmsPosting != null)     {       //9. Set the Posting data, by synchronizing the Start/Expiry       //   dates. The postings are automatically published and       //   expired together.       cmsPosting.DisplayName = job.JobTitle.ToString();       cmsPosting.StartDate   = job.JobPostingStartDate;       cmsPosting.ExpiryDate  = job.JobPostingExpiryDate;       //10. Grab the description placeholder       HtmlPlaceholder cmsPlaceholder =         cmsPosting.Placeholders["JobDescription"]        as HtmlPlaceholder;       //11. Set the description placeholder's content       //    The caller will include everything that they want to       //    show in this single syndicated Posting in this field       cmsPlaceholder.Html = job.JobDescription;       //12. Submit the Posting       //    This will kick off whatever workflow BOTS has decided       //    to implement for the user we authenticated with       cmsPosting.Submit();       //13. Commit all changes. If not explicitly called, the       //    disposition of changes is based upon       //    RollbackOnSessionEnd       cmsContextApp.CommitAll();     }     //14. Dispose of the stand-alone Application Context     cmsContextApp.Dispose();     //15. Let the caller of the Web Service know the job succeeded     return jobName + " successfully syndicated";   }   catch(Exception eError)   {     //16. Write Error to File     LogErrorToFile(eError.Message.ToString());     //17. Let the caller of the Web Service know an error occurred     return job + eError.Message.ToString();   } } 

The parameter passed to this WebMethod function is of type Job Posting. That is the public class that we created earlier in this chapter. When the caller invokes this AddChangeJob method of our Web Service, they will need to pass it a JobPosting object. With .NET this is easy. They simply create an instance of our public JobPosting class that is exposed to them, populate it with the data that they would like to syndicate, and then pass the instance as if they were calling a local component's method. Because the class has the [Serializable] attribute, the .NET Framework will automatically serialize the object as XML and transfer it to the Web Service where it will automatically be deserialized, and BOTS can use it as if it were a local object. All of the state that was present when the object was serialized will be back when it is deserialized. Frequently, this is referred to as deflating and inflating, or dehydrating and hydrating an object. Very cool.

The jobName under comment 3 is of interest. Because CMS allows us to use a different DisplayName from the actual name of the Posting, we are using two pieces of information that the caller provides to create a name for the syndicated Posting that users won't see but has great utility for us. The naming convention should keep the jobs sent to BOTS in uniquely named Postings. Also, because CMS has a search that we can use to find specifically named assets, the naming convention provides us with a convenient way to determine if we have created this Posting before. By naming the Postings in this way, we can also sort the PostingCollection by Name, assuming that BOTS might set this up with more companies than just Woodgrove, yielding a list of Postings ordered by ID within Employer. That could prove to be very helpful. Woodgrove will be sending us a GUID as an ID, so it is highly unlikely that anyone else would ever send the same ID, but it is feasible that as BOTS expands this service, two companies could send Postings with the same ID. The naming convention protects us against that scenario by including the Employer in the name.

However, this solution is not at all bulletproof. We obviously should do a lot more exception handling. For instance, under comment 6 we search for a hard-coded Template but we never validate that it was successfully found before we create our new Posting. Or if the jobName returns empty, we proceed to create a Posting with no name. We also don't do any bounds or content checking, so a malicious or just careless partner could introduce potentially hazardous content (maybe voluminous garbage, maybe containing a script or some kind of hack, maybe spam, or worse).

The way it is set up right now, the JobDescription Placeholder will only contain plain text. So, no matter how elegant the content is that a client sends, our current Posting will present it with no frills. BOTS may want to consider changing the JobDescription Placeholder to allow for FullFormatting.

Step 10

We need one final [WebMethod]. If BOTS partners decide to remove a job posting before or even after it has expired, BOTS needs to reflect that deletion. The DeleteJob WebMethod code is explained with comments throughout.

 [WebMethod] public string DeleteJob(JobPosting job) {   try   {     //1. Grab an Authenticated Context in Update PublishingMode     //   using the GetAuthenticatedCmsApplicationContext function     //   from Listing 25-1     CmsApplicationContext cmsContextApp =       GetAuthenticatedCmsApplicationContext(PublishingMode.Update);     //2. Generate a consistent name for the Posting from data     //   provided. The resulting name will look something like:     //   Woodgrove.12345     string jobName = GenerateJobName(job.JobEmployer,       job.JobPostingID);     //3. Grab this existing Posting for syndicated deletion     Posting cmsPosting =       cmsContextApp.Searches.GetByPath(       "/Channels/botsconsulting/careers/jobs/" + jobName)       as Posting;     //4. Check to see if the Posting was found, if it was delete it     if (cmsPosting != null)     {       //7. Delete the Posting       cmsPosting.Delete();       //8. Submit the Posting       //    This will kick off whatever workflow BOTS has decided       //    to implement for the user we authenticated with       cmsPosting.Submit();       //9. Commit all changes. If not explicitly called, the       //   disposition of changes is based upon       //   RollbackOnSessionEnd       cmsContextApp.CommitAll();     }     //10. Dispose of the stand-alone Application Context     cmsContextApp.Dispose();     //11. Let the caller of the Web Service know the job succeeded     return jobName + " successfully deleted";   }   catch(Exception eError)   {     //12. Write Error to File     LogErrorToFile(eError.Message.ToString());     //13. Let the caller of the Web Service know an error occurred     return job + eError.Message.ToString();   } } 

Build the PostJobToBOTS Web Service solution in VS.NET. Our Web Service is now complete. Let's modify the Woodgrove Web site to consume it.



Microsoft Content Management Server 2002. A Complete Guide
Microsoft Content Management Server 2002: A Complete Guide
ISBN: 0321194446
EAN: 2147483647
Year: 2003
Pages: 298

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net