COM+ and the .NET Framework

http://my.execpc.com/~gopalan/dotnet/complus/complus.net_accountmanager.html

 

COM+ and the .NET Framework
Building a complete COM+ Server component using C# and .NET

Gopalan Suresh Raj

Note
To work with any of these samples, you will need the following:
.........................................Microsoft .NET SDK
.........................................Microsoft Visual Studio.NET Beta 2 or higher

 

To ease development, I recommend using the Visual Studio.NET IDE. However, you are free to develop your application in the favorite editor of your choice, using the command-line to execute the various commands to build and deploy it.

Building COM+ aware components using the .NET Framework

To develop .NET managed components that can be configured to function under the COM+ Runtime, you need to provide these components with numerous attributes defined in the System.EnterpriseServices namespace. To start off, each .NET class that's supposed to run under COM+ needs to derive from the System.ServicedComponent class. This base class provides default implementations of the classic MTS/COM+ interface IObjectControl - Activate(), Deactivate(), and CanBePooled(). You can override the default implementations if you wish to do so, just as will be seen shortly.

Once any number of COM+ centric attributes are added to the .NET component, the assembly will have to be compiled. However, to place this assembly under the control of COM+, a new utility (regsvcs.exe) will have to be used as we will see soon. In addition to installing the component into the COM+ catalog, this utility also provides a lot of other services that we shall soon see.

Finally, for the COM+ Surrogate (dllhost.exe) to locate your assembly and to host it in a given activity, it must be able to locate your binary. Therefore, you should install your assembly into the system's Global Assembly Cache (GAC).

The various steps that are involved in creating a COM+ Server Component using C# and the .NET Framework are as follows (I'm going to assume you're using the VS.NET IDE):

  1. Create a Visual C# - Class Library project

  2. Generate a Key-Value pair to use when deploying your Shared Assembly

  3. Configure your Project Property Pages with the right information

  4. Develop the AccountManager.cs library

  5. Modify the generated AssemblyInfo.cs to add the right assembly information

  6. Build the Project Files

  7. Deploy the component as a Shared Assembly, and Configure the Assembly in the COM+ Catalog

The BookKeeper Module

My goal is to simplify illustration of a typical COM+ serviced component development process. I am therefore, in this article, going to reuse the BookKeeper example for all database operations. As a result, all our data is going to be maintained in an XML datastore!!! I had used the BookKeeper example in an earlier article to illustrate ADO.NET's disconnected operation facility - the DataSet. To refresh, the DataSet facilitates the client to manipulate and update a local copy of any number of related tables while still disconnected from the data source and submit the modified data back for processing using a related data adapter at a later point in time.

The AccountManager Module

Our hypothetical AccountManager Module (that we will build in this article), is actually a COM+ Server component that performs just a couple of functions. It is the module that manages creation and deletion of accounts (Checking accounts or Savings accounts) for a Large Commercial Bank Project. It offers no other services except "Create Account", and "Delete Account".

1. Create a Visual C# - Class Library project

Create a new Visual C# Class Library project. Remember that the COM+ Runtime can only host types contained in a DLL.

 

2. Generate a Key-Value pair to use when deploying your Shared Assembly

Shared Assemblies are those that can be used by any client application, such as a system DLL that every process in the system can use. Unlike private-assemblies, shared assemblies must be published or registered in the system's Global Assembly Cache (GAC). As soon as they are registered in the GAC, they act as system components. An essential requirement for GAC registration is that the component must possess originator and version information. In addition to other metadata information, these two items allow multiple versions of the same component to be registered and executed on the same machine. Unlike Classic COM, we don't have to store any information in the system registry for clients to use these shared assemblies.

There are three general steps to registering shared assemblies in the GAC:

  1. The Shared Name (sb.exe) utility should be used to obtain the public/private key pair. This utility generates a random key pair value, and stores it in an output file - for example, AccountManager.key.

  2. Build the assembly with an assembly version number and the key information in the AccountManager.key

  3. Using the .NET Global Assembly Cache (gacutil.exe) utility, register the assembly in the GAC.

The assembly now becomes a shared assembly and can be used by any client in the system.

Therefore, as a first step, use the Shared Name Utility to obtain a public/private key pair and store it in a file (AccountManager.key, in this case) as shown below.

Command Prompt
C:\MyProjects\Cornucopia\COMplus\BankServer\AccountManager>sn -k AccountManager.key

Microsoft (R) .NET Framework Strong Name Utility Version 1.0.2914.16
Copyright (C) Microsoft Corp. 1998-2001. All rights reserved.

Key pair written to AccountManager.key

C:\MyProjects\Cornucopia\COMplus\BankServer\AccountManager>

The -k option generates the random key pair and saves the key information in the AccountManager.key file. We use this file as input when we build our Shared Assemblies.

 

3.  Configure your Project Property Pages with the right information

Configure the Project Properties with the right information. Make sure you specify the Assembly Name that you want for the Assembly. Specifically, move to the General tab, and in the Wrapper Assembly Key File area, enter the key file to use. In this case, it is AccountManager.key.

Move to the Reference Path Properties area, and select the directory that contains the BookKeeper executable.

Go to "Project Dependancy" and select the BookKeeper as a dependancy for this project. This means the BookKeeper project has to be compiled before compiling this project.

To the AccountManager project files, also add the BookKeeper.cs, and the AccountKey.cs files from the BookKeeper project.

4. Develop the AccountManager.cs library

Transactions

To develop a .NET class that supports transactions, here's what you have to do:

  • The class must derive from the System.ServicedComponent class to exploit COM+ Services as shown in Line 94.
  • The class must be created with the correct Transaction attribute such as Transaction (TransactionOption.Required) as shown in Line 78.

Besides this, you can use the System.EnterpriseServices.ContextUtil class to obtain information about the COM+ object context as shown in Line 138. This class exposes important methods of COM+ like SetComplete() and SetAbort(), and IsCallerInRole(), and important COM+ properties like IsInTransaction, and MyTransactionVote. Additionally, while it's not necessary to specify COM+ Application installation options, you can always specify what you want. Notice that we use attributes to specify a number of things.

In the AccountManager.create() method, we simply call ContextUtil.SetComplete() - Line 138 -when we've successfully created a new account into our database. If something has gone wrong during the process, we will vote to abort the transaction by calling ContextUtil.SetAbort() as shown on Line 142.

Instead of calling ContextUtil.SetComplete() and ContextUtil.SetAbort() explicitly, we can also use the AutoComplete( true ) attribute, as shown on line 165 which is conceptually equivalent to the previously shown AccountManager.create() method.

AccountManager.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
115:
116:
117:
118:
119:
120:
121:
122:
123:
124:
125:
126:
127:
128:
129:
130:
131:
132:
133:
134:
135:
136:
137:
138:
139:
140:
141:
142:
143:
144:
145:
146:
147:
148:
149:
150:
151:
152:
153:
154:
155:
156:
157:
158:
159:
160:
161:
162:
163:
164:
165:
166:
167:
168:
169:
170:
171:
172:
173:
174:
175:
176:
177:
178:
179:
180:
181:
182:
183:
184:
185:
186:
187:
188:
189:
190:
191:
192:
193:
194:
195:
196:
197:
198:
199:
200:
201:
202:
203:
204:
205:
206:
207:
208:
209:
210:
211:
212:
213:
214:
215:
216:
217:
218:
219:
220:
221:
222:
223:
224:
225:
226:
227:
228:
229:
230:
231:
232:
233:
234:
235:
236:
237:
238:
239:
240:
241: 
242:
//////////////////////////////////////////////////////
/// The following example illustrates a COM+ type
/// developed using C# and the .NET Framework.
///
/// author: Gopalan Suresh Raj
/// Copyright (c), 2001. All Rights Reserved.
/// URL: http://gsraj.tripod.com/
/// email: [email protected]
///
/// <register>
/// gacutil /i Bank.dll
/// regsvcs /fc Bank.dll
/// </register>
//////////////////////////////////////////////////////

using System;
using System.Runtime.InteropServices;
using System.EnterpriseServices;
// Include the following for the Trace class
using System.Diagnostics;  
// Include the following for Windows Message Box
using System.Windows.Forms;
// Include the BookKeeper namespace
using BookKeeper;

namespace Bank {

  ///////////////////////////////////////////////////////////////
  /// <summary>
  /// The Account Manager interface
  /// </summary>
  /// <remarks>
  /// This interface defines create and delete methods to
  /// add or delete Bank Accounts
  /// </remarks>
  ///////////////////////////////////////////////////////////////

  /// Indicate whether a managed interface is dual, IDispatch or
  /// IUnknown based when exposed to COM
  [ InterfaceTypeAttribute( ComInterfaceType.InterfaceIsDual ) ]

  public interface IAccountManager {

    /// <summary>
    /// The create method
    /// </summary>
    /// <remarks>
    /// Method used to create a new Bank account
    /// </remarks>
    /// <param name="type">Either Checking or Savings</param>
    /// <param name="customerNames">Customers who own this account</param>
    /// <param name="startingBalance">Initial Deposit</param>
    /// <returns></returns>
    int create (AccountType type, string[] customerNames, float startingBalance);

    /// <summary>
    /// The delete method
    /// </summary>
    /// <remarks>
    /// Method used to delete and existing Bank account
    /// </remarks>
    /// <param name="accountKey">the Account Number</param>
    /// <returns>true if Account deleted, false if not</returns>
    bool delete (int accountKey);
  }

  ///////////////////////////////////////////////////////////////
  /// <summary>
  /// AccountManager used to create new Accounts or delete accounts.
  /// </summary>
  ///////////////////////////////////////////////////////////////

  /// Specify a name for your serviced component
  [ ProgId( "COM+ Bank Server Account Manager" ) ]
  /// Add content to hosting COM+ App's description field
  [ Description( "COM+ Bank Server Account Manager" ) ]
  /// Configure component's Transaction Option
  [ Transaction( TransactionOption.Required ) ]
  /// Configure component's object pooling
  [ ObjectPooling( MinPoolSize = 5, MaxPoolSize = 10, CreationTimeout = 20 ) ]
  /// Specify COM+ Context Attributes
  [ MustRunInClientContext( false ) ]
  /// Enable event tracking
  [ EventTrackingEnabled( true ) ]
  /// Enable JITA for the component
  [ JustInTimeActivation( true ) ]
  /// Enable Construction String Support for the component
  [ ConstructionEnabled( Enabled=true, Default="Gopalan's Bank Server" ) ]
  /// Configure activity-based Synchronization for the component
  [ Synchronization( SynchronizationOption.Required ) ]
  /// Indicate the type of class interface that will be generated for this class
  [ ClassInterface( ClassInterfaceType.AutoDual ) ]

  public class AccountManager : ServicedComponent, IAccountManager {

    /// <summary>
    /// Public No-argument Default Constructor
    /// </summary>
    public AccountManager() {
      MessageBox.Show ("Bank::AccountManager() invoked...");
    }

    /////////////////////////////////////////////////////////////////////////
    /// The Following methods support core functionality required of the
    /// AccountManager component and implement the IAccountManager interface
    /////////////////////////////////////////////////////////////////////////

    /// <summary>
    /// The create method
    /// </summary>
    /// <remarks>
    /// Method used to create a new Bank account
    /// </remarks>
    /// <param name="type">Either Checking or Savings</param>
    /// <param name="customerNames">Customers who own this account</param>
    /// <param name="startingBalance">Initial Deposit</param>
    /// <returns></returns>

    /// Add content to hosting COM+ App's description field
    [ Description( "Creates a new account for the Bank Server" ) ]

    public int create (AccountType type, string[] customerNames, float startingBalance) {
      MessageBox.Show ("Bank::create() invoked...");
      AccountKey key = null;
      try {
        // Create the BookKeeper class
        BookKeeper.BookKeeper keeper = new BookKeeper.BookKeeper();
        if (null != keeper) {
          // Call the BookKeeper to create a new Bank Account
          key = keeper.createAccount (type, customerNames, startingBalance);
          // Clean-up the BookKeeper object
          keeper.Dispose();
        }
        else {
          throw new Exception ( "BookKeeper Object could not be created." );
        }
        // Since everything went well, commit the changes
        ContextUtil.SetComplete();
      }
      catch (Exception exception) {
        // An Error occured, so rollback the changes
        ContextUtil.SetAbort();

        // Trace the current COM+ context ID (its GUID) to the output window
        // use the ContextId static property of ContextUtil
        Guid contextID = ContextUtil.ContextId;
        String traceMessage = "Context ID is " + contextID.ToString();
        Trace.WriteLine (traceMessage.ToString ());

        MessageBox.Show (exception.ToString (), "Bank::create()");
      }
      return key.Key;
    }

    /// <summary>
    /// The delete method
    /// </summary>
    /// <remarks>
    /// Method used to delete and existing Bank account
    /// </remarks>
    /// <param name="accountKey">the Account Number</param>
    /// <returns>true if Account deleted, false if not</returns>

    /// Take advantage of COM+'s method auto-deactivation
    [ AutoComplete( true ) ]
      /// Add content to hosting COM+ App's description field
    [ Description( "Deletes an existing account from the Bank Server" ) ]

    public bool delete (int accountKey) {
      bool result = false;
      MessageBox.Show ("Bank::delete() invoked...", "Key Value = "+accountKey);
      /*
      try {
        // Programming Role based security
        SecurityCallContext callContext;
        callContext = SecurityCallContext.CurrentCall;
        string caller = callContext.DirectCaller.AccountName;
        bool isInRole = callContext.IsCallerInRole ( "Manager" );
        if (false == isInRole) {
          throw new Exception ( "Only Managers can delete Customers" );
        }
        */

        BookKeeper.BookKeeper keeper = new BookKeeper.BookKeeper();
        if (null != keeper) {
          AccountKey key = new AccountKey();
          key.Key = accountKey;
          result = keeper.deleteAccount (key);
          keeper.Dispose();
        }
        else {
          throw new Exception ( "BookKeeper Object could not be created." );
        }
      /*}
      catch (Exception exception) {
        Guid contextID =
ContextUtil.ContextId;
        String traceMessage = "Context ID is " + contextID.ToString();
        Trace.WriteLine (traceMessage.ToString ());

        MessageBox.Show (exception.ToString (), "Bank::delete()");
      }
*/

      return result;
    }

    /////////////////////////////////////////////////////////////////////////
    /// All the Following methods support overriding functionality required for
    /// implementing some of the COM+ support interfaces like IObjectConstruct.
    /////////////////////////////////////////////////////////////////////////

    /// <summary>
    /// The method is called after the component's constructor
    /// and is passed in the user specific constructionString
    /// </summary>
    /// <param name="constructionString"></param>
    override public void Construct (string constructionString) {
      MessageBox.Show ("Bank::Construct() invoked...");
      MessageBox.Show (constructionString, "Construction String");
    }

    /// <summary>
    /// Do context specific initialization in this method
    /// </summary>
    override public void Activate () {
      MessageBox.Show ("Bank::Activate() invoked...");
    }

    /// <summary>
    /// Do context specific cleanup in this method
    /// </summary>
    override public void Deactivate () {
      MessageBox.Show ("Bank::Deactivate() invoked...");
    }

    /// <summary>
    /// Object Pooling support method
    /// </summary>
    /// <returns>true if pooling is supported, false if not</returns>
    override public bool CanBePooled () {
      MessageBox.Show ("Bank::CanBePooled() invoked...");
      return true;
    }
  }
}

Object Pooling

Object Pooling is a feature that was introduced in COM+, but was missing in MTS. Object Pooling allows you to minimize the use of system resources, by pooling objects that support transactions but are expensive to create. This improves performance and helps system scalability. If you want to support object pooling in you components, you need to derive from the System.ServicedComponent class, and override any of the Activate(), Deactivate(), and CanBePooled() methods, and specify object pooling requirements in an ObjectPooling attribute as shown on Line 80. You can take advantage of the Activate() and Deactivate() methods to perform the appropriate initialization and cleanup. The CanBePooled() method is used to tell COM+ whether this object can be pooled or not. This way, you can provide any expensive object-creation functionality in the constructor of the component.

Since our COM+ components support Object Pooling, The COM+ runtime activates and deactivates them as required. After each Client call has been serviced, it puts the component object back into the object pool. As soon as a new Client call arrives, it picks the same component object back from the pool to service the new request.

 

5. Modify the generated AssemblyInfo.cs to add the right assembly information

You provide the compiler with your assembly information in an assembly file called AssemblyInfo.cs. The assembly information file is compiled with the  rest of the project's source files. The information is in the form of assembly attributes - directives to the compiler on the information to embed in the assembly.

AssemblyInfo.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
using System.Reflection;
using System.Runtime.CompilerServices;
using System.EnterpriseServices;

//
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
//
[assembly: AssemblyTitle("AccountManager for Bank")]
[assembly: AssemblyDescription("Creates and Deletes Accounts for the Bank")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("eCommWare Corporation")]
[assembly: AssemblyProduct("COM+ Bank Server")]
[assembly: AssemblyCopyright("(c) 2001, Gopalan Suresh Raj. All Rights Reserved.")]
[assembly: AssemblyTrademark("Web Cornucopia")]
[assembly: AssemblyCulture("en-US")]

//
// Version information for an assembly consists of the following four values:
//
//      Major Version
//      Minor Version
//      Build Number
//      Revision
//
// You can specify all the values or you can default the Revision and Build Numbers
// by using the '*' as shown below:

[assembly: AssemblyVersion("1.0.0.0")]

//
// In order to sign your assembly you must specify a key to use. Refer to the
// Microsoft .NET Framework documentation for more information on assembly signing.
//
// Use the attributes below to control which key is used for signing.
//
// Notes:
//   (*) If no key is specified, the assembly is not signed.
//   (*) KeyName refers to a key that has been installed in the Crypto Service
//       Provider (CSP) on your machine. KeyFile refers to a file which contains
//       a key.
//   (*) If the KeyFile and the KeyName values are both specified, the
//       following processing occurs:
//       (1) If the KeyName can be found in the CSP, that key is used.
//       (2) If the KeyName does not exist and the KeyFile does exist, the key
//           in the KeyFile is installed into the CSP and used.
//   (*) In order to create a KeyFile, you can use the sn.exe (Strong Name) utility.
//       When specifying the KeyFile, the location of the KeyFile should be
//       relative to the project output directory which is
//       %Project Directory%\obj\<configuration>. For example, if your KeyFile is
//       located in the project directory, you would specify the AssemblyKeyFile
//       attribute as [assembly: AssemblyKeyFile("..\\..\\mykey.snk")]
//   (*) Delay Signing is an advanced option - see the Microsoft .NET Framework
//       documentation for more information on this.
//
[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile("AccountManager.key")]
[assembly: AssemblyKeyName("")]

[assembly: ApplicationName( "COM+ Bank Server Account Manager" )]
[assembly: ApplicationActivation( ActivationOption.Server )]

In particular, pay attention to the fact that we specify a version number for this library using the AssemblyVersion attribute and also specify the assembly key file using the AssemblyKeyFile attribute. The ApplicationName attribute is self-explanatory. However, the attribute of special interest is the ApplicationActivation attribute. As you may know, MTS and COM+ applications may either be hosted as a Library (e.g., Activated in the Caller's process) or Server (e.g., Activated in a new instance of dllhost.exe). The default attribute is to configure your COM+ application as a Library. Here we want to explicitly set the activation option to be specified as ActivationOption.Server.

6. Build the Project Files

Build the files that make up the project.

------ Rebuild All started: Project: BookKeeper, Configuration: Debug .NET ------

Preparing resources...
Updating references...
Performing main compilation...

Build complete -- 0 errors, 0 warnings
Building satellite assemblies...



------ Rebuild All started: Project: AccountManager, Configuration: Debug .NET ------

Preparing resources...
Updating references...
Performing main compilation...

Build complete -- 0 errors, 0 warnings
Building satellite assemblies...



---------------------- Done ----------------------

Rebuild All: 2 succeeded, 0 failed, 0 skipped

7. Deploy the component as a Shared Assembly and Configure it in the COM+ Catalog

After you've built the assembly, you can use the .NET Global Assembly Cache (GAC) utility to register this assembly into the GAC as shown below.

Command Prompt
C:\MyProjects\Cornucopia\COMplus\BankServer\AccountManager\bin\Debug>gacutil /i Bank.dll

Microsoft (R) .NET Global Assembly Cache Utility. Version 1.0.2914.16
Copyright (C) Microsoft Corp. 1998-2001. All rights reserved.

Assembly successfully added to the cache

C:\MyProjects\Cornucopia\COMplus\BankServer\AccountManager\bin\Debug>
regsvcs /fc Bank.dll
RegSvcs - .NET Services Installation Utility Version 1.0.2914.16
Copyright (C) Microsoft Corp. 2000-2001. All rights reserved.

Installed Assembly:
Assembly: C:\MyProjects\Cornucopia\COMplus\BankServer\AccountManager\bin\Debug\Bank.dll
Application: COM+ Bank Server Account Manager
TypeLib: c:\myprojects\cornucopia\complus\bankserver\accountmanager\bin\debug\Bank.tlb

C:\MyProjects\Cornucopia\COMplus\BankServer\AccountManager\bin\Debug>

Successful registration against the cache turns this component into a shared assembly. A version of this component is copied into the GAC so that even if you delete this file locally, you will still be able to run your client program.

Configuring our Assembly in the COM+ Catalog

Configuring a .NET assembly in the COM+ Catalog means, you need to generate a COM Type Library (tlbexp.exe), and register the type in the system registry (regasm.exe). You also have to make sure that you enter the right information into the COM+ Catalog (RegDB). Instead of using all these tools individually, the .NET SDK provides an additional tool called the Register Services utility (regsvcs.exe). This utility simplifies the process by making sure that all required details are taken care of in a single step. It performs the following functions:

  • Our Assembly is loaded into memory
  • Out Assembly is registered (e.g., just like using regasm.exe)
  • A COM Type Library (.tlb file) is generated and registered (e.g., just like using tlbexp.exe)
  • The generated COM Type Library is installed in the specified COM+ Application
  • Our Components are configured according to the attributes that are specified in the type definitions

If you notice carefully, when we use the regsvcs.exe utility, we specify the /fc option (find or create) to instruct the tool to build a new COM+ application if one does not currently exist.

The Component Services Explorer

Once you have done all this, you can open up the Windows 2000 Component Services Explorer and discover that your .NET Assembly is now recognized as valid COM+ Application.

While you explore the various property windows for this COM+ Application, you realize that the various attributes that you specified in the C# class have been used to configure our component in the COM+ Catalog. Right Click the Component and check out the Activation tab for example as shown in the screen shot below.

The above settings have been automatically configured based on the following class-level attributes that you set programmatically in your original C# class from lines 82-88 in the source code above.

  /// Configure component's object pooling
  [ ObjectPooling( MinPoolSize = 5, MaxPoolSize = 10, CreationTimeout = 20 ) ]
  /// Specify COM+ Context Attributes
  [ MustRunInClientContext( false ) ]
  /// Enable event tracking
  [ EventTrackingEnabled( true ) ]
  /// Enable JITA for the component
  [ JustInTimeActivation( true ) ]
  /// Enable Construction String Support for the component
  [ ConstructionEnabled( Enabled=true, Default="Gopalan's Bank Server" ) ]
 

Now you need to build a client application that can access this COM+ Server component.

COM+
Building a complete COM+ Server component using C# and .NET
Building a COM+ Client using C# and .NET

 

你可能感兴趣的:(framework)