Implementing a Web-Services Interface

Implementing a Web-Services Interface

In many ways, a web-service interface is easier to construct than a Windows Forms or Web Forms interface, because we don't need to worry about any issues of display or user interaction. Those are the responsibility of the consumer. All we need to worry about is providing an interface that allows the developer of a consumer to access the information and functionality provided by our application's business logic and data.

Web-Service Design

In designing a web service, we must face the following four primary issues:

  • What methods do we want to expose?

  • How will we organize the web methods into web services?

  • What data do we want to expose and accept?

  • What are our security requirements?

Deciding on Web Methods

It's possible to subdivide our ProjectTracker application's functionality in many different ways. For example, we could be very specific and provide a set of discrete services, such as those listed in Table 10-1.

Table 10-1: Possible Services We Could Expose as Web Methods

Add project

Get project

Remove project

Change project name

Change project start date

Change project end date

Add resource

Get resource

Remove resource

Change resource first name

Change resource last name

Get list of projects

Get list of resources

Change project description

Add resource to project

Remove resource from project

Add project to resource

Remove project from resource

Change role of resource on project

and so on

Following this approach, we could end up writing a rather large number of web methods! Although it's perfectly possible to do that, we might instead consider consolidating some of these operations into web methods with broader functionality, as follows :

  • Get a list of projects

  • Get details for a project

  • Add or update a project

  • Delete a project

  • Get a list of resources

  • Get details for a resource

  • Add or update a resource

  • Delete a resource

This is a smaller list of discrete operations, and by having fewer operations, we have less code to maintain. Moreover, we provide a higher level of abstraction: A consumer has no idea what happens when it requests details for a project, and over time we may change how that process works without having any impact on our consumers. Perhaps most importantly, having a small number of operations tends to improve performance, since a client application needs to make fewer cross-network method calls to get its work done.

Grouping Web Methods into Web Services

Under the .NET Framework, web methods are grouped together within a URL such as http://server/root/projecttracker.asmx, where projecttracker.asmx is the page or file that contains our web service. Within a given virtual root on a given web server, there can be any number of such web-service pages, each with its own set of web methods.

This then is a decision point in our design: Should we put all our web methods into a single web-service file, or put each web method in its own web service, or something in between? Unfortunately, there's no hard-and-fast rule to guide our decision.

Tip 

At the time of writing, web-services technology is too new for best practices to have evolved, and there are conflicting views that may drive your particular design choices. Are web services an API technology, a messaging technology, a component technology, or an Enterprise Application Integration (EAI) or Business Process Integration (BPI) technology? Web services can be viewed as any one of these, and each has its own set of best practices that guide how to group procedures or functions together. The one you favor will color your decisions in terms of design.

In this context, one way to view a web service is as a component that we happen to be accessing via Internet technologies. A component is a container for similar groupings of functionality (COM or .NET components typically contain a group of related classes), so likewise a web-service "component" should contain a group of related web methods. Of course, we know that all of our functionality is related in some way; the question is whether we should break it up into multiple web servicesperhaps one for project-related tasks , and one for resource- related tasks?

However, there's another angle to this question that we need to consider before making our decision, and that's the consumer. Consumers don't reference an entire virtual root; they reference a specific web service (ASMX file). The more granular we make our web services, the more different references the developer of the consumer will need to make in order to use our web methods.

Because of this, I prefer to group related web methods into web services based on the likely usage pattern of consumer developers. Since the web methods will all be related within the context of the ProjectTracker application, we're following basic component design concepts; and since we're creating an interface to our application, we're also taking into account the needs of the end user (the consumer developer).

For our ProjectTracker sample application, this means putting all our web methods into a single web service. They are all related to each other, so they naturally fit into a component. More importantly, it's likely that any consumer will be dealing with both projects and resources, and there's no sense in forcing the consumer developer to establish two separate references to our server just to use our web methods.

Returning Our Data

The next issue we need to consider is how to return our complex business data. Our data exists in our business objects, but it needs to be returned to the consumer via SOAP-formatted XML.

In many sample web services, the web methods return simple data types such as int or string , but that doesn't match the needs of most applications. In our example, we need to return complex data, such as an array or collection of project data. And the project data itself isn't a simple data typeit consists of multiple data fields.

There are a couple of approaches we might consider, as follows:

  • Returning the business objects directly, tying the data format directly to the object interface

  • Using a formal facade to separate the data format from the business-object interface

As we'll see, the more formal approach is superior , but to be thorough, let's discuss the first option, too.

Returning Business Objects Directly

It may seem tempting to return a business object (or an array of business objects) as a result of a web method. Why should we go through the work of copying the data from our business object into some formal data structure just so that data structure can be converted into XML to be returned to the consumer? After all, the .NET web-services infrastructure can automatically examine our business class and expose all the public read-write properties and public fields of our object, as shown in Figure 10-8.

image from book
Figure 10-8: Directly returning a business object's data to the consumer

Unfortunately, there are two flaws with this approach that make it untenable. First and most important is the fact that doing this restricts our ability to change, enhance, and maintain our business objects over time. If we expose the business object directly, then the object's interface becomes part of the web-service interface. This means the object's interface is part of the contract that we're establishing by publishing the web service. This is almost never acceptablewe need to retain the ability to alter and enhance the interface of our business objects over time without impacting other applications that use our web service.

Second, make careful note of the fact that only the public , read-write properties, and public fields are exposed. Nonpublic properties aren't exposed. Read-only properties (such as ID on the Project and Resource objects) aren't exposed. Unless we're willing to compromise our object design specifically to accommodate the requirements of web service design, we won't be able to expose the data we choose via web services.

Beyond this, we must also expose a public default constructor on any class exposed directly via web services. If we don't provide a public default constructor, we'll get a runtime exception when attempting to access the web service. The design of CSLA .NET business objects specifically precludes the use of public default constructors, as we always use static factory methods to create instances of our business objects.

Due to these drawbacks, directly exposing our objects isn't a good practice. The answer instead is to create a facade around the business objects that can separate the public interface of our web service from the interface of our business objects. We can construct this facade so that its properties and fields are always available for serialization into XML.

Returning Formal Data Structures

We can easily create a formal data structure to define the external interface of our web service by using a struct or user-defined type. This data structure will define the public interface of our web service, meaning that the web-service interface is separate from our business-object interface. The web service and this data structure form a facade so that consumers of the web service don't know or care about the specific interface of our business object.

For instance, we can define a struct that describes the data for a project like this:

  public struct ProjectInfo     {       public string ID;       public string Name;       public string Started;       public string Ended;       public string Description;     }  

Then we can have our project-related web methods return a result of this type or even an array of results of this type. When this is returned as a result of our web method, its data will be converted into SOAP-formatted XML that's returned to the consumer. Figure 10-9 illustrates what we're talking about doing here.

image from book
Figure 10-9: Using a facade to define the data returned to the consumer

When consumers reference our web service, they'll gain access to the definition of this type via the WSDL data that's associated with the service. This means that the consumer will have information about the data we're returning in a very clear and concise format.

Tip 

When we create a consumer for the web service, VS .NET uses this information to create a proxy class that mirrors the data structure. This gives consumer developers the benefits of Intellisense, so that they can easily understand what data we require or return from our web methods.

Security

The final consideration is security. Of course, there are many types and layers of security, but what we're concerned with here is how to use either CSLA .NET or Windows' integrated security to identify the users and their roles.

Even though the "user" in this case is a remote application, that application must still identify itself so that we can apply appropriate security-related business rules and processing. In our case, this means that only someone in the ProjectManager role can add or edit a project, for example.

Whether the remote consumer uses a hard-coded username and password, or prompts its actual user for credentials, isn't up to us. All we can do is ensure that the consumer provides our application with valid credentials.

If we opt to use Windows' integrated security, we'll configure IIS to disallow anonymous access to the virtual root containing our web-service ASMX file. We'll also add an <identity impersonate="true" /> element into the <system.web> section of the site's Web.config file, so that ASP.NET knows to impersonate the user's account. This will force the consumer to provide valid Windows credentials in order to interact with our web service.

No extra work is required in our code, other than ensuring that the Web.config file in our web-service application has the <appSettings> entry to configure CSLA .NET to use Windows security.

Tip 

Windows' integrated security is probably not a viable option in most cases. It's relatively unlikely that unknown clients on unknown platforms will be authenticated within our Windows domain. While our architecture does support this option, using it would mean that consumers must start out with valid Windows domain accounts with which they can authenticate to our server.

CSLA .NET security requires a bit more work, but avoids any necessity for the remote consumer (or its users) to have Windows domain user accounts in our environment. To implement CSLA .NET security, IIS should be left with the default configuration that allows anonymous users to access our virtual root. We must then include code in our web service to ensure that they provide us with a username and password, which we can validate using the BusinessPrincipal class in the CSLA .NET frameworkjust like we did in the Windows Forms and Web Forms interfaces.

The harder question is how to get the username and password from the consumer, and there are two basic approaches to an answer. The first of these is to have each of our web methods include username and password parameters. Each time the consumer called one of our methods, it would then have to provide values for these two parameters (along with any other parameters our method requires). Within our web method, we could then call the BusinessPrincipal class to see if the combination is valid.

Although this works, it pollutes the parameter lists of all our methods. Each method ends up with these two extra parameters that really have nothing to do with the method itself. This is far from ideal.

The other approach is to use the SOAP header to pass the information from consumer to server outside the context of the method, but as part of the same exchange of data. In other words, the username and password information will piggyback on the method call, but won't be part of the method call.

This is a standard technique for passing extra information along with method calls. It's supported by the SOAP standard, and therefore by all SOAP-compliant client development tools. What this means is that it's a perfectly acceptable approachin fact, it's the preferred approach. We'll use it as we develop our sample interface.

Web-Service Implementation

VS .NET makes the creation of web methods and web services very easy. Of course, the development of a web method is all about the business code we writeand that can be quite complex. Fortunately, we already have business objects that implement all the data access and business logic, so our code will be pretty straightforward!

Creating the Project

In the existing ProjectTracker solution, add a new project using File image from book Add Project image from book New Project. As shown in Figure 10-10, make it an ASP.NET Web Service project, and name it PTService .

image from book
Figure 10-10: Creating the PTService project

VS .NET will automatically create a new virtual root for our web-service application, so any web services that we create will have a URL similar to this:

 http://server/PTService/myservice.asmx 

Of course, " server " will be the name of your web server, and myservice.asmx will be the name of the actual web-service file we'll create.

Referencing and Importing Assemblies

Since our code will be making use of the business objects from Chapter 7, we need to add a reference to the ProjectTracker.Library project. That in turn relies on the CSLA .NET Framework DLLs, so we'll need to reference those as well by using the Browse button, just as we did for the Windows Forms and Web Forms interfaces. This is shown in Figure 10-11.

image from book
Figure 10-11: Referencing the CSLA and ProjectTracker.Library assemblies
Configuring the Application

As with our other interfaces, we need to provide application-configuration information through a configuration file. Since a web service is just another type of web application, its configuration file is Web.config , and one is automatically added to our project. Open that file and add the following < appSettings> block:

 <?xml version="1.0" encoding="utf-8" ?> <configuration>  <appSettings>     <add key="Authentication" value="CSLA" />     <add key="DB:Security"         value="data source=    server    ;initial catalog=Security;                integrated security=SSPI" />     <add key="DB:PTracker"         value="data source=    server    ;initial catalog=PTracker;                integrated security=SSPI" />   </appSettings>  

Note that this is exactly the same as our configuration for the Web Forms interface. Again, we've configured the CSLA .NET Framework so that the DataPortal will run in the same process as our web-service code, thereby providing optimal performance and scalability.

Tip 

As with any web application, your security environment may dictate that the data access must be handled by an application server behind a second firewall. If this is the case, change this configuration to be the same as the Windows Forms interface from Chapter 8. Keep in mind that doing this will result in a performance reduction of about 50 percent when compared to running the data-access code directly on the web server. This is a cost that's often paid to gain increased security.

Since web services don't typically use the concept of a Session , we should turn that off as well. The following is an entry in the <system.web> section of Web.config :

 <sessionState  mode="Off"  stateConnectionString="tcpip=127.0.0.1:42424"      sqlConnectionString="data source=127.0.0.1;Trusted_Connection=yes"      cookieless="false"      timeout="20"  /> 

This reduces the amount of overhead for the application because it doesn't have to worry about Session objects that we won't be using anyway.

Creating the Web Service

VS .NET starts with a default web-service file named Service1.asmx . Remove it now and use Project image from book Add Web Service to add a new web service named ProjectTracker as shown in Figure 10-12.

image from book
Figure 10-12: Adding the ProjectTracker web service

We'll be writing all our code in this new file.

Tip 

Although it's technically possible to rename Service1.asmx to ProjectTracker.asmx , and then to do a mass substitution of Service1 for ProjectTracker within the prebuilt code, it's often simpler just to delete the default file and create a new one with the right name.

Web services exist in a "namespace" that's fairly similar to the .NET concept of the same name. For web services, however, the namespaces are global on the Internet, and they're typically identified by the organization's domain name. By default, web services created in VS .NET are configured to use a diagnostic namespace, and we should change this to a namespace that's appropriate to the organization.

This is done by opening the code window for ProjectTracker.asmx and changing the Namespace value in the [WebService()] attribute, as follows:

  [System.Web.Services.WebService(Namespace="http://ws.lhotka.net/PTService/ProjectTracker")]  public class ProjectTracker : System.Web.Services.WebService 

Notice that the domain name of the namespace is now a meaningful value that corresponds to a specific organization. (You should use your organization's domain here instead of ws.lhotka.net .)

Handling Security on the Server

Notice that our <appSettings> configuration includes the following line:

 <add key="Authentication" value="CSLA" /> 

So we're using our table-based CSLA .NET security model.

Tip 

We could also use the Windows' integrated security model, as described earlier. However, if we decide to go down that route, we must not implement the security code we're about to create.

When we use the CSLA .NET security model, we must call the Login() method of the BusinessPrincipal class to authenticate the user. This requires username and password values, which means that we need to get those values from the consumer that's calling our web service.

As we discussed earlier, we could do this by putting username and password parameters on all our web methods, but that would pollute the parameter lists of our methods. Instead, we can use a SOAP header to transfer the values. This is a standard SOAP concept, and it's easily implemented in our .NET code (on both the server and consumer).

Tip 

Note that the username and password will be passed in clear text in the SOAP envelope. You may want to use the .NET Framework's cryptography support to encrypt this data for additional security.

Before we write this code, we should use the appropriate security namespaces into our code for ProjectTracker.asmx , as shown here:

 using System.Web.Services;  using System.Web.Services.Protocols; using CSLA.Security;  

The System.Web.Services.Protocols namespace includes functionality that we'll need in order to implement SOAP header that contains the username and password values. The CSLA.Security namespace includes the BusinessPrincipal class that we'll use to authenticate the user.

The following three steps are required in order to set up and use the SOAP header for our security credentials:

  • Implement a SoapHeader class that defines the data we require from the consumer.

  • Implement a method that takes the username and password values and uses them to authenticate the user and set up the principal object on the current thread.

  • Apply a [SoapHeader()] attribute to all our web methods as we write them, indicating that the web method requires our particular SOAP header.

We'll follow these steps as we create the rest of our code.

Creating the SoapHeader Class

SoapHeader is just a class that defines some fields of data that are to be included in the XML header data of a SOAP message that we talked about earlier. In our case, we need two valuesusername and passwordto be passed in the SOAP header along with every method call. Our SoapHeader class will clearly define this requirement. In the ProjectTracker class, add the following code:

  #region Security     public class CSLACredentials : SoapHeader     {       public string Username;       public string Password;     }     public CSLACredentials Credentials = new CSLACredentials();     #endregion  

Note that this code is inside the existing ProjectTracker web-service class; we're creating a nested class named CSLACredentials . By nesting it within our web-service class, we ensure that it's properly exposed to the consumers via our WSDL definition. The class itself is very simpleit just defines the two data fields we require, as shown here:

 public string Username;     public string Password; 

More important is the fact that it inherits from SoapHeader . Not surprisingly, it's this line that turns our class into a SoapHeader . It means that its values will be automatically populated by the .NET runtime, based on the data in the SOAP header that's provided as part of each method call.

The CSLACredentials class is also public in scope, so it will be included as part of our web-service definition in our WSDL. This means that any consumers that reference our web service will have full access to the type information that we've defined here, so they will clearly see that we require username and password values.

Tip 

If we're creating the consumer with VS .NET, it will automatically create a consumer-side proxy class for CSLACredentials , thus dramatically simplifying the process of providing the data. We'll see an example of this later in the chapter.

We also declare a variable based on this class, as follows:

 public CSLACredentials Credentials = new CSLACredentials(); 

This step is required because the actual data values will be placed into this object. There's no magic herewe'll just use the [SoapHeader()] attribute on each method call to indicate that the SOAP header data should be loaded into this object.

Using the [SoapHeader()] Attribute

Now that we've defined a SoapHeader class, any consumer that references our web service will have a clearly defined structure into which the username and password values can be placed. By default, web methods don't require SOAP headers, but we can use the [SoapHeader()] attribute on a web method to indicate that it does. This attribute accepts a parameter that we can use to link the SOAP header to a specific SoapHeader object in our codein this case, to the Credentials object that we declared to be of type CSLACredentials .

This means that our web methods will be declared like this:

  [WebMethod(Description="A sample method")]   [SoapHeader("Credentials")]   public void SampleMethod()   {     // Web method implementation code goes here   }  

When this method is invoked by a consumer, the .NET runtime uses reflection to find a variable within our code called Credentials . It then uses reflection against that Credentials variable to discover its type. Based on that type information, it looks at the SOAP header data to find the SOAP header that matches that type, and takes the appropriate data out of the SOAP header.

This SOAP XML might look something like this (with our header in bold):

 POST /PTservice/projecttracker.asmx HTTP/1.1 Host: localhost Content-Type: text/xml; charset=utf-8 Content-Length: length SOAPAction: "http://ws.lhotka.net/PTService/ProjectTracker/GetResourceList" <?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance     xmlns:xsd="http://www.w3.org/2001/XMLSchema"     xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">   <soap:Header>  <CSLACredentials xmlns="http://ws.lhotka.net/PTService/ProjectTracker">       <Username>string</Username>       <Password>string</Password>     </CSLACredentials>  </soap:Header>    <soap:Body>      <GetResourceList xmlns="http://ws.lhotka.net/PTService/ProjectTracker" />    </soap:Body> </soap:Envelope> 

That data is loaded into our Credentials object, and then the web method itself is called.

Note 

Note that the [SoapHeader()] attribute indicates a required SOAP header, so our method can only be called by a consumer that provides this information.

This means that by the time our web-method code is running, the Credentials object will be loaded with the username and password values provided by the consumer, via the SOAP header.

Implementing a Login Method

Now that we have a way of requiring the consumer to provide a username and a password, and of making those values automatically available to our code, we can move on to implement a method to authenticate the user based on those values.

As part of this process, the current thread's principal object will be set to a valid BusinessPrincipal object, meaning that our business objects, the CSLA .NET Framework objects, and our web-service code can all use standard .NET security code to check whether the user was authenticated, and to determine the user's roles.

Since each method will need to perform this login step, we'll create a single method within the ProjectTracker class to take care of the details. Then our web methods can just call this one method to do the work. Add the following method within the Security region in our code:

  public void Login()    {      if(Credentials.Username == null  Credentials.Username.Length == 0)        throw new System.Security.SecurityException("Valid credentials not provided");      BusinessPrincipal.Login(Credentials.Username, Credentials.Password);      System.Security.Principal.IPrincipal principal =        Thread.CurrentPrincipal;      if(principal.Identity.IsAuthenticated)      {        // the user is valid - set up the HttpContext        HttpContext.Current.User = principal;      }      else      {        // the user is not valid, raise an error        throw new System.Security.SecurityException("Invalid user or password");      }    }  

Since the Credentials object is automatically populated by the .NET runtime, we can simply write code to use it. First, we check to make sure the consumer provided us with a username that actually contains data. If we get past that check, we just call the Login() method of our BusinessPrincipal class.

BusinessPrincipal.Login() attempts to authenticate the username and password against our security database, and creates a BusinessPrincipal object that reflects the success or failure of that effort. That object is automatically set to be the thread's current principal object, so all our code has easy access to the information using standard .NET security coding techniques.

Once this is done, we ensure that the user was successfully authenticated, as follows:

 System.Security.Principal.IPrincipal principal =         Thread.CurrentPrincipal;       if(principal.Identity.IsAuthenticated)       {         // the user is valid - set up the HttpContext         HttpContext.Current.User = principal;       } 

If the user credentials weren't valid, we raise an exception that's automatically returned to the consumer by the .NET runtime. Thanks to this, the consumer will know that its method call failed due to a security violation.

All of this work ensures that only valid, authenticated users gain access to our web methods, provided that those methods have the following structure:

 [WebMethod(Description="A sample method")]    [SoapHeader("Credentials")]    public void SampleMethod()    {  Login();  // Web method implementation code goes here    } 

Now we can move on to define the data structures and implement the web methods that form the contract we're making with our consumers.

Defining Data Structures

Earlier in the chapter, we discussed the dangers involved in exposing business objects directly via web services, and decided that we're best served by creating clearly defined data structures that are independent of our actual business objects. These structures will be exposed to consumers, thereby forming a large part of the contract that we agree to uphold once our web service has been published.

If we define these struct types with public scope within our ProjectTracker class, they will automatically become part of our web-service definition (as defined in the WSDL), so consumers have easy access to the descriptions of the structures. Add the following to the ProjectTracker class:

  #region Data Structures     public struct ProjectInfo     {       public string ID;       public string Name;       public string Started;       public string Ended;       public string Description;       public ProjectResourceInfo [] Resources;     }     public struct ProjectResourceInfo     {       public string ResourceID;       public string FirstName;       public string LastName;       public string Assigned;       public string Role;     }   public struct ResourceInfo     {       public string ID;       public string Name;       public ResourceAssignmentInfo [] Assignments;     }     public struct ResourceAssignmentInfo     {       public string ProjectID;       public string Name;       public string Assigned;       public string Role;     }     #endregion  

For the most part, these struct types mirror our business objects, and you might think that this isn't particularly surprising. The real benefit here, however, is that our business objects can change over time without risk of breaking these data structures. We might add new values to Project or Resource as our application's requirements change over time, but our web-service interface can remain consistent.

Note 

The reality is that we'll probably need to update our web-service interface over time as well, but this split allows us to keep application updates and web-service updates largely independent of one another.

The ProjectInfo data structure defines the basic data for a project, including an array containing data about the resources assigned to the project. The ResourceInfo data structure provides similar information for a resource.

Notice that the data here is all of type string . This isn't requiredthe web-services infrastructure supports many common data types, including int , DateTime , and so forthbut in our case we're supporting the concept of a blank date value, and a string allows us to represent this more readily than a DateTime value could. We also have the project ID values, which are internally of type Guid . Not all client platforms have native support for a GUID data type, so it's better to leave them as type string too.

Tip 

Even if we do set up our variables to use DateTime or Guid data types, remember that the data is ultimately converted into XML text anyway. All we're doing here is dictating the way .NET converts the text data into and out of our local variables.

When we turn to look at the ProjectResourceInfo data structure, we see that it includes both read-only and read-write data fields. Remember that some of the fields in our ProjectResource object are read-only because they're "borrowed" from the Resource objectin reality, only the Role field can be changed.

For better or worse , we have no way of conveying this sort of information to consumers. The SOAP standard isn't expressive enough to allow us to define some things as read-only, and others as read-write. All we can do is provide data in a generic form for consumers to use as they see fit.

What this means is that documentation is important. It's not enough simply to publish a web service and expect the WSDL to be a sufficient documentation tool. WSDL contains a lot of valuable information, but it doesn't replace human-readable documentation describing other requirements or restrictions on our data.

Tip 

WSDL doesn't convey semantic information about the meaning of data, either. Though we don't have anything in our sample application that's semantically complex, most business applications do. If we expose a ProductID field, what is the meaning of that field? Is it a simple ID, or is it one in which the first two characters indicate one thing, the next four indicate another thing, and the digits at the end indicate something else? None of these subtleties are part of WSDL, which means that we need to provide good documentation for the users of our web services.

By defining these data structures, we've clarified half of the contract we're creating with consumers. The rest of the interface comes in the form of our web methods themselves . The name of each web method, its parameter list, and its return values comprise the remainder of the interface we're creating, so let's move on to create them.

Get a Project List

.NET makes the creation of a basic web method very easy: You simply apply the [WebMethod()] attribute to a method in a web service, and you're all set. The trickier part is implementing the logic within the web method.

Fortunately for us, our business objects do most of the work. All we need to do is take the data from our business objects, and put it into the data structures we've defined as part of our web service's interfaceor vice versa.

For instance, let's look at what is probably the simplest case: retrieval of a list of projects. We already have a ProjectList business object that retrieves basic project information. Using that object, we can just loop through each project to populate our ProjectInfo data structure. Since this is our first web method, we'll walk through it in some detail, as follows:

  #region Projects     [WebMethod(Description="Get a list of projects")]     [SoapHeader("Credentials")]     public ProjectInfo [] GetProjectList()     {       Login();       ProjectList list = ProjectList.GetProjectList();       ProjectInfo[] info = new ProjectInfo[list.Count];       int index = 0;       foreach(ProjectList.ProjectInfo project in list)       {         // ProjectList only returns two of our data fields         info[index].ID = project.ID.ToString();         info[index].Name = project.Name;         index++;       }       return info;     }     #endregion  

Here we have the standard declaration of a web method, which includes our [SoapHeader()] attribute so that we get security credentials:

 [WebMethod(Description="Get a list of projects")]     [SoapHeader("Credentials")]     public ProjectInfo [] GetProjectList() 

Notice that the return type from the method is an array of ProjectInfo data structures. From our perspective as web-service authors, this is very simple, readable code. It's also easily understood by development tools that might be used to create a consumer.

Next, the Login() method is called to authenticate the user and get a valid BusinessPrincipal object into our thread's current principal slot, as follows:

 Login(); 

Once we're past this call, we know that the user has been successfully authenticated, so now we can move on to do our work. If the authentication process fails, it throws an exception, which is automatically returned to the consumer by the .NET runtime.

We don't need to include code to check the user's role (which affects her ability to perform certain operations) because our business objects already perform those checks. In our Windows Forms and Web Forms interfaces, we checked those roles to enable or disable various UI components. With a web-services interface, all we can do is throw an exception in the case of failureand our business objects already have that functionality implemented.

After logging in, we retrieve a ProjectList object, as shown here:

 ProjectList list = ProjectList.GetProjectList(); 

Then we can use the following information from this collection object to dimension and populate our array of ProjectInfo data structures:

 ProjectInfo[] info = new ProjectInfo[list.Count];     int index = 0;     foreach(ProjectList.ProjectInfo project in list)     {       // ProjectList only returns two of our data fields       info[index].ID = project.ID.ToString();       info[index].Name = project.Name;       index++;     } 

This is simply a matter of copying the values from each element in the ProjectList collection into an element in our array. Once the values have been copied , we simply return the array as a result, as follows:

 return info; 

Even if our business objects change over time, we can preserve the web-service interface just by updating the code here to accommodate the new business object or model. Any consumers will continue to get a consistent, predictable result.

Get a Project

Almost as a variation on the theme, we can provide a web method that returns detailed information about a specific project, based on the ID value. This is no more complex, because we're still just copying data from business objects into data structures. In this case, we can return all the data about a project, since we have a full-blown Project object rather than the more lightweight data from the ProjectList object, as follows:

  [WebMethod(Description="Get detailed data for a specific project")]    [SoapHeader("Credentials")]    public ProjectInfo GetProject(string id)    {      Login();      ProjectInfo info;      Project project = Project.GetProject(new Guid(id));      info.ID = project.ID.ToString();      info.Name = project.Name;      info.Started = project.Started;      info.Ended = project.Ended;      info.Description = project.Description;   // load child objects     info.Resources = new ProjectResourceInfo[project.Resources.Count];     for(int idx = 0; idx < project.Resources.Count; idx++)     {       info.Resources[idx].ResourceID = project.Resources[idx].ResourceID;       info.Resources[idx].FirstName = project.Resources[idx].FirstName;       info.Resources[idx].LastName = project.Resources[idx].LastName;       info.Resources[idx].Assigned = project.Resources[idx].Assigned;       info.Resources[idx].Role = project.Resources[idx].Role;     }     return info;   }  

Here we retrieve the requested Project object based on its ID value, as shown:

 Project project = Project.GetProject(new Guid(id)); 

Again, all the hard work is done in a single line of codeall we do now is copy the values from the populated business object into the data structure, as follows:

 info.ID = project.ID.ToString();       info.Name = project.Name;       info.Started = project.Started;       info.Ended = project.Ended;       info.Description = project.Description; 

Because the business object and the data structure have clearly defined elements, we get full Intellisense support while typing the code, and the result is very clear, readable, and thus maintainable .

What makes the function more interesting is that we're loading not only the basic information, but also an array with all the resources that are assigned to the project. First we size the array to the appropriate length, as follows:

 // load child objects       info.Resources = new ProjectResourceInfo[project.Resources.Count]; 

After that, it's just a matter of looping through all the items in the business collection, copying the data from each into an element of the array:

 for(int idx = 0; idx < project.Resources.Count; idx++)       {         info.Resources[idx].ResourceID = project.Resources[idx].ResourceID;         info.Resources[idx].FirstName = project.Resources[idx].FirstName;         info.Resources[idx].LastName = project.Resources[idx].LastName;         info.Resources[idx].Assigned = project.Resources[idx].Assigned;         info.Resources[idx].Role = project.Resources[idx].Role;       } 

Again, the code is quite readable because the data structure and the business object have clearly defined properties.

Add or Update a Project

The two web methods that we've implemented so far merely retrieve data. However, there's nothing stopping us from providing a web method that allows a consumer to add or update project data. All we need to do is accept a ProjectInfo data structure as a parameter, copy its values into a Project object, and ask the object to save itself.

Since the business object already contains all our business logicincluding validation logicthe only thing we need to figure out is whether the project is to be added or updated. We can do this by checking to see if the consumer passed us an ID value for the project. If it did, we'll assume that it means to update a project; otherwise , it will be considered an add operation. Here's the complete code:

  [WebMethod(Description="Add or update a project " +      "(only provide the ID field for an update operation)")]    [SoapHeader("Credentials")]    public string UpdateProject(ProjectInfo data)    {      Login();      Project project;      if(data.ID == null  data.ID.Length == 0)      {        // no ID so this is a new project        project = Project.NewProject();      }      else      {        // they provided an ID so we are updating a project        project = Project.GetProject(new Guid(data.ID));      }      if(data.Name != null)        project.Name = data.Name;      if(data.Description != null)        project.Started = data.Started;      if(data.Ended != null)        project.Ended = data.Ended;      if(data.Description != null)        project.Description = data.Description;      if(data.Resources != null)      {        // load child objects        for(int idx = 0; idx < data.Resources.Length; idx++)        {   if(project.Resources.Contains(data.Resources[idx].ResourceID))        {          // update existing resource          // of course only the role field is changeable          project.Resources[data.Resources[idx].ResourceID].Role =            data.Resources[idx].Role;        }        else        {          // just add new resource          project.Resources.Assign(data.Resources[idx].ResourceID, data.Resources[idx].Role);        }      }    }  

The first thing we do is to decide whether the client application is adding or updating a project:

 Project project;       if(data.ID == null  data.ID.Length == 0)       {         // no ID so this is a new project         project = Project.NewProject();       }       else       {         // they provided an ID so we are updating a project         project = Project.GetProject(new Guid(data.ID));       } 

If we're provided with an ID value, we use it to retrieve the existing Project object. Otherwise, we create a new Project object (automatically preloaded with any default values, of course).

Then we simply copy the values from the ProjectInfo data structure into the business objectincluding any resource assignments. That's a little tricky, because we need to determine if a particular resource assignment is new, or if the user just wants to change the Role property of an existing one. Fortunately, we implemented a Contains() method on our ProjectResources collection object, so it's easy to determine whether a specific resource has already been assigned to the project, as follows:

 if(project.Resources.Contains(data.Resources[idx].ResourceID)) 

If our collection already contains the resource, we simply update the Role property. If not, we call the Assign() method to assign the resource to the project.

Once all the data fields have been copied from the data structure into our business objects, we just call the Save() method to update the database, as shown here:

 project = (Project)project.Save(); 

The nice thing about the use of business objects in this case is that we don't need to worry about enforcing business rules, validation, or security. These rules and any other business processing are handled by the objects, and are therefore totally consistent across our Windows Forms, Web Forms, and web-services interfaces.

If the consumer attempts to do anything invalid, our business objects will detect that, and throw an exception. The exception is returned to the consumer so that it knows its action was invalid.

Delete a Project

The only other major operation that we should support is the ability to remove a project from the system. As with our other web methods, this is easily accomplished through the use of our business objects. In fact, for a delete operation, it's just one line of code, as follows:

  [WebMethod(Description="Remove a project from the system")]     [SoapHeader("Credentials")]     public void DeleteProject(string projectID)     {       Login();       Project.DeleteProject(new Guid(projectID));     }  

All security checks are handled by the business object code, so we can simply call the DeleteProject() method and let the magic happen.

Resource Web Services

There's a corresponding set of four web methods to deal with resources as well. The GetResourceList() , GetResource() , and DeleteResource() methods are virtually identical to the code we've just seen, so we won't go through them here. The code for those methods is included in the code download for this book.

There are some slight differences in the UpdateResource() method, however, because the ID value for a resource isn't a GUID, but a user-supplied value. Because of this, in UpdateResource() we need to use a different strategy for determining whether we're adding or updating a Resource object.

Rather than checking for a blank ID value, we always require an ID value. To find out if that corresponds to an existing Resource object, we attempt to load the object. If there's no corresponding Resource object, we'll get an exception, which we can catch, as follows:

 Resource resource;         try         {           resource = Resource.GetResource(data.ID);         }         catch         {           // failed to retrieve resource, so this must be new           resource = Resource.NewResource(data.ID);         } 

If an error occurs, we simply create a new, empty Resource object (which will automatically be loaded with default values by our business logic).

From this point forward, the functionality is virtually identical to what we saw in UpdateProject() . We simply copy the data from the data structure into the Resource object, and then call its Save() method to update the database. The object contains the code to do an insert or update operation, as appropriate.

At this point, our web service is complete. We have a set of web methods that allow any consumer using SOAP to interact with our application's business logic and data. These consumers may be running on any hardware platform or OS, and may be written in virtually any programming languagewe can't know and don't care. What we do know is that they're interacting with our data through our business logic, including validation and security, thereby making it difficult for a consumer to misuse our data or functionality.



Expert C# Business Objects
Expert C# 2008 Business Objects
ISBN: 1430210192
EAN: 2147483647
Year: 2006
Pages: 111

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