Cutting Edge

Using an Eval Function in Web Services

Dino Esposito

Code download available at:CuttingEdge0209.exe(37 KB)

Contents

Building on Web Services
Designing Web Services
Minimizing Round-trips
Web Stored Procedures
Implementation
Accessing the Web Service from Mobile Code
Security Considerations
Conclusion

Web Services are often presented as the perfect tool for pro-grammers. They're interoperable, based on open standards such as SOAP and WSDL, and are fully integrated with the Microsoft® .NET platform. However, what seems to be their major point of strength, the seamless integration within the .NET Framework, is also a looking glass into some of their limitations.

In this column, I'll tackle some of these limitations and discuss workarounds that you can employ to build more effective services. I'll examine main areas where limitations arise: cross-platform interoperability, the security model, and the programming interface. I'll reserve a discussion of real-world interoperability and integration with existing applications for a future column. Although I'll be touching on some security-related points, bear in mind that the Web Services security concerns cannot be fully resolved until the Web Services Interoperability (WS-I) specification is implemented. Aaron Skonnard provided the basics of the WS-I initiative in the June 2002 The XML Files column.

In a nutshell, the WS-I is a cross-vendor initiative that defines the official standard through which interacting heterogeneous applications can exchange security credentials effectively. Today you can find true interoperability for security and other areas, including data exchange, only within the .NET platform. However, if all of your code is limited to the .NET platform, it would be more advantageous to employ the .NET Remoting technology, which is tailored for .NET-to-.NET communications.

Building on Web Services

Does this mean the end of Web Services? Absolutely not! Web Services are a major milestone for software development and system integration, but they are just a beginning. Further advancements will fix the security and serialization engines and also set up guidelines to optimize the way in which clients use published methods.

For security and interoperability issues, you are better off waiting for a standardized approach because the complex problems involve many different vendors and distinct platforms. Structural improvements to the programming interface of Web Services will be easier to implement. Mobile code, which will allow software agents to transport code to specialized servers for long-time executions, is a concept that will prove to be very useful for Web Services. Although mobile code can solve many problems, it can also unleash underlying weaknesses in the security and the serialization model.

Designing Web Services

The similarity between calling Web Services and local classes is one of the key factors to the success of Web Services. Web Service calls, though, are normally resolved in a matter of seconds rather than milliseconds. Visual Studio® does a fantastic job of generating the proxy class that references the client methods and the programming interface of the remote Web Service on the fly. In Figure 1, you can see the reference map for a linked Web Service. Calling a method is as easy as calling a method on a local class. Unfortunately, though, the Web Service is not a marshal-by-value object and each call must travel over the network with its full bag of arguments and signature information, penetrate port 80, and then execute code on the server platform. Every method invocation inevitably requires a round-trip. Minimizing round-trips is a key step in the design of the Web Services programming interface. But what are good guidelines for this?

Figure 1 Reference Map

Figure 1** Reference Map **

Minimizing Round-trips

Since the round-trip is indissolubly tied to the request of an operation on the Web Service, the best way (and possibly the only way) to minimize round-trips is by merging together logically distinct functions. This can be done by using additional methods in the interface or additional parameters in the prototypes of some methods. Having extremely simple, succinct, and direct methods allows for a better overall design, but it certainly does not minimize round-trips since you need at least two round-trips to execute two functions.

On the other hand, incorporating more functionality in the body of a single and more complex method is effective in terms of performance, but not necessarily in terms of usability. Clients could receive more information than they actually need and pay a price in terms of download time. Moreover, clients could be forced to use an overly complex signature, risking getting the requested information by trial and error. The ultimate goal of minimizing round-trips would be lost.

The solution lies somewhere in the middle. You should avoid method signatures that are too simple or too complex. One of the guidelines for Microsoft Transaction Services (MTS) and COM+ applications has always been to write simple, stateless, and possibly parameter-less methods. However, for Web Services, you should start with complex methods that may also incorporate other functions with related parameters. There are a couple of factors you can control. First, verify the complexity of the server-side code that validates the parameters and then successfully executes the method. Second, make sure that consumers don't receive too much information that is not pertinent to the original request. And, more importantly, make sure that this unrelated and unrequired information is not the most relevant part of the response. Finally, don't forget that the more complex the programming interface is, the more you need effective and well-designed documentation.

Web Stored Procedures

In databases, a stored procedure is a way to pack a complex, multistep procedure onto the server. Authorized users can define new procedures, thus enriching the database with new functionality.

However, this method won't work with Web Services. Adding new methods to the Web Service would alter its WSDL script, thus breaking the alignment between the content of the WSDL and the actual interface. An alternate method is to define Web stored procedures as files on the server, and use a common and invariant interface to call them. This can be done by resorting to a weakly typed approach similar to invoking methods using an object's reflection. Figure 2 illustrates the role of the Web stored procedures. Of course, these procedures would be able to access methods directly through the regular Web Service interface.

Figure 2 The Role of Web Stored Procs

Figure 2** The Role of Web Stored Procs **

Web stored procedures are only one possible way of proceeding and, more importantly, they are the static way to go. A dynamic approach would entail passing the code to execute in the body of a single call, as shown in this pseudo-code:

proxy.Eval('string text="Hello, world";return text;');

In this case, the code is mobile in the sense that it travels from the client to the remote server where it gets compiled on the fly, loaded into memory, executed, and then discarded. Just as with database stored procedures, you could arrange things in such a way that the compiled version is persisted on the server and reused more quickly after the first call.

How do you write these Web stored procedures? You can take advantage of C# or use any language for which the .NET Framework provides a compiler class in the System.CodeDom.Compiler namespace. Classes in this namespace provide you with the programmatic counterpart of the C# and Visual Basic® .NET compilers. You can use them to compile in-memory code, adding references and importing namespaces. Once created, the library assembly is loaded and its methods are called using reflection. I'll provide a sample implementation of this technique shortly. In the meantime, here's the interface of the method that provides for dynamic code execution:

<WebMethod()> _ Public Function Eval(ByVal csCode As String) As Object

The method Eval, marked as WebMethod, takes a string representing C# code and returns any type boxed in an Object object.

Implementation

Now let's discuss the internal implementation of the Eval method in more depth. Compilers are a constituent part of the .NET Framework, although the shipping version of the .NET Framework includes only a few languages, including C# and Visual Basic .NET. To compile code on the fly, you start by creating a new instance of your compiler of choice. Compilers are buried in the System.CodeDom.Compiler.CodeDomProvider namespace. A CodeDOM provider is an object that can be used to instantiate two categories of components: code generators and code compilers. Code generators are similar to XML writers; both allow you to output syntactically correct text using ad hoc methods. In particular, the signature of these methods focuses on the exposed functionality rather than the text to write out in a given language. Just as XML writers expose WriteXXX methods to create nodes (for example, WriteStartElement to write out the open tag of an XML node), code generators make available a handful of GenerateXXX methods to generate pieces of source code. For example, GenerateMethod generates code for the specified method.

Code compilers represent a very special case of code generators as they generate correct Common Intermediate Language (CIL) code based on a C# or Visual Basic .NET source. Furthermore, the compiler classes expose a number of extra parameters such as references, compiler options, and arguments. All settings that you can communicate to a compiler through the command line, or the Visual Studio .NET user interface, can be programmatically set when you use a compiler class. The available compiler classes are CSharpCodeProvider, JscriptCodeProvider, and, VBCodeProvider. The following code shows how to create an in-memory instance of the C# compiler:

Dim c As CSharpCodeProvider = New CSharpCodeProvider() Dim icc As ICodeCompiler = c.CreateCompiler()

At this point, you set a few options and then generate the final assembly on disk as well as in memory. In Figure 3 you can see the implementation of the Eval method in the sample Web Service. To set compiler options, you use the CompilerParameters class:

Dim cp As CompilerParameters = New CompilerParameters() cp.CompilerOptions = "/t:library" cp.GenerateInMemory = True

At this time, you must also add as many assembly references as required by the source code you plan to compile. For example:

cp.ReferencedAssemblies.Add("system.dll")

Figure 3 Implementing Eval

<WebMethod()> _ Public Function Eval(ByVal csCode As String) As Object Dim c As CSharpCodeProvider = New CSharpCodeProvider() Dim icc As ICodeCompiler = c.CreateCompiler() ' Set some compiler options Dim cp As CompilerParameters = New CompilerParameters() cp.ReferencedAssemblies.Add("system.dll") cp.ReferencedAssemblies.Add("system.xml.dll") cp.ReferencedAssemblies.Add("system.data.dll") cp.ReferencedAssemblies.Add("SomeBaseService.dll") cp.CompilerOptions = "/t:library" cp.GenerateInMemory = True ' Complete the user code so that it can be compiled Dim sb As StringBuilder = New StringBuilder("") sb.Append("using System;") sb.Append("using System.IO;") sb.Append("using MSDNMag;") sb.Append("using System.Xml;") sb.Append("using System.Data;") sb.Append("using System.Data.SqlClient;") sb.Append("namespace MSDNMag { class MSDNMagLib {") sb.Append("public static object EvalCode() {") sb.Append("MSDNMag.SomeBaseService thisObject = new") sb.Append("MSDNMag.SomeBaseService();") sb.Append(csCode) sb.Append("}}}") ' Compile the code into an assembly Dim cr As CompilerResults = icc.CompileAssemblyFromSource(cp, _ sb.ToString()) Dim a As System.Reflection.Assembly = cr.CompiledAssembly ' Load the assembly and execute the code Dim o As Object Dim mi As MethodInfo ' Execute the code o = a.CreateInstance("MSDNMag.MSDNMagLib") Dim t As Type = o.GetType() mi = t.GetMethod("EvalCode") Dim s As Object = mi.Invoke(o, Nothing) ' Assume that the code has a RETURN statement Return s End Function

For non-Global Assembly Cache (GAC) assemblies, you may need to indicate a relative or an absolute path as well. The ReferencedAssemblies member is an array of strings. Compilation occurs when you call the CompileAssemblyFromSource method on the compiler object. The method takes the CompilerOptions structure and the C# source code as its input and compiles the assembly as requested. By using a different method, you can compile from a disk file, too.

At this point, the assembly is not yet loaded into memory. You get a reference to the compiled assembly object as shown here:

Dim a As System.Reflection.Assembly = cr.CompiledAssembly

The code you pass to the compiler must have a valid format. In other words, it must contain a class definition or any other programming construct that is acceptable to the compiler. I wrote sample code to automatically add a namespace and the main class definition so that you only need to write the code you want to execute. Say your mobile code takes the following form:

string cs = "Hello, world!"; return cs;

The modified and extended code that is actually processed by the compiler looks like this:

namespace MSDNMag { class MSDNMagLib { public static object EvalCode() { // user-defined code goes here string cs = "Hello, world!"; return cs; } } }

All the names (namespace, class, and method) are totally arbitrary. In general, you should call .NET Framework classes with their fully qualified name; for example, use System.Data.DataSet rather than just DataSet. By default, the sample code adds a few standard using statements to make it possible for developers to shorten some class names. Bear in mind, though, that to be able to use the methods of a class, your assembly should include a reference to the class assembly. Admittedly, the sample implementation does not provide enough flexibility on this point since all the references and the namespace importations are hardcoded. I plan to cover this in a future column that will describe an effective scripting system built around C# and Visual Basic .NET.

Now that you have a compiled assembly, you need to figure out a way to invoke the method and grab the results. The assembly's CreateInstance method creates a living instance of the specified class from the given assembly. You use reflection techniques to locate and invoke a particular method. As I mentioned earlier, MSDNMag.MSDNMagLib is the standard class that wraps the user-defined code and EvalCode is the unique static method exposed by this volatile class.

Dim o As Object Dim mi As MethodInfo o = a.CreateInstance("MSDNMag.MSDNMagLib") Dim t As Type = o.GetType() mi = t.GetMethod("EvalCode") Dim s As Object = mi.Invoke(o, Nothing)

The Type's GetMethod method returns the MethodInfo object, which describes EvalCode. You can call the Invoke method with an array of objects indicating the needed arguments. In this case, there is no argument to pass.

Notice that the sample implementation assumes that the user-defined code ends with a return statement; otherwise you will get an exception. This stems from the definition of the EvalCode, which returns an object and fails if there are no return values.

In summary, to endow a Web Service with the ability to execute C# mobile code, start by defining a method such as Eval and add the following namespaces to the ASMX source code:

Imports Microsoft.CSharp Imports System.CodeDom.Compiler Imports System.Reflection

If you plan to make the Web Service execute code in Visual Basic instead, then replace the Microsoft.CSharp namespace with Microsoft.VisualBasic.

Accessing the Web Service from Mobile Code

Being able to compile and execute dynamically defined code is only the first step. The real added value of this solution is the reduction in the number of round-trips to obtain the needed data. If the Web Service programming interface already provides a method that returns data the way you want it, you have no need to call the Eval method. By contrast, if the Web Service author feels that the exposed API can satisfy any reasonable request, then they might decide to avoid exposing a generic Eval method to let users shape up the call they need.

Making an Eval method available would be rather useless if it's not accompanied by a convenient way to call into the other Web Service methods. This is much like discussing what the programming power of stored procedures would really be if SQL Server™ didn't provide access to T-SQL!

Figure 4 Assembly Separation

Figure 4** Assembly Separation **

To be precise, first you define the official programming interface of the Web Service and then decide whether you need to add an extra Eval method to let users combine the output of other methods. Unfortunately, though, the code executed by Eval is compiled in an in-memory assembly different from the Web Service itself. This means that there is no way to call directly into Web Service methods. The separation between the Web Service functions space and the in-memory assembly space is total (as shown in Figure 4). To work around this issue, you must recognize that Eval is just an extra method that you add on top of the official Web Service interface. You can group all the methods besides Eval into a worker class and make the Web Service inherit from that:

<%@ WebService Language="VB" Class="SomeService" %> <%@ Assembly Name="SomeBaseService" %> ••• <WebService(Namespace:="cutting edge")> _ Public Class SomeService Inherits MSDNMag.SomeBaseService ••• End Class

The SomeBaseService class provides the core implementation of the Web Service methods compiled in a distinct assembly. At this point, these functions become easily accessible from the Eval's dynamic assembly tool. Figure 5 shows the sample implementation of the base service class. The EvalCode internal method includes one more line of code that instantiates a variable of type SomeBaseService:

namespace MSDNMag { class MSDNMagLib { public static object EvalCode() { MSDNMag.SomeBaseService thisObject; thisObject = new MSDNMag.SomeBaseService(); // user-defined code goes here string cs = "Hello, world!"; return cs; } } }

Figure 5 Base Class Implementation

Imports System Imports System.Web.Services Namespace MSDNMag Public Class SomeBaseService Public Sub New() End Sub Public Overridable Function ExecuteMethod() As String Return "I executed some method." End Function Public Overridable Function ExecuteAnotherMethod() As String Return "And now I'm just ready to execute another one." End Function End Class End Namespace

The thisObject keyword is the key to accessing the native Web Service functions. It allows you to access the public methods of the local instance of the class which is actually a Web Service. In this way Eval can access all the public methods and not just those marked with the WebMethod attribute. The simplest way to solve this problem is to avoid public methods in the base service class not marked as WebMethod (see Figure 6).

Figure 6 Accessing Native Methods with Eval

Figure 6** Accessing Native Methods with Eval **

Bear in mind that core functions that happen to be implemented in a separate assembly are not automatically exposed as Web methods. To make up for this, you might want to override any public method in the base Web Service class and give each the key WebMethod attribute. The overriding method will have a very simple implementation that refers to the base method:

<WebMethod()> _ Public Overrides Function ExecuteMethod() As String Return MyBase.ExecuteMethod() End Function

Using the Eval method is quite simple. Passing the following code as a string to the Eval method:

string buf = thisObject.ExecuteMethod(); return buf + " " + thisObject.ExecuteAnotherMethod();

produces the output shown here:

<?xml version=:"1.0" encoding="utf-8" ?> <anyType xmlns:q1="https://wwww.w3.org/2001/XMLSchema" d1p1:type="q1:string" xmlns:d1p1="https://www.w3.org/2001/XMLSchema-instance" xmlns="cutting edge">I executed some method. And now I'm just ready to execute another one.</anyType>

Looking at Figure 5, you can see that the code I just showed you makes Eval return the combined output of a couple of Web Service methods to the caller. More importantly, all this takes place in a single step and in a single round-trip.

Security Considerations

Many aspects of what I've discussed thus far can be modified and improved, and often just generalized. However, even from such a simple prototype it's easy to see some possible security concerns. If you are free to define the code to be run on the server, then you are capable of doing everything on the server, but that would pose an unacceptable risk. Well, almost. Web Services are a kind of ASP.NET application and the same security restrictions apply to them that apply to all ASP.NET applications. For example, there is no write access to the file system or the registry unless you explicitly allow it. You will get an exception of type UnauthorizedAccessException whenever you try to create or to modify files through code.

In addition, you can control how the dynamically created assembly is loaded in memory and set the security evidence for it. To do this, use one of the overloads of the Assembly.Load method rather than the assembly's CreateInstance method. Assembly evidence is the set of information that the Common Language Runtime (CLR) utilizes to make decisions about what permissions can be granted to that code. Finally, bear in mind that as the Web Service author, you also control the set of assembly references. If you don't want callers to use classes in a given assembly, don't reference that assembly and an exception will be thrown.

Conclusion

Web Services are an incredibly powerful architecture to provide for cross-platform interoperability and even to isolate valuable functions behind a secured machine. One of the biggest drawbacks of Web Services is that each call needs a round-trip to the server. Although you can limit the impact by optimizing the pub-lic API, more structurally effective techniques to group function calls are needed.

This column was inspired by the concept of mobile code and software agents—an architecture that allows for code transportation and in-place execution. In general, mobile agents are autonomous and intelligent programs that travel through the Internet interacting with existing services on the user's behalf. Agents do not send remote procedure calls, but physically move themselves onto a target platform to interact with local software. Web Services are not mobile agents, but can easily become the target of a .NET-specific mobile agent.

I can't offer you the ultimate solution for improving Web Services performance, but I feel confident that by using some of the ideas I've discussed, you can develop more effective Web Services.

Send questions and comments for Dino to cutting@microsoft.com.

Dino Espositois an instructor and consultant based in Rome, Italy. He is the author of Building Web Solutions with ASP.NET and ADO.NET and the upcoming Applied XML Programming for .NET, both from Microsoft Press. Reach Dino at dinoe@wintellect.com.