Microsoft Visual J# .NET (Core Reference) (Pro-Developer)
I l @ ve RuBoard |
So far, this chapter has focused on describing a Web service and defining the types passed between client and server. However, this is only one consideration when you create a Web service application. Let's look at some of the other issues you'll commonly deal with when you create distributed applications ”such as security, transactions, and exposing existing (heritage) functionality. Web Services as ASP.NET Applications
Most Web service implementations under .NET will be ASP.NET Web services, which means that they'll have all the underlying capabilities of an ASP.NET Web application. As noted earlier, you'll have access to all of the intrinsic objects available to an ASP.NET Web application, such as Context , Server , and Application . The model of invocation is also the same, in that the page is instantiated to service the request and is then discarded once the method has been completed. This means that many of the same principles apply in terms of component lifecycle. State Management
The state management model for an ASP.NET Web service is exactly the same as for an ASP.NET Web application ”it uses the Session and Application objects as described in Chapter 16. You might recall that the Session object is created on a per-client basis, normally based on cookies passed between the client and server. The Application object is shared globally between all clients of an application and does not rely on the passing of state information. As you might expect, access to the Application object does not require any special processing or decoration in an XML Web service. The following code shows how you can maintain a Web service “wide counter by storing it in the Application object: /**@attributeWebMethodAttribute()*/ publicintIncrement()throwsSystem.ApplicationException { intcount=0; if(get_Application()==null) { thrownewApplicationException("Applicationwasnull"); } System.Objectobj=get_Application().get_Item("count"); if(obj==null) { get_Application().Add("count",newInteger(0)); } obj=get_Application().get_Item("count"); IntegerobjCount=(Integer)obj; count=objCount.intValue(); count++; objCount=newInteger(count); get_Application().set_Item("count",objCount); returncount; } If a client calls this method repeatedly, it will see that the returned count increments . On the other hand, you do not get a per-client session by default. You must set the EnableSession property of the WebMethod attribute to true for each method that needs to maintain state information. If you want to allow clients to build up an order over multiple calls to the service, you must start by initializing a new order, as shown in the following code: /**@attributeWebMethodAttribute(EnableSession=true)*/ publicvoidStartBuiltUpOrder(Stringname,intid) { Orderorder=newOrder(); order.customerName=name; order.orderId=id; get_Session().Add("Order",order); } After the customer informs the client application of the items he wants to order, the client can call a method to add each item in turn to the order maintained by the service on the client's behalf : /**@attributeWebMethodAttribute(EnableSession=true)*/ publicvoidAddToOrder(Stringdescription,intquantity) throwsSystem.ApplicationException { Itemitem=newItem(); item.itemDescription=description; item.quantity=quantity; Orderorder=(Order)get_Session().get_Item("Order"); if(order==null) { thrownewApplicationException("Orderwasnull"); } intlength=0; Item[]newItems=null; if(order.items==null) { length=1; newItems=newItem[length]; } else { length=order.items.length+1; newItems=newItem[length]; Array.Copy(order.items,newItems,length-1); } //Replacetheitems order.items=newItems; //Addthenewitem order.items[length-1]=item; } This approach involves a certain amount of processing overhead. The method first checks to see that there is currently an order for this client; if not, an exception is thrown. The new item is then added to the array of items held in the order. This array manipulation comes about because we're using the same Order object to hold the state as it is passed between client and server for simple orders. Given that this object is accessed only on the server side when you build up the order dynamically, you can, for convenience, rewrite the Order class so that it uses one of the System.Collection types rather than an array. All of the preceding code showing examples of application and session state is contained in the Ordering.asmx.jsl sample file. One obvious requirement for the session-based state management to work is that the client must support and allow cookies. This support is not enabled by default on client-side XML Web service proxies generated under .NET, so you must enable it explicitly. (This topic is covered in Chapter 18.) XML Web services cannot easily use the cookieless support for sessions. The cookieless support relies on the use of URL rewriting (mangling) and redirection of the client to a mangled URL (an HTTP 302 Found response that tells the client to go to another page). An example is shown here: HTTP/1.1302Found Server:Microsoft-IIS/5.1 Date:Tue,07May200212:58:15GMT Location:/CakeCatalogService/(a55ctz55y2oyq0mnsenwv555)/Ordering.asmx Cache-Control:private Content-Type:text/html;charset=utf-8 Content-Length:177 <html><head><title>Objectmoved</title></head><body> <h2>Object movedto <ahref='/CakeCatalogService/(a55ctz55y2oyq0mnsenwv555)/Ordering.asmx'>here </a>.</h2> </body></html> A standard Web service proxy generated under the .NET Framework will not handle this response because it expects an HTTP 200 OK response containing a SOAP envelope. You can potentially provide your own proxies or roll your own sessions by passing a customer SOAP header, but a discussion of such strategies is beyond the scope of this chapter. Signaling Errors
When you create classes and interfaces for use under the .NET Framework, you generally signal error conditions using exceptions. You can subclass the ApplicationException class and define your own application-specific exceptions. But what happens when you need to indicate an error across a Web service interface? Consider the scenario in which a client calls the Web method GetCustomer . If the code used by the method cannot access the database it needs, an SqlException will be generated. Unless you catch the exception, it will ripple all the way down the stack until it reaches the point at which the GetCustomer call arrived at the server. This exception must somehow be propagated back to the client. However, SOAP does not support the concept of an exception. Instead, a SOAP response can include a SOAP Fault to indicate that an error occurred on the server. The format of a .NET exception includes a stack trace and possibly nested exceptions. This does not map easily into a SOAP Fault, so the .NET Framework Class Library defines the System.Web.Services.Protocols.SoapException class, which contains a set of properties that map to a SOAP Fault, including
In the case of an unhandled exception, the message is taken from the original exception, such as the SqlException in the example, and copied to the generated SoapException . The name of the originating Web method is set on the SoapException , and the SoapException is then converted into a SOAP Fault and sent back to the client. The benefit of encapsulating error information in a SOAP Fault is that any type of client can take this information and interpret it. In the case of a client built using the .NET Framework, the SoapException is regenerated based on the SOAP Fault. All of the usual .NET Framework exception information, such as the stack trace, is added at this point. The exception is then thrown on the client side and hopefully caught by the client code! The default error handling provided in a .NET Framework XML Web service is a useful fallback for propagating errors. However, it is not ideal because the message from the underlying exception might not have much meaning to the client. Therefore, it is best to catch exceptions that occur during a Web method call and then explicitly generate SoapExceptions containing the appropriate information. The advantage of this approach is that not only do you get to set a meaningful message, but you can also include more information using the Detail property. To use the Detail property, you must create an appropriate XML document that can be used as part of the SOAP Fault. This document must contain a predefined root element named detail to fit with the SOAP specification. The correct name and namespace are provided by the read-only DetailElementName and DetailElementNamespace properties of the SoapException class. The following code, taken from the EnquiryWithException.asmx.jsl sample file, shows how to use these properties: XmlDocumentdoc=newXmlDocument(); //CreateaSOAP-compliantdetailelementthatcanbe //usedaspartoftheSOAPFault. XmlElementroot= doc.CreateElement(SoapException.DetailElementName.get_Name(), SoapException.DetailElementName.get_Namespace()); doc.AppendChild(root); You can then populate the detail element with your own error information. The following example shows a version of the FeedsHowMany method that throws a SoapException if either of its string parameters is null : public intFeedsHowMany(intdiameter, Stringshape, Stringfilling)throwsSoapException { if(filling==nullshape==null) { StringdetailNamespace= "http://fourthcoffee.com"; Stringprefix= "coffee"; XmlDocumentdoc=newXmlDocument(); //CreateaSOAP-compliantdetailelementthatcanbe //usedaspartoftheSOAPFault. XmlElementroot= doc.CreateElement(SoapException.DetailElementName.get_Name(), SoapException.DetailElementName.get_Namespace()); doc.AppendChild(root); //Addourowncontenttothedetailmessage XmlElementparams=doc.CreateElement(prefix, "Params",detailNamespace); root.AppendChild(params); XmlElementxmlShape=doc.CreateElement(prefix, "Shape", detailNamespace); xmlShape.AppendChild(doc.CreateTextNode(shape)); params.AppendChild(xmlShape); XmlElementxmlFilling=doc.CreateElement(prefix, "Filling", detailNamespace); xmlFilling.AppendChild(doc.CreateTextNode(filling)); params.AppendChild(xmlFilling); thrownewSoapException("Youpassedinanullargument", SoapException.ClientFaultCode, "FeedsHowManyWithException", doc); } } A Params element is created below the details element. This Params element contains two other elements, each of which represents the value of one of the two string parameters passed into the method. By examining this information, the client can determine which parameter was incorrect. These child elements are defined in a namespace specific to the example company (Fourth Coffee); this is good practice whenever you create documents to be passed across Web service boundaries. The SoapException is created using the generated detail document. The use of the ClientFaultCode flag indicates that the processing difficulty was due to a problem with the parameters sent from the client. Note that the method must declare that it throws a SoapException . The SoapException can be caught on the client in a try/catch block. The SOAP Fault generated by passing a null Shape argument is shown here: HTTP/1.1500InternalServerError. Server:Microsoft-IIS/5.1 Date:Tue,07May200213:40:32GMT Cache-Control:private Content-Type:text/xml;charset=utf-8 Content-Length:816 <?xmlversion="1.0" encoding="utf-8"?> <soap:Envelopexmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <soap:Fault> <faultcode>soap:Client</faultcode> <faultstring>System.Web.Services.Protocols.SoapException:Youpassed inanullargumentat CakeCatalogService.EnquiryWithException.FeedsHowMany(Int32diameter, Stringshape,Stringfilling)in c:\inetpub\wwwroot\CakeCatalogService\EnquiryWithException.asmx.jsl:line 86</faultstring> <faultactor>FeedsHowManyWithException</faultactor> <detail> <coffee:Paramsxmlns:coffee="http://fourthcoffee.com"> <coffee:Shape> </coffee:Shape> <coffee:Filling>fruit</coffee:Filling> </coffee:Params> </detail> </soap:Fault> </soap:Body> </soap:Envelope> Securing a Web Service
XML Web services based on ASP.NET use the same security mechanisms as ASP.NET Web applications (discussed in Chapter 16). Given the nature of Web services, some security options such as forms-based authentication are redundant. However, new options are also available, such as the use of SOAP headers to carry authentication information. You could write a whole chapter (if not a whole book) on Web services and security, so this section will employ the Pareto Principle ( otherwise known as the 80/20 rule) and concentrate on the three main options:
The type of security you use will depend on your requirements for authentication and privacy. If you need privacy for data in transit, you must select one of the SSL options. If you do not need privacy, it is quicker and easier to use Windows-based authentication. Be sure that you have Windows authentication specified in the Web.Config file for your Web service: <configuration> <system.web> <authenticationmode="Windows" /> </system.web> </configuration> In the context of the online cake store, you might want to provide access to certain additional services that are restricted to business partners. For example, consider an XML Web service called PartnerServices.asmx that contains the following method to give partners access to the secret Fourth Coffee cake recipe (which is not fully listed here due to commercial confidentiality): /**@attributeWebMethodAttribute()*/ publicStringGetRecipe() { return "Eyeofnewtandtoeofbat..."; } By default, Web services built using Visual Studio .NET are configured to allow anonymous access. This means that the client does not need to take any special measures to propagate their security information. Such unlimited access might be fine in an intranet environment, but it's not suitable for interorganization or cross-department Web services in which security is important. The need for authentication will also vary depending on the sensitivity of the functionality being exposed. (You would not want to allow everyone to increase their salary by using the HR Web service, for example!). To protect a Web service, you can use IIS security to indicate the type of authentication required, as follows :
If a client now tries to access the service without authentication, it will receive this error message: TherequestfailedwithHTTPstatus401:Accessdenied. To access the Web service, the client must provide credentials to authenticate itself. These credentials can be a combination of username, password, and domain name. If the client is part of the same domain or workgroup as the Web service host computer, you can also use integrated security. Details on passing credentials from a client to a .NET Framework Web service are provided in Chapter 18. You can grant or deny access to a specific Web service for a particular user or set of users by using the <authorization> element in the Web.Config file. This is the same mechanism employed by APS.NET Web applications. Chapter 16 discussed the use of the <authorization> element in detail. You've now added authentication to your Web service. To protect data in transit you can employ SSL. To use SSL, you must obtain and install a server certificate. Clients will then access your Web service through a URL starting with https :// in place of http:// . You can make this happen by editing the WSDL generated by the Web service to use https in its service URL or by changing the URL used by the client at run time. Details about setting the target URL for a Web service client at run time are discussed in Chapter 18. For a more flexible security solution across organizational boundaries, you can use client certificates. As before, you should use Windows authentication, but you should also associate specific client certificates with identities defined on your server. This configuration is performed under the Internet Information Services administration tool used earlier by using the Properties dialog box of the specific Web service files or over the whole virtual directory containing your Web services. On the File Security or Directory Security tab of the Properties dialog box, you'll find a section labeled Secure Communications that allows you to configure client certificates. For more details, see the product documentation. Once you've configured the certificates to use, you can pass them as part of the SOAP message sent by the client. Again, for more details, see Chapter 18. You can find details on using SOAP Headers as a means of transporting authentication information in the .NET Framework documentation in the section titled "Securing XML Web Services Created Using ASP.NET." Transactions and Web Services
Many business applications require updating of multiple databases as part of one business operation. To do this in a controlled and consistent way, you generally need to employ transactions. You can write code that controls transactions on a per-connection basis, or you can interact with the Distributed Transaction Coordinator (DTC) to take part in transactions that span multiple databases. As you saw in Chapter 14, serviced components , such as those provided by COM+, can take part in transactions without your having to write transaction-specific code. The component is tagged with TransactionAttribute , indicating to its container the type of transaction support it requires. Even if a transaction does not span multiple databases, you might find it convenient to use declarative transactions to simplify development. Just as transactions are important to components, they are also important to Web services. A Web service will typically occupy the same role in an application as a serviced component ”as a repository of business logic to be called by a client as necessary. Hence, you want to obtain the same level of transactional control. As before, you can control transactions on a per-connection basis if you're communicating with a single database. Methods for controlling such transactions are provided through the ADO.NET connection classes. However, you can also use declarative transactions with ASP.NET-based Web services by taking advantage of the transaction support in the System.EnterpriseServices namespace. To do this, you must specify the TransactionOption property of the WebMethod attribute. You assign this property a value from the TransactionOption enumeration to indicate what type of transaction support you need for the method. (The TransactionOption enumeration lives in the System.EnterpriseServices namespace, so remember to import this into the Web service JSL file and to add the System.EnterpriseServices.dll assembly as a reference into the project.) The following code shows an updated form of the SubmitOrder method that uses a transaction attribute: /**@attributeWebMethodAttribute(TransactionOption= TransactionOption.RequiresNew)*/ publicWsReceiptSubmitOrder(Orderorder, booleanshouldFail) { //Shouldprobablyhavesomedatabasestuffhere... if(shouldFail) { thrownewSoapException("Becauseyouaskedforthis..." + msg,SoapException.ClientFaultCode); } WsReceiptreceipt=newWsReceipt(order.get_CustomerName(), order.get_OrderId(),DateTime.get_Now()); returnreceipt; } When a client calls this form of the SubmitOrder method, a new transaction will be started in which the method will run. An exception will cause the transaction to roll back. If no exceptions are thrown, ASP.NET will try to commit the transaction. You can access the current transaction context through the ContextUtil class and use the static methods SetAbort and SetComplete to control the outcome of the transaction. It is important to understand that a Web method can act only as the root of a transaction. There is currently no standard way of propagating transaction information over SOAP calls (at least not until the GXA arrives), so a transaction started in a Web service client will be suspended while the Web service call is made. If a transaction is started by ASP.NET in response to a Web service method call, the transaction will propagate through calls to other .NET objects and COM+ components. Any updates to transactional resources, such as a database, made by these objects or components will become part of the transaction. These objects or components can affect the outcome of the transaction by throwing exceptions or setting the transaction state in the transaction context (through such methods as SetAbort ). In the same way that a client cannot propagate a transaction to a Web service, one Web service cannot propagate its transaction onto another Web service ”the same issue of crossing a SOAP boundary applies. If a Web service method calls another Web service method that is denoted as transactional, it will suspend any existing transaction and start a new transaction for the new method. Exposing Existing Applications as Web Services
In some cases, you'll create Web service-based applications from scratch. But in other cases, you might want to incorporate existing functionality into new Web service applications or provide Web service wrappers for existing functionality. We'll discuss this topic next . What Do You Have Already?
Existing functionality can come in a variety of forms:
When you're looking to reuse existing functionality as part of a Web service, you should take some time to think about whether it actually fits into the Web service model. Considerations include:
If you anticipate serious issues with the functionality you want to expose, you might want to use an alternative strategy, as described next. Strategies for Exposing Functionality
To expose existing functionality as a Web service, you can choose from a few different approaches, depending on your requirements:
Ultimately, Web services are a very flexible form of distribution. But as with any form of distribution, you must be careful about what you distribute and consider the consequences of doing so.' |
I l @ ve RuBoard |