Introduction
The .Net Services (classes derived from the ServicedComponent
class) can be installed into
the COM+ 1.0 Application using the Microsoft command line tool - regsvcs.exe.
This tool does
all necessary work to run .Net Service class in the COM+ Catalog as a configured component COM.
Based on the assembly and class attributes, the regsvcs tool will configure the application and
component properties in the COM+ Catalog. The following snippet code shows an example of the
empty boilerplate of .Net Queueable Service:
[assembly: AssemblyKeyFileAttribute(@"..\..\ComponentQC.snk")]
[assembly: ApplicationName("QCLogFile")]
[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: ApplicationQueuing(Enabled=true, QueueListenerEnabled=true)]
[assembly: ApplicationAccessControl(Value = false, Authentication = AuthenticationOption.None)]
namespace ComponentQC
{
[Guid("BD294B3E-E9AF-47f9-9E12-1983DD42E1BB")]
[InterfaceQueuing]
public interface IQLogFile
{
void Write(string str);
}
[Guid("254E1CE3-B0EC-4aaa-A0D9-34733ECB68F3")]
[Transaction]
[ObjectPooling(Enabled=true, MinPoolSize=2, MaxPoolSize=5)]
[EventTrackingEnabled]
public class QLogFile : ServicedComponent, IQLogFile
{
public QLogFile() {}
public void Write(string str) {}
public override bool CanBePooled() { return true; }
public override void Activate() {}
public override void Deactivate() {}
}
}
Everything is going straightforward until the situation when your design pattern needs to
use different class inheritance (for instances: SoapHttpClientProtocol
class, third party, etc.)
and using the interface contract cannot be properly solution. Note that .Net does not allow
multiple inheritance of implementation.
This article describes a solution how to use your .Net class without using the
ServicedComponent
class and install it as a configured COM component. The solution shows
retrieving the assembly and class attributes (included custom) from the assembly file and
their storing into the COM+ Catalog Objects using the C# language.
Concept and Design.
The concept of this solution is based on the following steps:
- Using the Interface contract of the COM+ Services such as
IObjectControl,
IObjectConstruct, IObjectConstructString,
etc. instead of the class ServicedComponent.
- Using the Microsoft Assembly Registration tool
regasm.exe
to register your .Net
classes as non-configured COM components. It will be allow calling your classes transparently
using the .Net Interop design pattern (the bridge between managed and unmanaged code).
- Using the
regasm2.exe
tool (see below) to install non-configured COM components
(which have been created in the second step) into the COM+ Catalog and configure them.
The first step is a design issue related with your application. For instance, the previously
example using the COM+ interface contracts might look like the following:
[assembly: AssemblyKeyFileAttribute(@"..\..\ComponentQC.snk")]
[assembly: ApplicationName("QCLogFile")]
[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: ApplicationQueuing(Enabled=true, QueueListenerEnabled=true)]
[assembly: ApplicationAccessControl(Value = false, Authentication = AuthenticationOption.None)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
Guid("51372aec-cae7-11cf-be81-00aa00a2fa25")]
public interface IObjectControl
{
void Activate();
void Deactivate();
bool CanBePooled();
}
namespace ComponentQC
{
[Guid("BD294B3E-E9AF-47f9-9E12-1983DD42E1BB")]
[InterfaceQueuing]
public interface IQLogFile
{
void Write(string str);
}
[Guid("254E1CE3-B0EC-4aaa-A0D9-34733ECB68F3")]
[Transaction]
[ObjectPooling(Enabled=true, MinPoolSize=2, MaxPoolSize=5)]
[EventTrackingEnabled]
public class QLogFile : IQLogFile, IObjectControl
{
public QLogFile() {}
public void Write(string str) {}
public override bool CanBePooled() { return true; }
public override void Activate() {}
public override void Deactivate() {}
}
}
The changes are minor. The class instead of deriving from the ServicedComponent
class
using the IObjectControl
interface contract in the callback manner.
All the remaining steps are participated for the post-build process and they can be
incorporated into your make file. The other choice is to create a special .bat file,
which it might look like the following:
rem unregister
gacutil -u AttributeTester
regasm /u AttributeTester.dll /tlb:AttributeTester.tlb
rem register/update
regasm AttributeTester.dll /tlb:AttributeTester.tlb
regasm2 AttributeTester.dll
gacutil -i AttributeTester.dll
Note that is very important to unregister your assembly and typelib. The otherwise your
system will accumulate all versions of those registry (based on the GUID). The regasm2
tool
seems to be like the extension step to the regasm. It will take only one argument, which is
a name of the assembly. For the simplicity, the regasm2.exe file should be located in the
same path than regasm.exe file.
Also, it's a good practice to incorporate your post-build sequences into the build script
and automate all process, it's important especially when you are dealing with COM+ Services.
Regasm2 Internals.
The regasm2 is a command line program with responsibilities to install and configure your
application and its components into the COM+ Catalog. The program requires in prior access
to the assembly and typelib files. Note that both files have to be located on the same path.
The main process of the regasm2 is shown in the following snippet code:
static int Main(string[] args)
{ int retval = 0;
try
{
Console.WriteLine();
Console.WriteLine("The .Net/COM+ Installation Tool, Version 1.0");
Console.WriteLine("(C) Copyright 2001, Roman Kiss, rkiss@pathcom.com");
if(args.Length == 0 || File.Exists(args[0]) == false)
{
throw new Exception("Missing the Assembly file.");
}
Assembly assembly = Assembly.LoadFrom(args[0]);
Console.WriteLine("Assembly file: ' {0} '", assembly.FullName);
string tlbName = Path.ChangeExtension(args[0], "tlb");
if(File.Exists(tlbName) == false)
{
throw new Exception("Missing the typelib file.");
}
Console.WriteLine("Typelib file: ' {0} ' ", tlbName);
Console.WriteLine();
Console.WriteLine("Scanning Assembly for attributes ...");
Install(assembly, tlbName);
Console.WriteLine("Done");
retval = 1;
}
catch (Exception ex)
{
Console.WriteLine("ErrorMessage: {0}", ex.Message);
Console.WriteLine("StackTrace: {0}", ex.StackTrace);
}
return retval;
}
The key task of the main function is invoking the Install(assembly, tlbName)
function,
which is actually "horse" function to provide all functionality of the regasm2.
Its implementation is based on the scanning attributes in the assembly. The assembly is a
magic place where sited all information (metada). Just we need them pickup and use them for
our purpose - setting the catalog object's properties. Of course, it needs to know their
access (abstract definition). Handling properties in the catalog is using the same mechanism
for any objects such as application, component, roles, interfaces, etc. I will describe in
details only the application object.
static void Install(Assembly ass, string typelib)
{
...
COMAdminCatalog cat = new COMAdminCatalog();
ICatalogCollection colA = cat.GetCollection("Applications") as ICatalogCollection;
colA.Populate();
ICatalogObject appl = colA.Add() as ICatalogObject;
foreach(object attr in ass.GetCustomAttributes(true) )
{
if(attr is ApplicationAccessControlAttribute)
{
ApplicationAccessControlAttribute a = attr as ApplicationAccessControlAttribute;
appl.set_Value("ApplicationAccessChecksEnabled", a.Value);
appl.set_Value("AccessChecksLevel", a.AccessChecksLevel);
if(Enum.IsDefined(typeof(AuthenticationOption), a.Authentication))
{
appl.set_Value("Authentication", a.Authentication);
}
if(Enum.IsDefined(typeof(ImpersonationLevelOption), a.ImpersonationLevel))
{
if(a.ImpersonationLevel == ImpersonationLevelOption.Default)
{
appl.set_Value("ImpersonationLevel", ImpersonationLevelOption.Impersonate);
}
else
{
appl.set_Value("ImpersonationLevel", a.ImpersonationLevel);
}
}
}
else
if(attr is ApplicationCrmEnabledAttribute)
{
ApplicationCrmEnabledAttribute a = attr as ApplicationCrmEnabledAttribute;
appl.set_Value("CRMEnabled", a.Value);
}
else
AssemblyAttrExtension(attr, appl);
}
if(bIsApplNameExist == false)
{
appl.set_Value("Name", ass.FullName.Split(new char[] {','})[0]);
}
colA.SaveChanges();
Console.WriteLine(string.Format(" Step {0}. The Application '{1}' has been installed.",
step++, appl.Name));
}
Before actually scanning process we need to ask the COM+ catalog for a particular object.
In our case (see the about snippet code) we created new one:
ICatalogObject appl = colA.Add() as ICatalogObject;
This application object will automatically initiate its properties by default values. Now,
during the scanning process for each found attribute these properties are going to be
overwritten by your configuration values, for instance:
appl.set_Value("Authentication", a.Authentication);
The scanning of the assembly attributes is finishing saving all changes into the catalog:
colA.SaveChanges();
The scanner has implemented all assembly and class attributes which are part of the .Net/COM+ Services. What about custom attributes?
The scanner loop ends up in calling the chain function,
AssemblyAttrExtension(attr, appl);
which it will allow to handle your custom attributes in the loosely coupled design pattern
(using the System.Reflection) without adding their references. This is your place to put a business
logic based on your custom attributes. Two custom assembly attributes are shown in the following
snippet code:
static void AssemblyAttrExtension(object assemblyAttr, ICatalogObject appl)
{
Type t = assemblyAttr.GetType();
if(t.Name.ToString() == "ApplicationAdvancedAttribute")
{
MethodInfo mi = t.GetMethod("get_RunForever");
appl.set_Value("RunForever", mi.Invoke(assemblyAttr, null));
mi = t.GetMethod("get_CreatedBy");
appl.set_Value("CreatedBy", mi.Invoke(assemblyAttr, null));
mi = t.GetMethod("get_Deleteable");
appl.set_Value("Deleteable", mi.Invoke(assemblyAttr, null));
mi = t.GetMethod("get_ShutdownAfter");
appl.set_Value("ShutdownAfter", mi.Invoke(assemblyAttr, null).ToString());
}
else
if(t.Name.ToString() == "CustomDescriptionAttribute")
{
MethodInfo mi = t.GetMethod("get_Description");
appl.set_Value("Description", mi.Invoke(assemblyAttr, null));
}
}
Note that I created my CustomDescriptionAttribute attribute as a work around to the
DescriptionAttribute, where are not accessors such as getter and setter for private
field _desc.
Custom Attributes.
Any class derived from the Attribute
class can be used as a xxxxAttribute to setup the metada
in the assembly image. As an example of the custom attribute, I created the following
attributes to setup advanced properties of the application such as
RunForever
, change ShutdownAfter
time and CreatedBy.
Application file:
[assembly: ApplicationAdvanced(RunForever = true, ShutdownAfter = 60, CreatedBy = "Roman Kiss")]
[assembly: CustomDescription("COM+ and C# Application Test")]
Implementation file:
using System;
using System.Runtime.InteropServices;
namespace LibOfCustomAttributes
{
[AttributeUsage(AttributeTargets.Assembly)]
public class ApplicationAdvancedAttribute : Attribute
{
private string _CreatedBy = "";
private bool _Deleteable = true;
private bool _RunForever = false;
private long _ShutdownAfter = 3;
public string CreatedBy { get { return _CreatedBy; } set { _CreatedBy = value;} }
public bool Deleteable { get { return _Deleteable; } set { _Deleteable = value;} }
public bool RunForever { get { return _RunForever; } set { _RunForever = value;} }
public long ShutdownAfter { get { return _ShutdownAfter; } set { _ShutdownAfter = value;} }
}
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class)]
public class CustomDescriptionAttribute : Attribute
{
private string _Description = "";
public CustomDescriptionAttribute(string desc)
{
Description = desc;
}
public string Description { get { return _Description; } set { _Description = value;}}
}
}
Tips.
Using the .Net Services in your design the following tips are useful:
- Use an interface contracts with your .Net classes. It will allow to use a loosely coupled design pattern and access from any COM/COM+ component.
- Use explicitly GUIDs for each abstract definition such as assembly, classes, interfaces, which your registry will be updated instead of creating new ones.
- Use the
ServicedComponent
class where it is possible and regsvcs.exe installation tool.
- Automate your post-build process to be sure that your components have been updated.
Test.
The Regasm2 program can be tested using an empty application boilerplate such as
AttributeTester and LibOfCustomAttributes projects:
using System;
using System.Diagnostics;
using System.EnterpriseServices;
using System.EnterpriseServices.CompensatingResourceManager;
using System.Runtime.InteropServices;
using RKiss.COMPlusInterfaces ;
using LibOfCustomAttributes;
[assembly: ApplicationID("E970F84A-6B45-49c3-B3F7-3C210BFA6E9B")]
[assembly: ApplicationName("AttributeTester")]
[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: Description("COM+ and C# Application Test")]
[assembly: ApplicationAccessControl(Value = false, Authentication = AuthenticationOption.None)]
[assembly: ApplicationQueuing(Enabled=false, QueueListenerEnabled=false)]
[assembly: SecurityRole("Roman", true, Description = "Security role test")]
[assembly: SecurityRole("John", true, Description = "Security role test")]
[assembly: ApplicationCrmEnabled]
[assembly: ApplicationAdvanced(RunForever = true, ShutdownAfter = 60, CreatedBy = "Roman Kiss")]
[assembly: CustomDescription("COM+ and C# Application Test")]
namespace RKiss.COMPlusInterfaces
{
#region Interfaces: IObjectControl, IObjectConstruct, IObjectConstructString
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
Guid("51372aec-cae7-11cf-be81-00aa00a2fa25")]
public interface IObjectControl
{
void Activate();
void Deactivate();
bool CanBePooled();
}
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
Guid("41C4F8B3-7439-11D2-98CB-00C04F8EE1C4")]
public interface IObjectConstruct
{
void Construct([In, MarshalAs(UnmanagedType.IDispatch)] object pCtorObj);
}
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch),
Guid("41C4F8B2-7439-11D2-98CB-00C04F8EE1C4")]
public interface IObjectConstructString
{
string ConstructString { get; }
}
#endregion
}
namespace AttributeTesterAB
{
[Guid("F8D303FC-A118-4925-A6E5-39D5BB4C7774")]
[Description("Test Component A")]
[ObjectPooling(Enabled=true, MinPoolSize=2,MaxPoolSize=10)]
[Transaction(TransactionOption.Disabled)]
[ConstructionEnabled(Default="server=ATZ-ROMAN;uid=sa;pwd=;database=Logger")]
[EventTrackingEnabled]
[LoadBalancingSupported]
[Synchronization(SynchronizationOption.RequiresNew)]
[ExceptionClass("MyQCExceptionClass")]
[CustomDescription("Test Component A")]
public class TesterA : IObjectControl, IObjectConstruct
{
public TesterA()
{ Trace.WriteLine(string.Format("[{0}] TesterA.TesterA", this.GetHashCode())); }
public string Echo(string msg)
{ return string.Format("TesterA.Echo([{0}]", msg); }
public void Construct(object pCtorObj)
{
string constr = (pCtorObj as IObjectConstructString).ConstructString;
Trace.WriteLine(string.Format("[{0}] TesterA.Construct = {1}", this.GetHashCode(), constr));
}
public void Activate()
{ Trace.WriteLine(string.Format("[{0}] TesterA.Activate", this.GetHashCode())); }
public void Deactivate()
{ Trace.WriteLine(string.Format("[{0}] TesterA.Deactive", this.GetHashCode())); }
public bool CanBePooled()
{
Trace.WriteLine(string.Format("[{0}] TesterA.CanBePooled", this.GetHashCode()));
return true;
}
}
[Guid("FF1DBFA8-F1A6-408c-97A1-7E3AA1EFEBA1")]
[Description("Test Component B")]
[ObjectPooling(Enabled=false, MinPoolSize=5,MaxPoolSize=10)]
[Transaction(TransactionOption.Supported)]
[ConstructionEnabled(true, Default="server=ATZ-ROMAN;uid=sa;pwd=;database=Logger")]
[EventTrackingEnabled]
[Synchronization]
[CustomDescription("Test Component A")]
public class TesterB : IObjectControl, IObjectConstruct
{
public TesterB()
{ Trace.WriteLine(string.Format("[{0}] TesterB.TesterB", this.GetHashCode())); }
public string Echo(string msg)
{ return string.Format("TesterB.Echo([{0}]", msg); }
public void Construct(object pCtorObj)
{
string constr = (pCtorObj as IObjectConstructString).ConstructString;
Trace.WriteLine(string.Format("[{0}] TesterB.Construct = {1}", this.GetHashCode(), constr))
}
public void Activate()
{ Trace.WriteLine(string.Format("[{0}] TesterB.Activate", this.GetHashCode())); }
public void Deactivate()
{ Trace.WriteLine(string.Format("[{0}] TesterB.Deactive", this.GetHashCode())); }
public bool CanBePooled()
{
Trace.WriteLine(string.Format("[{0}] TesterB.CanBePooled", this.GetHashCode()));
return true;
}
}
}
Instructions:
- Create the assembly of the
AttributeTester
project and then start the InstallComponent.bat
file located in this project. On the console you will see all installation progress (see console screen snap on the top of this article).
- Open the COM+ Explorer and check all expected configurations for the
AttributeTester
Application (roles, components and interfaces) in the catalog.
- Open the DebugView for Windows utility (http://www.sysinternals.com).
- Start Application
AttributeTester.
The result might look like the following screen snaps:
Conclusion.
The assembly image is a very powerful source of the information related with all application
abstract definitions, configuration, etc.
In this article, we've seen how easy can be retrieved and used during the deploying time.
Beside that you have a small tool, which can be useful as a part of your development utility
library for Applications driven by .Net Services.