COM+ Technical Articles
Developing a Visual Basic Component for IIS/MTS
 

Troy Cambra
Support Engineer
Microsoft Corporation

Updated September 2001

Summary: This article describes how to build components that take advantage of the new features in IIS and MTS, and provides an extensive list of tips on building components, performance and process isolation, and debugging. (19 printed pages)

Contents
Introduction
Why Write Components?
Why Use Microsoft Transaction Server?
Why Use Visual Basic?
Using the MTS Objects
Using the ASP Intrinsic Objects
Performance and Process Isolation Tips
Tips for Building MTS Components
Debugging Tips

Introduction

The integration of Microsoft® Internet Information Server (IIS) 4.0 with Microsoft Transaction Server (MTS) 2.0 yields a powerful server environment. The integration provides process isolation, transactional Web page support, and a rich framework for building components into IIS. This article describes how to build components that take advantage of the new features in IIS and MTS, and provides an extensive list of tips on building components, performance and process isolation, and debugging.

This article assumes that you have a solid working knowledge of Internet Information Server, Active Server Pages (ASP), Microsoft Transaction Server, Microsoft Visual Basic® 6.0 and the Component Object Model (COM). You can install IIS 4.0 and MTS 2.0 from the Microsoft Windows NT® Option Pack, which is available from http://www.microsoft.com/windows/downloads/default.asp.

Note   Currently, Internet Information Services version 5.0 is available with Microsoft Windows® 2000. Microsoft Transaction Server has merged with COM technologies to become COM+ Services, and is also available with Windows 2000.

Why Write Components?

With ASP, it is entirely possible to build rich applications using only the features provided in IIS. You can access server context, perform transactions, access databases, and so forth, without having to write your own components. So why, you might ask, would you want to write components? The three most compelling reasons are:

Performance and scalability. Executing native code is faster and typically less memory-intensive than interpreting scripts.

Business logic encapsulation. This creates better code organization, and also improves code debugging, distribution, and upgrading. In addition, you can reuse components within and across your applications.

Separation of data and user interface (UI). Components make it easy to add a fresh UI to applications without having to touch business logic and data. This is important in a fast-changing Web-based application.

For more information on components, see Agility in Server Components.

Back to top

Why Use Microsoft Transaction Server?

If you are using distributed transactions on your site, MTS is the obvious choice. However, even if you don't use transactions or databases, you will find that MTS is a very useful environment in which to run your components. MTS provides object brokering and run-time services as well as transaction monitoring. Consider MTS if your components require any of the following:

  • Transactional support
  • Fault tolerance
  • Process isolation
  • Manageable component and interface security
  • Access to the ASP intrinsic objects
  • Thread management
  • Auditing
  • Resource pooling

TopBack to top

Why Use Visual Basic?

Any language capable of creating apartment-threaded in-process COM components is suitable for developing MTS components. The choice of language should be primarily based on your familiarity with it and its suitability for the task. Familiarity with a language is the most important factor. Knowing how to use a language correctly and to its greatest efficiency and power can determine, in large part, the success of a project. However, suitability for the task also needs to be considered. For example, if raw speed were of utmost importance, a lower-level language such as C++ would be the better language to use.

Visual Basic is a solid tool for most applications. It is mature, stable, and proven. It is a sound rapid application development (RAD) tool, but has enough flexibility to get down to the underlying APIs and enable the developer to tweak performance or provide advanced features.

TopBack to top

Using the MTS Objects

Any class in an ActiveX DLL project can potentially be an MTS component. However, to take full advantage of the MTS features, it is necessary to use the MTS programmer's interface. The first step in doing this is to reference the Microsoft Transaction Server type library. This enables access to the MTS objects, the most important of which is ObjectContext. ObjectContext provides access to your object's MTS context, which provides the core MTS functionality.

In MTS, a logical thread of work is called an activity. An activity can span multiple objects and multiple method calls to any object. An activity's scope spans from the first call from the base client until either the root object calls ObjectContext.SetComplete and exits the method or the base client releases the root object. Although it is possible to have activities that span multiple objects or method calls on an object, it is best to limit the activity to a single method call per object if possible. If this is done, you obtain context in the method and release it before exiting. The code would look something like this:

Public Sub SomeMethod()

    Dim objCtx as ObjectContext

    Set objCtx = GetObjectContext
    If objCtx Is Nothing Then _
        Err.Raise 91 'error if failed

    'Perform work.

    Set objCtx = Nothing

End Sub
	  

If the activity will span several method calls and the calls all require access to the object's context, it is generally a good idea to store the object's context in a class member variable. It is bad form to obtain and release the object's context in the Initiate and Terminate events. These events should be considered the constructor and destructor of the class, so context should not be assumed to be available. This is not always the case for Visual Basic, but should be followed for consistency with all other languages, for better error handling and reliability.

The best way to obtain and release context when an activity will span several method calls is to use the ObjectControl interface methods. ObjectControl provides a solid mechanism for context handling, and also allows the component to take advantage of object pooling when it becomes available in future releases.

Code using the ObjectControl interface methods to handle context in the class module might look something like this:

Option Explicit
'Private class member variables
Private m_objCtx As ObjectContext

'Public class functions
'Implementation of ObjectControl interface
Implements ObjectControl

Private Sub ObjectControl_Activate()
    Set m_objCtx = GetObjectContext
    If m_objCtx Is Nothing Then _
        Err.Raise 91 'error if failed
End Sub

Private Function ObjectControl_CanBePooled() As Boolean
    ObjectControl_CanBePooled = False  'don't pool for now
End Function

Private Sub ObjectControl_Deactivate()
    Set m_objCtx = Nothing 'release
End Sub

'Actual public class functions
'TBD
	  

Once the context is obtained, you can use it to control transaction outcome and object recycling, to query context properties, and to perform various functions. The following function shows a typical use of the context properties and methods:

	  Public Function BasicFunction(ByVal blnAudit As Boolean) As Variant

	      'sample MTS function
	      Dim strAuditString As String
	      Dim strErrString As String

	      'This function will optionally log auditing information
	      On Error GoTo ErrHand
	      If blnAudit Then
	          strAuditString = "Method: BasicFunction " & vbCrLf & _
	              "Thread ID: 0x" & Hex(App.ThreadID) & vbCrLf & _
	              "Transactional: " & m_objCtx.IsInTransaction & vbCrLf & _
	              "DirectCaller: " & m_objCtx.Security.GetDirectCallerName & vbCrLf & _
	              "OriginalCaller: " & m_objCtx.Security.GetOriginalCallerName
	          Call App.LogEvent(strAuditString, vbLogEventTypeInformation)
	      End If

	      'Perform the actual work
	      'Actual work TBD

	      'Finish up
	      BasicFunction = "Something Wonderful Happened"
	      'Commit the transaction
	      m_objCtx.SetComplete
	      Exit Function

	  ErrHand:
	      'log the actual error
	      strErrString = "Error Code: 0x" & Hex(Err.Number) & vbCrLf & _
	          "Error Description: " & Err.Description & vbCrLf & _
	          "Error Source: " & Err.Source
	      On Error Resume Next
	      Call App.LogEvent(strErrString, vbLogEventTypeError)
	      'Abort the transaction - if one
	      m_objCtx.SetAbort
	      'Raise a friendly error to the caller. Minimizing the number of
	      'errors returned to the client makes programming at that level
	      'easier. For example, you typically have two types of errors
	      'recoverable and non recoverable. You could simply send back
	      'one or the other so the caller can easily make a decision
	      'about how to handle it.
	      Call Err.Raise(vbObjectError + 1001, _
	          "Basic Component", _
	          "Basic Error. Please contact the system administrator")

	  End Function

	  	  

 

TopBack to top

Using the ASP Intrinsic Objects

The ASP intrinsic objects are available within MTS objects through the object's context. Making use of the ASP intrinsic objects from within an MTS object is simple and can be quite useful. For example, if you have an ASP page that is building a complicated HTML page using the Response object, you can encapsulate the code in a component that will execute much faster, and be much easier to debug. An e-commerce site might use this method to build a page that will report progress as a complicated transaction is being performed. Here is a sample function from a component and a sample ASP script that calls it:

The Component

	  Public Function ASPVarUse() As Variant

	      'sample mts function
	      Dim strErrString As String
	      Dim objApplication As Object
	      Dim objResponse As Object
	      Dim objRequest As Object
	      Dim objServer As Object
	      Dim objSession As Object
	      Dim objItem As Object

	      On Error GoTo ErrHand

	      'Get the IIS intrinsic objects
	      If m_objCtx.Count < 5 Then _
	          Call Err.Raise(vbObjectError + 1002, "Basic Function", _
	          "This object is meant to be called by ASP pages only")

	      Set objApplication = m_objCtx.Item("Application")
	      Set objResponse = m_objCtx.Item("Response")
	      Set objRequest = m_objCtx.Item("Request")
	      Set objServer = m_objCtx.Item("Server")
	      Set objSession = m_objCtx.Item("Session")

	      'Access the ASP objects
	      objResponse.Write "<p> Outputting from the MTS component </p>"
	      Call objServer.HTMLEncode("The paragraph tag: <P>")
	      objApplication("AppTest") = "Application Test"
	      objSession("SessionTest") = "Session Test"
	      For Each objItem In objRequest.QueryString("RequestTest")
	          objResponse.Write objItem & "<BR>"
	      Next

	      ASPVarUse = "Successful Test"
	      'Commit the transaction
	      m_objCtx.SetComplete
	      Exit Function

	  ErrHand:
	      'log the actual error
	      strErrString = "Error Code: 0x" & Hex(Err.Number) & vbCrLf & _
	          "Error Description: " & Err.Description & vbCrLf & _
	          "Error Source: " & Err.Source
	      On Error Resume Next
	      Call App.LogEvent(strErrString, vbLogEventTypeError)
	      'Abort the transaction - if one
	      m_objCtx.SetAbort
		
	      On Error GoTo 0
 
		  'Raise a friendly error to the caller.
	      Call Err.Raise(vbObjectError + 1001, _
	          "Basic Component", _
	          "Basic Error. Please contact the system administrator, error: " & strErrString)

	  End Function

	  

The ASP Script

	  <%
	  Dim objTest

	  On Error Resume Next
	  'Create the MTS object
	  Set objTest = Server.CreateObject("PMTSIISVB.IMTSIISVB")
	  If Err.Number <> 0 Then
	      Response.Clear
	      Response.Write "<p> " & Err.Description & " </p>"
	      Response.End
	  End If

	  Response.Write "<p> Starting Test </p>"
	  Response.Write "<p> ASPVarUse Return: " & objTest.ASPVarUse() & " </p>"
	  If Err.Number <> 0 Then
	      Response.Clear
	      Response.Write "<p> " & Err.Description & " </p>"
	      Response.End
	  End If
	  Response.Write "<p> Session Variable = " & Session("SessionTest") & " </p>"
	  Response.Write "<p> Application Variable = " & Application("AppTest") & " </p>"
	  Response.Write "<p> Ending Test </p>"
	  %>
	  	  

TopBack to top

Performance and Process Isolation Tips

Balancing fault tolerance, programming ease, and performance can be tricky. There are countless things that can be done to fine-tune performance. I've put together a list of some of the more common things you can do or avoid doing to improve performance. For a more detailed discussion of IIS and ASP performance tuning, please see the Internet Information Server Resource Kit.

  • Use pooled-process ASP pages and components as much as possible. If the IIS server is a single-use server (your application is the only one) and you are confident about the robustness of your ASP pages and MTS components, it is acceptable to run them all in the IIS process. Caution: A problem in an ASP page or component can bring down the whole server when run in the IIS process. In IIS versions 5.0 and greater, a new method of process isolation is present called Pooled Out of Process. Applications marked Pooled do not share space in the IIS process, but they are not in their own process either. All pooled applications share one process, which not only protects the IIS process, but it also improves performance.
It is important to fully analyze the entire server. You might think that running every ASP page in every application or virtual Web site in-process or in the pooled process would create better performance than running some or all out-of-process. This is not always true. There are literally thousands of variables in the performance equation. The only way to be sure of optimum performance is to plan performance testing into your application development cycle. For example, I have had some larger Web sites actually see a substantial performance increase by moving some applications out-of-process.

If process isolation between the application and the IIS server is important, run the IIS application in a separate process, but use a library package for the MTS components. This is typically a good tradeoff. A problem in a component could bring down the application, but even if they were in two different processes, the problem would probably propagate back to the ASP in some form anyway. Further, this configuration will typically outperform running the ASP in the IIS process and the MTS components in a server process.

  • Watch those object references. Passing object pointers around may make programming easier, but can kill performance. This is especially true over network links, but cross-process references can be quite a performance killer as well. Recordsets are objects that folks commonly want to pass around. If there is a need to pass recordsets -- especially to clients -- use the Client batch recordset that is part of the ActiveX Data Objects (ADO) and the Remote Data Service (RDS). This disconnected recordset can be marshaled by value, and it can also be marshaled via HTTP.

  • Watch property Get and Set calls. It is typically better to use a single method call to get a group of related properties rather than individual property Get and Set calls. Each call requires a round-trip, so minimizing calls minimizes round-trips, which maximizes performance. In other words, objects with a small number of methods that have many arguments will typically outperform objects that have many properties and methods with few arguments.

  • Avoid delegation. The Visual Basic Implements keyword enables the programmer to delegate calls to achieve a simple form of inheritance. While this is good for code reuse, take care not to overuse it. It can quickly degrade performance -- especially in a stateless environment such as MTS where objects are constantly created and released.

  • Avoid run-time catalog queries in transactional components. System tables are typically small and run-time queries can cause serious database blocking. This can be obvious or obscure. For example, it is easy to avoid building SQL statements that access system tables, but it's not easy to know when some flag in a database API or object causes a method to access system tables. Check ODBC and database logs and traces for signs of this behavior before deploying the application.

  • Be careful with your shared data. The Shared Property Access Manager, the Session object, and the Application object can all hold shared data. However, never store apartment-threaded objects in any of them and take care not to overuse them when storing other data. The interface marshaling and thread synchronization (respectively) can be a performance hit.

  • Avoid accessing non-tuned COM servers. For example, I have seen MTS applications that call Microsoft Word to format data before storing it, and so forth. Word and other such non-tuned COM servers were not developed with this use in mind, so performance is typically less than optimal.

  • Avoid storing state. There are a few cases where storing state can be beneficial, but most of the time storing state hurts scalability and leads to other problems.

  • Do not call directly off-machine from ASP pages. Knowledge Base article Q159311, which is available from http://support.microsoft.com, discusses using an intermediate MTS package that exists on the IIS machine and in turn calls the remote server. The actual reason for this is due to security considerations, and when I first heard it I thought, "What a performance nightmare." However, after I thought about it and tried a few sample tests, I realized that you can use this to improve performance.
ASP pages are going to use the IDispatch interface of the component. This causes two calls per method call. If you use early binding in your intermediate component, you can limit the network calls to one call per method. Further, you can design your intermediate component to hold references, cache data, and so forth. This can minimize the amount and frequency of data accessed over the network.
  • Minimize transaction duration in transactional components. MTS starts a Microsoft Distributed Transaction Coordinator (DTC) transaction for the object on instantiation of the first transactional component. This transaction is then flowed throughout the activity. Although it is instantiated, the resource manager transaction does not begin until the first attempt to access something in it. Therefore, try to begin the transaction as late as possible, delay actual resource manager access as long as possible, and end the transaction as soon as possible.

TopBack to top

Tips for Building MTS Components

Here is a list of suggestions for building MTS components for use in IIS. This is not a set of rules, but flexible guidelines.

  • Try to SetComplete/SetAbort in every method call. Stateless is the way to go in MTS. This is not to say that you should not have multiple objects as part of an activity. It simply means that you should try to limit calls to any given object in the transaction. Activities that span multiple methods can get into deadlock situations as use increases. Further, this simplifies the execution path for performance and also helps to eliminate possible failure points (especially for remote components).

  • Always check Retain in Memory and Unattended Execution. Without those flags, the VB runtime will be unloaded and reloaded continuously. This is not thread safe. You will definitely run into hangs and crashes.
  • Perform sample tests to compare methodologies, components, and tools before starting. This will give you an idea of performance and problems early on. For example, using the ODBC API will result in very fast code, but is it worth the added development cost or does some careful ADO code with a little extra hardware make more sense? The MTS performance team is starting to release performance data, but a simple prototype of your application can provide you with invaluable information.
  • Consider the future. How far will the application need to scale? Will it need to become more distributed? Will you need location transparency? These types of questions influence development. For example, if the application does not need to scale very far and will always run in-process, passing object references and performing many property Gets and Sets may not be a problem. If your application needs to change, performance and reliability could suffer with such a design.
  • Test with at least as many users (simulated) as you plan on deploying to and run on similar hardware. Test in this manner throughout the development cycle. All too often an application is developed and tested in single-user mode or perhaps with a couple of clients. The application is then tested under heavy load -- or worse, rolled out to many users -- and fails miserably.
  • Use a consistent and universal error-handling mechanism. Error handling is often an afterthought. This will not usually cause problems in a desktop application, but in a mission-critical multi-tiered distributed application, error handling that's not carefully planned and tested can result in serious problems. Error handling techniques will likely be covered in a subsequent paper.
  • Be wary of third-party controls and object libraries. Although they may provide some cool feature or core logic, many have not yet been tested with MTS and others have never been tested in a high-stress server environment. Be sure that the vendor has done some testing in this area and/or will be responsive when bugs are found.
  • Avoid callbacks. Having the server call the client back is typically a problem waiting to happen. Further, the MTS programming model does not support callbacks very well. Callbacks may be explicitly done in Visual Basic code by passing object references to the server or implicitly using the WithEvents keyword. The WithEvents keyword uses connection points and therefore keeps callbacks under the hood.
  • Use the RDS components and ADO ClientBatch disconnected Recordset objects. You can do this to get rowsets to clients and even to call the business objects directly via HTTP. Combined with IIS, MTS, and ASP, these technologies enable you to build Web applications that are every bit as full featured and robust as a traditional client/server solution, without sacrificing any of the Web application's advantages.
  • Do not store MTS components in session or application variables. MTS treats all components as apartment-threaded, so storing them in session or application variables will cause performance and scalability problems. See the Scripter's Reference in the IIS product documentation for more information.
  • Do not use CreateObject() in ASP scripts to create MTS objects. Use Server.CreateObject(). Server.CreateObject() flows context and provides improved security checks. Use CreateObject only if you fully understand the ramifications of doing so and really need to have the MTS component run in a different activity. See the Scripter's Reference in the IIS product documentation for more information.
  • Pass arguments ByValue whenever possible. It is more efficient than ByRef. Only use ByRef if you need to return data to the client. Make sure that all method arguments that return data to the script (by reference) are of type Variant. Visual Basic® Scripting Edition (VBScript) cannot use any other type for arguments that return data.
  • Be cautious when using the New keyword. When used to create objects that exist in the same project, the New keyword will not use a COM creation method. Therefore, objects created using New will not be MTS objects even though they are installed in MTS.
  • Be sure to use binary compatibility. Also, set the threading model to Apartment for the Visual Basic project.
  • Do not develop the server components on a client machine. Developing on the client sounds convenient at first, but after going through the "compile the component, unregister component on the client, copy to server, refresh component in MTS, re-export the package to generate the client export executable, run the client export executable on the client, and then run the client" cycle a few times and sorting out all the problems along the way, you'll want to develop on the server.
  • Avoid implementing security in component code. For example, it is possible to implement method-level security using IsCallerInRole. However, this means that security administration is not completely independent of component implementation. Administrative changes such as the changing of role names can necessitate component code changes.
  • Always compile with Symbolic Debug Information turned on. Without it, there is no way you can get useful information from a Dr. Watson dump. Scenarios like 100 percent CPU, process hang (unresponsive process), and process crashes can only be solved when symbolic debug information is available.

TopBack to top

Debugging Tips

Visual Basic has a strong interpreted debugger in the integrated development environment (IDE). However, since Visual Basic 6.0, it is possible and very easy to debug your components directly in the IDE. So there is no longer the need to debug our components with Visual C++ or WinDBG.

Any language that uses a large run-time or virtual machine (such as Visual Basic or Java) can make tracking bugs more difficult, because there's a lot going on below the level at which you are working. Further, the use of libraries that wrap underlying APIs such as ActiveX Data Objects (ADO), Remote Data Objects (RDO), and Oracle Power Objects can further complicate things. It would be nice to think that they are bug-free, but the reality is that they are not -- and finding those bugs without source code and symbols is very difficult. Finally, the run-times and wrappers can do things that are programmatically convenient but cause performance or functionality problems in the MTS environment. One such example is the querying of the database system tables that many of the data-access objects perform. This can make programming easier, but can also cause serious blocking in the database.

The following are prerequisites in order to debug VB components under MTS.

  • Requires Visual Basic 6.0 or later.

  • Requires Windows NT 4.0 Service Pack 4 (SP4) or later. MTS debugging is not supported under Windows 95 or Windows 98.
  • The Visual Basic Class that you are debugging should have its MTSTransactionMode property set to anything other than 0—NotAnMTSObject.
  • Must compile (build the DLL) and set binary compatibility on the project.

The following steps explain how to create and debug a simple Visual Basic MTS component.

  1. Open a new Visual Basic ActiveX DLL project.
  2. Rename Project Name to prjMTSDebug and Class1 to clsMTSDebug.
  3. Set the Transaction property of clsMTSDebug to 1. No Transactions.
  4. Add the following code to clsMTSDebug:.
    Public Function Sum( Val1 As Integer, Val2 As Integer) As Integer
    	Sum = Val1 + Val2
    End Function 
  5. Compile the DLL.
  6. Set the binary compatibility for the project. (While you're at it, also set Retain in Memory and Unattended Execution and check the Create Symbolic Information box.)
  7. Create a new MTS package called MTSDebug.
  8. Add the DLL to the package.
  9. Press F5 in the VB IDE to run the project. Accept the default settings, and click OK.
  10. Create and add this ASP to one of your virtual directories.
           Dim Obj
           Set Obj = Server.CreateObject("prjMTSDebug.clsMTSDebug")
           Response.Write Obj.Sum(2,3)
           Set Obj = Nothing 
  11. Put a breakpoint in Sum function (in Visual Basic IDE).
  12. Run the ASP page from a browser. It breaks at the breakpoint.

Always check the event log. MTS, IIS, and ASP log many error conditions. It can often be quite helpful in tracking down many types of errors. (See the Knowledge Base article Q262187, INFO: Interpret the Microsoft Transaction Server Events in the Event Log.)

Sometimes there are bugs that the above simply does not catch. Tracking down hangs and performance problems is always fun. There are two things to look at first when doing so: CPU utilization and database blocking. If CPU utilization is normal on both the MTS server and the database server, blocking in the database is often the cause. Finding the underlying cause typically requires looking at the database and ODBC logs and traces. If the CPU utilization is peaking on one machine, start looking there for bottlenecks. There is no simple solution to this problem. You have to look at everything: it could be disk utilization on the database server, an indication that the objects are too large, simply an underestimate of necessary server hardware, and so forth—all a little beyond the scope of this paper.

Tracking down sporadic exceptions is yet another time-consuming and less than enjoyable experience. The MTS, ASP, or COM environments typically handle exceptions by trapping the error, logging the event, and cleaning up as necessary. It is possible to catch these by running the application with a debugger attached. Although you can debug these, you can also simply call product support. Be prepared to install symbols, debuggers, and even RAS on the machine. The Knowledge Base article Q286350, HOWTO: Use Autodump+ to Troubleshoot "Hangs" and "Crashes", should definitely be able to give you a headstart.

Debugging components from ASP pages adds yet another layer of difficulty due to IIS threading and security issues. However, the Script Debugger and the above debugging code can make it much easier to debug problems right in the application process. Copying the ASP code into a Visual Basic project and creating a client executable that can then be used to debug the component can be a useful (and sometimes necessary) trick. This can be especially useful when developing ASP scripts and Visual Basic components. Overall, Visual Basic is still the best editor for creating and testing Visual Basic code.

Page view tracker