The objectives
The goal here is to identify the user. For more of the Web Services, this might not be needed. There seems to be a common set of Web Services that only serve for small purposes where they only make available one single method of just a few. So, for them, either they do not require identifying the user or they just require two additional parameters in the method call to obtain the username and password. But, when your Web Service grow up, as soon as you hit over 20 methods or up to 100, for example, this approach is not practical. Basically, it doesn't make much sense to require the username and password for every method call as the first two parameters. This overloads the complexity of your Web Service, requires two additional parameters for every method call and irritates your users.
Until SOAP 2.0, the use of SOAP header was an approach we could use for that. However, the problem with that is that it was requiring your Web Service to be bound to the SOAP header and required the same from the client side. Thus, you had to provide some samples to your users on how to use it. As this was getting extremely complex depending on the environment, it was making it difficult to users of other environments, not part of your samples, to use your Web Service.
SOAP 3.0 comes up by maintaining its state. It brings a brand new concept of adding new functionalities to your Web Service. Basically, it goes such as a Web Browser. When a user accesses your Web site, lets say Universal Thread, he only has to login once and then has access to the messages area and is able to execute a series of requests. The user is not responsible to login each time or to pass the login info by any sort of mechanism. This is all handled automatically by the HTTP protocol. Well, the same is now true with SOAP 3.0 where the state is maintained and navigates quite well over the HTTP layer.
From the client side
In order to provide your Web Service with session state, the user can now consumes your Web Service by following those commonly used steps such as he would do for any other Web Service:
loUniversalThread=Createobject('MSSOAP.SoapClient30') loUniversalThread.MSSOAPInit('http://www.universalthread.com/universalthread.wsdl') loUniversalThread.Login(lcUsername,lcPassword) loUniversalThread.GetMessage(DATE())
The Web Service
Your Web Service should be responsible to establish a mechanism to create the state and recognize it for every other upcoming request, assuming the Login() method would be successful. It should also have a mechanism in place to protect any call to a method if the Login() method was not initiated. So, we will assume all of your methods are calling a method to detect that. Assuming it would fail, a COMRETURNERROR() call could be initiated to return a proper message to the user.
In order for your Web Service to benefit of such an implementation, various approaches can be used. Basically, it goes the same as any Web application you may have done so far. In our case, we will use the cookie approach.
Using an intrinsic approach
Your Web Service should be capable of getting access to the ASP intrinsic objects in order to benefit of the Request and Response objects for example. So, the first goal is to make those objects available within your environment. The following method can be added in your Web Service in order to instantiate those objects.
* Create the ASP object FUNCTION GetASP This.oMTS=CREATEOBJECT('MTXaS.Appserver.1') This.oObjectContext=This.oMTS.GetObjectContext() This.oRequest=This.oObjectContext.Item('Request') This.oResponse=This.oObjectContext.Item('Response') This.oSession=This.oObjectContext.Item('Session')
oMTS='' oObjectContext='' oRequest='' oResponse='' oSession=''
Defining your login method
Defining your login method is probably the most interesting part as this is where you will establish your state if the login is successful. Once this is the case, you will make some calls to one of the ASP intrinsic object to store that information.
Our goal here is to have the Login() method to return .T. or .F. So, the user will use that response to see if the login was successful. Assuming it is .T., before returning that response to the user, we will just instantiate the state. So, the Login() method may look like this:
* Do the login for the specific user FUNCTION Login(tcUsername as String,tcPassword as string) AS Boolean LOCAL llLogin * Detect if the login is successfull llLogin=DetectLogin(tcUsername,tcPassword) * If successful, we instantiate the state IF llLogin This.GetASP() This.oResponse.Cookies('Username')=This.cUser ENDIF RETURN llLogin ENDFUNC
See also the reference I make to This.cUser. Basically, it's just a property that holds the information I need to store in the cookie. We assume the DetectLogin() method is responsible to store the required information in that property. We assume also that this property is defined at the top of your Web Service.
Obtaining the state
The rest is the fun part. Basically, for any other hit, we just have to retrieve the state by the use of that cookie. Lets say we have a method GetForum() which is responsible to return the forum list of the Universal Thread. It can go like this:
FUNCTION GetForum() as String LOCAL lcXML This.CheckLogin() * Login ok so do the rest * Get the forum list lcXML=Something() RETURN lcXML ENDFUNC * If the login is not done FUNCTION CheckLogin This.GetASP() This.cUser=This.oRequest.Cookies('Username').Item() IF LEN(This.cUser)=0 COMRETURNERROR('You have to login in order to use the Web Service.','') ENDIF ENDFUNC
SOAP 3 requirements
In order to have your Web Service to benefit of this, you need to have your users to use the SOAP 3 client as well. So, you might want to indicate that in your documentation to let them know. Otherwise, the instantiation of the cookie at the Login() method level will be done but will never be transferred on the wire. So, this means, you will end up with the CheckLogin() method to always return the message to the user.
At the time of this article, we were using the SOAP 3 Beta. This version requires removing the whsc30.dll file from the Program Files\Common Files\MSSoap\Binaries directory. With that file in place, the state will be lost. This is expected to be fixed for RTM.
How to build the Web Service?
If you build your Web Service under an EXE, the use of the ASP intrinsic objects will not work. Basically, you will not be able to obtain an object context, as this will bug your Web Service as soon as you try to execute those lines.
You have to compile your Web Service under a DLL. As long as the Web Service is running in a DLL, it will be created in the same process as ASP is running. Thus, this would make available the ASP context to your Web Service. You can now use any ASP object as if you would be in an ASP page for example. When building the Web Service in a DLL, you have the choice to select single-threaded or multi-threaded. You have to select single-threaded. If you select multi-threaded, your Web Service will be created in a different environment than the ASP process. So, you will loose the context.
Note that there is a small workaround if you really wish to use multi-threaded. When I first discovered that, it was because I wanted to automated the update of the DLL, when I was building the project, instead of having to execute the command IISRESET /RESTART to reset the IIS server. As, when this is running under a DLL, as soon as you invoke a hit, the DLL remains in memory. Thus, you cannot update it during that time. So, I wanted to automate the process and I used Randy Brown's project hook to instantiate the update of my COM object into the Components Service automatically. This works well. However, it requires your COM object to be multi-threaded. Thus, if you use the default ASP listener file, it will not work. You would have to create one of your own in order to create a context prior to have the SOAP server invoke your Web Service. The following ASP code can be used in such a situation for the ASP listener file replacement:
<% Set loContext=CreateObject("Web Service.WebService") Set loServer=Server.CreateObject("MSSOAP.SoapServer30") lcWSDL=Server.MapPath("UniversalThread.wsdl") lcWSML=Server.MapPath("UniversalThread.wsml") loServer.Init lcWSDL, lcWSML loServer.SoapInvokeEx Request,Response,loContext %>
This also assumes that the WSDL generator creates the Web Service by the use of the ASP listener. Otherwise, you will never obtain an ASP context.
Following another approach, you can also use single-threaded but instead of executing the command IISRESET /RESTART, you may use a dedicated virtual directory for your Web Service, define it as Isolated mode and just use the Unload option whenever you wish to compile your Web Service. Doing that will only shut down the related services from that directory and will not affect any other services under IIS.
Conclusion
I hope this article has helped you to obtain more ideas on how to offer such flexibility in your Web Service. Don't hesitate to share your comments about the manipulation of Web Services on the Universal Thread.