Using the System.Management Namespace

Overview

Driven by the desire to make the bulk of WMI system management functions easily accessible to .NET developers, the designers of the System.Management namespace built quite a few powerful types that expose all but the most obscure aspects of WMI functionality. Housing more than 40 different classes, this namespace may look complex or even intimidating to a beginner or a casual observer. This seeming complexity, however, is just an illusion; in fact, most of the System.Management types are remarkably easy to use thanks to a well-defined structure and a well-thought-out design. I'm sure that once you spend some time programming .NET management applications, you will quickly overcome any initial confusion and grow to appreciate the elegance and straightforwardness of the System.Management object model.

The first thing that will help you to take the veil of complexity off the System.Management namespace is understanding that most of its types are auxiliary—they do not, by themselves, represent logical abstractions of WMI functionality. In fact, you will soon find yourself spending most of your time with two of the System.Management types: ManagementObject and ManagementClass . These two types reflect WMI objects and schema elements respectively. The rest of the types, although they are very useful and even indispensable at times, are just used to either obtain certain instances of the ManagementObject and ManagementClass types, or affect the behavior of these two types.

Since this chapter zeroes in on the methods and properties of ManagementObject and ManagementClass types, you may want to focus on the material presented here to understand the core of System.Management functionality and to become proficient at programming the management applications. While certain more complex or more specialized aspects of WMI programming are covered in subsequent chapters, the most fundamental concepts, such as navigating the CIM, reading and updating the management data, and analyzing the WMI schema, are presented here. In fact, the contents of this chapter alone should equip you with the skills necessary to build moderately complex system management tools.

ManagementObject Retrieving the Management Information

The most fundamental type that the System.Management namespace exposes is called ManagementObject. This type represents a .NET view of an arbitrary WMI class instance. In other words, it corresponds to an actual managed element within a given environment. Once created, an instance of the ManagementObject type provides the methods and properties necessary to manipulate the management data associated with an instance of a given WMI class.

Binding to WMI Objects

Creating an instance of the ManagementObject type is remarkably easy. The following line of code produces a single unbound instance of this type using its parameterless constructor:

ManagementObject mo = new ManagementObject();

This instance of the ManagementObject type is not very useful unless it is linked to an arbitrary WMI class instance. In order to establish this association you must use the type's overloaded constructor; this constructor takes a single parameter of type String, which represents the object path. As outlined in Listing 1-3 of Chapter 1, a full object path consists of the name of the server, which hosts the WMI implementation; the namespace handle; the name of the WMI class; and a key-value pair, which uniquely identifies an instance. Using these components, the following code will create an instance of the ManagementObject type, associated with the instance of the Win32_ComputerSystem WMI class. This class resides in the rootCIMV2 namespace on the BCK_OFFICE machine and it is uniquely identified by the value of its Name property:

ManagementObject mo = new ManagementObject( @"\BCK_OFFICE ootCIMV2:Win32_ComputerSystem.Name='BCK_OFFICE'");

Often when you are developing a managed application you will want to work with WMI objects on your local machine. But because hardcoding the server name into a program is not a very flexible solution, you can substitute "." (which stands for the name of the local machine) for the server name. In fact, you don't need to include the name of the hosting server or "." in the object path at all; if you omit it, the local computer system will be the default server.

The namespace portion of the path is also optional and, if you omit it, rootCIMV2 will be the default. In our discussion of WMI settings in Chapter 1, you learned that the default value for the namespace is specified by the HKLMSOFTWAREMICROSOFTWBEMScriptingDefault Namespace registry setting. You may also recall that changing the registry value does not affect the namespace to which ManagementObject gets bound. This is because the default path is hardcoded in the ManagementPath type's static type constructor so that every time this type is loaded, its static property, DefaultPath, is set to \. ootCIMV2. During its initialization sequence, the ManagementObject type examines the supplied object path and, if necessary, it reads the default value from the ManagementPath's DefaultPath property. Hopefully, in future FCL releases, Microsoft will address this issue and make sure that the default namespace for ManagementObject is configurable rather than hardcoded.

Although you must encode a unique object identity into the object path, including the name of the property that functions as an object's key is optional. In other words, you can place an equal sign and the value of the key directly after the name of the class—WMI is smart enough to apply the value to a correct property based on the class definition. Therefore, to create an instance of the ManagementObject type that is bound to the WMI class's Win32_ComputerSystem instance (which resides on the local machine), you would use the following code:

ManagementObject mo = new ManagementObject(@" Win32_ComputerSystem='BCK_OFFICE'");

Accessing Singletons

Anyone who spends some time playing with WMI will eventually notice that there are some classes that don't seem to have any key properties. For example, the Win32_WMISetting class, discussed in Chapter 1, does not designate any of its properties as a key that uniquely identifies a given instance. The reason behind this is simple—these classes, known in object-oriented parlance as singletons, are restricted to having only one instance at a time.

To bind a ManagementObject to an instance of a singleton class such as Win32_WMISetting, you may assume that you can just use the class name as a path and let WMI figure out the rest. However, this approach does not quite work; as a result, the following code throws an ArgumentOutOfRangeException:

ManagementObject mo = new ManagementObject("Win32_WMISetting");

Appending the equal sign right after the name of the class will not work either; doing so will just cause WMI to throw a ManagementException. It turns out that WMI has a special syntax to access singletons. As demonstrated by the following example, you have to use the @ placeholder as a property value:

ManagementObject mo = new ManagementObject("Win32_WMISetting=@");

During its initialization sequence, ManagementObject validates the object path that is passed as a parameter to its constructor. To aid the validation, it uses another type, called ManagementPath, which is responsible for parsing the path string and breaking it up into the server, the namespace path, and object path components. The ManagementPath is a public type, and as such, you can create it manually and then feed it into an overloaded version of the ManagementObject constructor. In fact, this type offers a slew of useful properties and methods and, on occasion, using it may ease the process of binding the ManagementObject to a WMI class instance. The following code, for instance, does the same thing as the previous example:

ManagementPath mp = new ManagementPath(); mp.Server = "BCK_OFFICE"; mp.NamespacePath = @" ootCIMV2"; mp.ClassName = "Win32_WMISetting"; mp.SetAsSingleton(); ManagementObject mo = new ManagementObject(mp);

Here the properties of the ManagementPath instance are explicitly assigned and then the path is marked as a singleton path using the SetAsSingleton method. This approach is cleaner and more readable because the ManagementPath object takes care of assembling a valid WMI object path and relieves the user of remembering the syntax details, such as separators and placeholder values.

Using the ManagementPath Type

I already mentioned that the ManagementObject type constructor creates an instance of ManagementPath internally, unless, of course, you pass it as a parameter. In either case, you can access the ManagementPath object, which is associated with a given instance of a ManagementObject, through the Path property of the ManagementObject type. Therefore, the code above could be rewritten as follows:

ManagementObject mo = new ManagementObject(); mo.Path.Server = 'BCK_OFFICE'; mo.Path.NamespacePath = @" ootCIMV2"; mo.Path.ClassName = "Win32_WMISetting"; mo.Path.SetAsSingleton();

This approach is, perhaps, a little more concise, but otherwise it is no different from the previous one.

One other property of the ManagementPath type, which I already mentioned, is DefaultPath; this property contains the default server and namespace portions of the object path. As you may remember, the initial value of this property is automatically set to a hardcoded string, "\. ootCIMV2", when the type is first loaded. Because the DefaultPath is a static property that can be written into, if you set it manually before using the ManagementPath type, you will affect the default namespace settings for all the subsequently created management objects. Thus, to make the default namespace setting configurable via the system registry, you can use code similar to the following:

RegistryKey rk = Registry.LocalMachine.OpenSubKey( @" SOFTWAREMicrosoftWBEMScripting"); ManagementPath.DefaultPath = new ManagementPath(rk.GetValue( "Default Namespace").ToString());

This code first reads the static LocalMachine property of the Microsoft.Win32.Registry type to get a reference to an instance of Microsoft.Win32.RegistryKey type that is bound to the HKEY_LOCAL_MACHINE registry key. It then retrieves the reference to the SOFTWAREMicrosoftWBEMScripting subkey using the OpenSubKey method of the RegistryKey. Finally, it assigns the value of the Default Namespace registry entry (obtained with the GetValue method of the RegistryKey type) to the ManagementPath type's static DefaultPath property.

The ManagementPath type has a few other properties that may come in handy when you are analyzing an arbitrary WMI object path. Using these properties, you can determine whether an object path refers to a class or an instance of a class and whether the object at which it points is a singleton. For instance, this snippet of code

ManagementPath mp = new ManagementPath("Win32_WMISetting=@"); Console.WriteLine("Class: {0}", mp.IsClass); Console.WriteLine("Instance: {0}", mp.IsInstance); Console.WriteLine("Singleton: {0}", mp.IsSingleton);

produces the following output on a console:

Class: False Instance: True Singleton: True

Implicit vs Explicit Binding

Although the ManagementPath constructor and property set routines validate the object path, the scope of this validation only ensures that the path is compliant with the WMI object path syntax constraints. So, if a path contains the name of a nonexistent class, the ManagementPath type will not throw any exceptions. For instance, if the name of the class in the previous example is mistakenly entered as Win64_WMISetting rather than Win32_WMISetting, the code will still work perfectly and produce the same results. Moreover, if you try to create an instance of ManagementObject type based on an object path that references a nonexistent WMI class, you will succeed. However, the instance you get will be largely unusable and if you invoke its methods, a ManagementException will be thrown.

The explanation for this unexpected behavior is simple—instances of the ManagementObject type are initialized in a lazy fashion. In an attempt to conserve resources, none of the ManagementObject constructors issue WMI COM API calls. Instead, when you first use an instance of ManagementObject, its Initialize method is called. The Initialize method checks whether the instance is already connected to WMI; if it isn't it invokes the initialization sequence of the ManagementScope type. Then the ManagementScope InitializeGuts method (briefly discussed in Chapter 1) obtains a pointer to IWbemLocator object and calls its ConnectServer method to retrieve the IWbemServices interface pointer. Once a valid WMI connection is established—or, in other words, when an IWbemServices interface pointer is obtained —the instance of ManagementObject calls the IWbemServices::GetObject method to retrieve an arbitrary instance of WMI class. The resulting WMI object reference (strictly speaking, an IWbemClassObjectFreeThreaded interface pointer) is cached in the wbemObject field of the ManagementObject instance. From this point on, the instance of the ManagementObject is said to be "bound" to WMI and can be used to manipulate the data associated with the corresponding WMI object.

Instead of relying on the ManagementObject type to connect to an appropriate WMI object in a lazy fashion, you can use the object's Get method to request that it is bound explicitly. For instance, the explicit binding to an underlying WMI object may be achieved as follows:

ManagementObject mo = new ManagementObject("Win32_WMISetting=@"); mo.Get();

Here, the explicitly initiated Get operation invokes the initialization sequence of the ManagementObject type and establishes a connection to WMI. In this scenario, it may seem that the Get method provides little value over lazy initialization, except perhaps for adding a little code clarity. However, you will find that the over-loaded variation of Get, which is designed to support asynchronous operations, often comes in very handy. The asynchronous data manipulation techniques will be discussed in detail in "Asynchronous Programming" later in this chapter.

Accessing Properties of WMI Objects

Now that you have created a valid instance of the ManagementObject type, you will find it easy to retrieve the properties of a WMI object it represents. In Listing 2-1, you can see a complete implementation of a PrintWMISettings class, which binds to a single instance of Win32_WMISetting class and prints all its property values on a system console.

Listing 2-1: Printing Win32_WMISetting Properties

using System; using System.Management; class PrintWMISettings { public static void Main(string[] args) { ManagementObject mo = new ManagementObject("Win32_WMISetting=@"); Console.WriteLine("ASPScriptDefaultNamespace : {0}", mo["ASPScriptDefaultNamespace"]); Console.WriteLine("ASPScriptEnabled : {0}", mo["ASPScriptEnabled"]); Console.WriteLine("AutorecoverMofs : {0}", mo["AutorecoverMofs"]); Console.WriteLine("AutoStartWin9X : {0}", mo["AutoStartWin9X"]); Console.WriteLine("BackupInterval : {0}", mo["BackupInterval"]); Console.WriteLine("BackupLastTime : {0}", mo["BackupLastTime"]); Console.WriteLine("BuildVersion : {0}", mo["BuildVersion"]); Console.WriteLine("Caption : {0}", mo["Caption"]); Console.WriteLine("DatabaseDirectory : {0}", mo["DatabaseDirectory"]); Console.WriteLine("DatabaseMaxSize : {0}", mo["DatabaseMaxSize"]); Console.WriteLine("Description : {0}", mo["Description"]); Console.WriteLine("EnableAnonWin9xConnections : {0}", mo["EnableAnonWin9xConnections"]); Console.WriteLine("EnableEvents : {0}", mo["EnableEvents"]); Console.WriteLine("EnableStartupHeapPreallocation : {0}", mo["EnableStartupHeapPreallocation"]); Console.WriteLine("HighThresholdOnClientObjects : {0}", mo["HighThresholdOnClientObjects"]); Console.WriteLine("HighThresholdOnEvents : {0}", mo["HighThresholdOnEvents"]); Console.WriteLine("InstallationDirectory : {0}", mo["InstallationDirectory"]); Console.WriteLine("LastStartupHeapPreallocation : {0}", mo["LastStartupHeapPreallocation"]); Console.WriteLine("LoggingDirectory : {0}", mo["LoggingDirectory"]); Console.WriteLine("LoggingLevel : {0}", mo["LoggingLevel"]); Console.WriteLine("LowThresholdOnClientObjects : {0}", mo["LowThresholdOnClientObjects"]); Console.WriteLine("LowThresholdOnEvents : {0}", mo["LowThresholdOnEvents"]); Console.WriteLine("MaxLogFileSize : {0}", mo["MaxLogFileSize"]); Console.WriteLine("MaxWaitOnClientObjects : {0}", mo["MaxWaitOnClientObjects"]); Console.WriteLine("MaxWaitOnEvents : {0}", mo["MaxWaitOnEvents"]); Console.WriteLine("MofSelfInstallDirectory : {0}", mo["MofSelfInstallDirectory"]); Console.WriteLine("SettingID : {0}", mo["SettingID"]); } }

In this code listing, the most interesting thing is the use of the indexer property to retrieve the property values. The indexer, which is defined in the base type of the ManagementObject—ManagementBaseObject, takes a string property name index as a parameter and returns the value of the corresponding object property. If a property associated with a given name cannot be located within a given WMI object (for instance, if the property name is misspelled), the indexer will throw a ManagementException.

In this listing, the indexer returns the property value as an object, which can be easily cast back to its actual data type if need be. Assuming that I am only interested in printing the property values, this implementation will be sufficient. This is because the WriteLine method will automatically convert its parameters to strings using the ToString method's overrides of the respective types; in most cases, this approach will result in the property value correctly printed on the console.

To account for those programming languages that do not support the indexer syntax, the ManagementObject type offers an explicit GetPropertyValue method, inherited from its base type—ManagementBaseObject. This method, just like the indexer, takes the string property name as a parameter and returns the respective property value as object. Truth be told, the Indexer does nothing more internally than just call the GetPropertyValue method. Thus, the example above can be rewritten using the following syntax:

Console.WriteLine("ASPScriptDefaultNamespace : {0}", mo.GetPropertyValue("ASPScriptDefaultNamespace")); Console.WriteLine("ASPScriptEnabled : {0}", mo.GetPropertyValue("ASPScriptEnabled")); ...

This involves more typing when you compare it to the indexer syntax, and while some may argue that it adds clarity, it is my opinion that using this method with programming languages that naturally support indexers is overkill.

One thing you may have noticed about the example in Listing 2-1 is that it is easy to misspell the property name, which makes the indexer syntax fairly error prone. A better approach would be to enumerate the properties of an object and print out the values in some generic manner. The example in Listing 2-2 illustrates how to do this.

Listing 2-2: Enumerating Properties of Win32_WMISetting

using System; using System.Management; class PrintWMISettings { public static void Main(string[] args) { ManagementObject mo = new ManagementObject("Win32_WMISetting=@"); foreach( PropertyData pd in mo.Properties ) { Console.WriteLine("{0} : {1}", pd.Name, pd.Value ); } } }

Here, the ManagementObject type offers a public read-only property, called Properties, which it inherits from its ManagementBaseObject base type. When read, this property returns an object of type PropertyDataCollection. The PropertyDataCollection type by itself is not very interesting—it is just a collection type that implements the standard collection interfaces IEnumerable and ICollection. What is interesting are the contents of the PropertyDataCollection object that is associated with a given instance of the ManagementObject type.

The PropertyDataCollection is a collection of PropertyData type instances that are designed to hold all the information pertinent to a single property of a WMI object. Listing 2-2 demonstrates two of the properties of the PropertyData type: Name and Value. These properties hold the name and the value of the corresponding WMI object property.

Accessing System Properties

If you remember our discussion of WMI system properties in Chapter 1, you may notice that the output produced by the code in Listing 2-2 does not include any of these properties. The reason for this is that system properties are not stored in the same property data collection. Instead, they are in a separate collection that is designed to hold just the WMI system properties. You can access this collection through the SystemProperties property of the ManagementObject type that is inherited from the ManagementBaseObject. Just like the regular properties collection, this collection contains the objects of PropertyData type, but this time, these objects refer exclusively to the system properties of an arbitrary WMI class. Thus, enumerating and printing the system properties is no different from enumerating regular WMI properties:

foreach(PropertyData pd in mo.SystemProperties) { Console.WriteLine("{0} : {1}", qd.Name, qd.Value); }

Interestingly, the indexers, discussed earlier, as well as the GetPropertyValue method, are capable of retrieving a value of any property, regular or system. In fact, the GetPropertyValue method examines the property name, and depending on whether or not it starts with a double underscore, it retrieves the value from the SystemProperties or Properties collections respectively. Thus, the following code is perfectly valid and will work just fine:

Console.WriteLine("__PATH: {0}", mo["__PATH"]);

In addition to Name and Value, the PropertyData type exposes a few other properties that greatly simplify the analysis of WMI property data. If you compiled and ran any of the examples in Listing 2-1 or 2-2, you may have noticed that some of the property values of the Win32_WMISetting class were not printed correctly. For instance, instead of printing the value of the AutorecoverMofs property, our PrintWMISettings class prints System.String[]. The reason for it is simple—the ToString method implementation for complex data types, such as an array of strings, returns the name of the type rather than the value of the object. Since some of the WMI properties are arrays, the default ToString implementation does not quite work. The code example in Listing 2-3 shows how to fix this problem.

Listing 2-3: Enumerating Properties of Win32_WMISetting

using System; using System.Management; using System.Text; class PrintWMISettings { public static void Main(string[] args) { ManagementObject mo = new ManagementObject("Win32_WMISetting=@"); foreach( PropertyData pd in mo.Properties ) { Console.WriteLine("{0} : {1}", pd.Name, Format(pd) ); } } public static object Format(PropertyData pd) { if (pd.IsArray) { Array pa = (Array)pd.Value; StringBuilder sb = new StringBuilder(); sb.Append(Environment.NewLine); for(int i=pa.GetLowerBound(0); i

Here I added a public static Format function, which is called every time I need a property value printed. First it checks to see if the property value is an Array type using the IsArray property of the PropertyData object. For scalar properties, this function simply returns the property value, which is automatically converted to a string by the WriteLine method. If a property is determined to be of Array type, the Format function casts the property value to an Array type and iterates through individual elements of the array using its GetLowerBound, GetUpperBound, and GetValue methods. As an alternative to this, you can use the following code to enumerate the array elements:

foreach(object o in (Array)pd.Value) { sb.Append(o.ToString()); sb.Append(Environment.NewLine); }

In either case, for array properties, this function returns a new line–delimited list of property values, which it assembles using the StringBuilder type from the System.Text namespace. Interestingly, the return type of the Format function is an object rather than a string. Returning the object from the function helps to avoid checking for null property values; instead, the task of converting values to strings is delegated to the WriteLine method.

The PropertyData type contains a few other useful properties and the following snippet of code demonstrates all but one of them:

PropertyData pd = mo.Properties["AutorecoverMofs"]; Console.WriteLine("IsLocal: {0}", pd.IsLocal); Console.WriteLine("Origin: {0}", pd.Origin); Console.WriteLine("Type: {0}", pd.Type);

This example produces the following output:

IsLocal: True Origin: Win32_WMISetting Type: String

The IsLocal property returns a Boolean value (TRUE or FALSE) based on whether a particular property is declared in the current WMI class or one of its parents. As shown here, it returns a TRUE value for the AutorecoverMofs property of the Win32_WMISetting, but if it is invoked for the SettingID property, it will return a FALSE value because this property is declared in the parent class of Win32_WMISetting—CIM_Setting. The Origin property contains the name of the declaring class, which in the case of the AutorecoverMofs property, is Win32_WMISetting. For the SettingID property it will return the string "CIM_Setting". Finally, the Type property can be used to determine the CIM data type of the underlying WMI property. The Type property is of type CimType, which is an enumeration that describes all possible CIM types that are used for properties, and qualifier and method parameters. Table 2-1 lists all members of the CimType enumeration.

Table 2-1: CimType Enumeration

MEMBER

DESCRIPTION

Boolean

Boolean type

Char16

16-bit character type

DateTime

Date or time value in DMTF string date/time format:

yyyymmddHHMMSS.mmmmmmsUUU

where:

yyyymmdd—year/month/day

HHMMSS—hours/minutes/seconds

mmmmmm—microsecondss

UUU—sign ()/UTC offset

Object

Embedded object type

Real32

32-bit floating point number

Real64

64-bit floating point number

Reference

Reference to another object (string containing the path to another object)

SInt16

Signed 16-bit integer

SInt32

Signed 32-bit integer

SInt64

Signed 64-bit integer

SInt8

Signed 8-bit integer

String

String type

UInt16

Unsigned 16-bit integer

UInt32

Unsigned 32-bit integer

UInt64

Unsigned 64-bit integer

UInt8

Unsigned 8-bit integer

Retrieving Object and Property Qualifiers

The last property of the PropertyData type is a collection of WMI property qualifiers. A qualifier, briefly mentioned in Chapter 1, is a special designation that CIM uses to associate certain qualities with an arbitrary property. For instance, to identify a property as an object key, a Key qualifier is used. The Qualifiers property of the PropertyData type is a straightforward collection of the QualifierDataCollection type, which contains instances of the QualifierData type. Two of the most important properties that the QualifierData type exposes are Name and Value, which represent the name and the value of an arbitrary WMI property qualifier. To iterate through the collection of property qualifiers, you would use code similar to the following:

PropertyData pd = mo.Properties["AutorecoverMofs"]; foreach(QualifierData qd in pd.Qualifiers) { Console.WriteLine("{0} : {1}", qd.Name, qd.Value); }

As an alternative to this code, you can access the qualifier values directly using the GetPropertyQualifierValue method of the ManagementObject type, which is inherited from the ManagementBaseObject. For example, to print the value of the CIMTYPE qualifier for the AutorecoverMofs property of the Win32_WMISetting object, you may use the following code:

Console.WriteLine("CIM Type: {0}", mo.GetPropertyQualifierValue("AutorecoverMofs", "CIMTYPE"));

This method simply takes the property name and qualifier name parameters of type string and returns the value of the qualifier, packaged in an object.

Besides the Name and Value properties, the QualifierData type exposes five other properties, one of which, IsAmended, has to do with the localization capabilities that are built into WMI (these will be discussed in "Localization and Internationalization Issues" later in this chapter). The remaining four properties—IsLocal, IsOverridable, PropagatesToInstance, and PropagatesToSubclass—indicate whether the qualifier is declared in the current class, whether it can be overridden by its subclass, whether it automatically propagates to instances of the class, and whether it propagates to subclasses respectively.

WMI object properties are not the only elements of CIM that may have associated qualifiers. In fact, qualifiers are used extensively to describe the semantics of classes, instances, properties, methods, and method parameters. For example, CIM uses the Singleton qualifier to mark singleton classes as such. Conveniently, the ManagementBaseObject type and its subtype ManagementObject expose a Qualifiers property—a collection that contains the QualifierData objects that refer to the qualifiers defined for an instance of the WMI class. As is shown here, the qualifiers of an object can be iterated through just as easily as the property qualifiers:

ManagementObject mo = new ManagementObject("Win32_WMISetting=@"); foreach( QualifierData qd in mo.Qualifiers ) { Console.WriteLine("{0} : {1}", qd.Name, qd.Value ); }

This example will produce the following output:

dynamic : True Locale : 1033 provider : WBEMCORE Singleton : True UUID : {A83EF166-CA8D-11d2-B33D-00104BCC4B4A}

As you can see, the Singleton qualifier for the instance of Win32_WMISetting class is set to TRUE, thus marking the instance as a singleton.

If you wanted to enable direct access to the qualifier values, you would use the GetQualifierValue method of the ManagementBaseObject type as follows:

ManagementObject mo = new ManagementObject("Win32_WMISetting=@"); Console.WriteLine("Singleton: {0}", mo.GetQualifierValue("Singleton"));

This method takes a single parameter of type string—the name of the qualifier—and returns the qualifier value, packaged as an object. Be careful when you are calling this method because it will throw the ManagementException if the qualifier, corresponding to a given name, cannot be found in the qualifiers data collection. For example, if you attempt to retrieve the value of the Singleton qualifier for an instance of the Win32_Process class, you will not get the FALSE qualifier value; instead, the method invocation will result in the ManagementException because the Singleton qualifier is not defined for the Win32_Process class.

If you are an inquisitive reader, the preceding discussion of qualifiers may seem far from exhaustive. There is a good reason, however, for why I did not delve into the gory details here: discussing qualifiers in detail in the current context does not make much sense. Because qualifiers constitute an essential part of WMI class definitions, many of their features and qualities are best understood when talking about classes rather than objects. Therefore, I will present the detailed overview of WMI qualifiers later in this chapter, when I cover the features of ManagementClass type.

ManagementObject Analyzing the Relationships Between Objects

No WMI object exists in isolation. As we stated in Chapter 1, CIM is an object model that is composed of numerous elements that are related through a set of well-defined associations. Your ability to model and express the relationships between individual managed elements is very important because it is this information—information that describes the object associations—that many management applications rely on in order to function correctly. Windows Service Control Manager, for instance, is a classic example of a management application that requires detailed knowledge about the dependencies between Windows services.

The ManagementObject type features a couple of methods that make the analysis of the object relationships easy and painless. These methods— GetRelated and GetRelationships—allow you to enumerate the instances, related to a given object, and analyze the associations between the individual objects.

Enumerating Related Objects

The following code illustrates how you can retrieve all the objects that are related to an instance of the Win32_Service class, which represents WMI WinMgmt service.

ManagementObject mo = new ManagementObject(@" Win32_Service='WinMgmt'"); foreach(ManagementObject o in mo.GetRelated()) { Console.WriteLine(o["__PATH"]); }

This snippet of code first binds to an instance of the Win32_Service class that is identified by its name, "WinMgmt", which is the name of the WMI service. Then it calls the GetRelated method of the ManagementObject type to retrieve a collection of ManagementObject instances, each of which represents WMI objects that are related to WMI Service. Finally, it traverses the resulting collection and prints the value of the __PATH property of each object on the console.

This code should be very straightforward. The only thing that you may not readily understand is why I chose to print the __PATH property rather than the Name property. Everything becomes even more clear once you look at the output that this code produces:

\BCK_OFFICE ootcimv2:Win32_WMISetting=@ \BCK_OFFICE ootcimv2:Win32_ComputerSystem.Name=" BCK_OFFICE" \BCK_OFFICE ootcimv2:Win32_Service.Name=" RpcSs"

Now you can see that the GetRelated method retrieves all the objects that are somehow related to the WinMgmt service, not just other instances of Win32_Service class. That is why I used the system __PATH property— all WMI classes have it. If I had attempted to use the Name property (or any other nonsystem property, for that matter) it would have resulted in a ManagementException because the Win32_WMISetting object does not have such a property.

Often you will want to retrieve not just all objects that are related to a given instance, but those that play a certain role in a relationship. The ability to determine the role of an object participating in a relationship may be very handy, especially when you analyze the dependencies between the individual instances of WMI classes. For instance, consider the task of enumerating all Windows services, on which WinMgmt depends. You may attempt to solve this problem by using an overloaded version of the GetRelated method, which takes a single string parameter—the name of the class of related objects:

ManagementObject mo = new ManagementObject(@" Win32_Service='WinMgmt'"); foreach(ManagementObject o in mo.GetRelated("Win32_Service")) { Console.WriteLine(o["__PATH"]); }

It may look like this code solves the problem. Just one object of type Win32_Service is identified—the Remote Procedure Call (RPC) service, on which WinMgmt is obviously dependent:

\BCK_OFFICE ootcimv2:Win32_Service.Name=" RpcSs"

However, if I change the code to retrieve all objects related to the RPC service

ManagementObject mo = new ManagementObject(@" Win32_Service='RpcSs'"); foreach(ManagementObject o in mo.GetRelated("Win32_Service")) { Console.WriteLine(o["__PATH"]); }

you will see that the output includes WinMgmt Service:

\BCK_OFFICE ootcimv2:Win32_Service.Name=" ProtectedStorage" \BCK_OFFICE ootcimv2:Win32_Service.Name=" Schedule" \BCK_OFFICE ootcimv2:Win32_Service.Name=" TlntSvr" \BCK_OFFICE ootcimv2:Win32_Service.Name=" WinMgmt" ...

There is only one conclusion that you can draw from this: both versions of the GetRelated method retrieve related objects regardless of the direction of the relationship and the object role in the relationship.

Fortunately, the ManagementObject type offers another overloaded version of the GetRelated method that provides a finer degree of control over the output of the method. This version of the method is fairly complex and has the following signature:

public ManagementObjectCollection GetRelated( string relatedClass, string relationshipClass, string relationshipQualifier, string relatedQualifier, string relatedRole, string thisRole, bool classDefinitionsOnly, EnumerationOptions options );

where the parameters are defined as follows:

This version of the GetRelated method easily solves the problem of enumerating the dependencies for WinMgmt service, as shown here:

ManagementObject mo = new ManagementObject(@" Win32_Service='WinMgmt'"); foreach(ManagementObject o in mo.GetRelated("Win32_Service", "Win32_DependentService",null,null," Antecedent"," Dependent",false,null)) { Console.WriteLine(o["__PATH"]); }

While this may look a bit complex at first, it is really not. In order to understand what is going on here, take a look at the definition for the Win32_DependentService class: it is an association that ties together Win32_Service classes as shown here:

class Win32_DependentService : CIM_ServiceServiceDependency { Win32_BaseService ref Antecedent = NULL; Win32_BaseService ref Dependent = NULL; };

This class has two properties, marked as references: Antecedent and Dependent. Both of these properties point to instances of the Win32_BaseService class (the parent class of Win32_Service). The Antecedent property refers to a "master" service, and the Dependent property refers to a service that is dependent on the master. Therefore, the preceding code example simply requests all instances of the Win32_Service class that are related to the current instance through an association of type Win32_DependentService such that the current instance is a dependent and the requested instance is a "master." If you wanted to do the opposite—identify all the services that are dependent on a given service—you would reverse the relatedRole and thisRole parameters as follows:

ManagementObject mo = new ManagementObject(@" Win32_Service='RpcSs'"); foreach(ManagementObject o in mo.GetRelated("Win32_Service", "Win32_DependentService",null,null," Dependent"," Antecedent ",false,null)) { Console.WriteLine(o["__PATH"]); }

For complex scenarios, you can filter the output of the GetRelated method even further by supplying the values for the relatedQualifier and relationshipQualifier parameters, which allow you to include just the instances of those classes where respective qualifiers are defined.

Another interesting and not quite obvious feature of the GetRelated method is its ability to enumerate related objects in a generic fashion using parent class names. Consider the following statement: a directory may contain files as well as other directories. In WMI, a data file is represented by the CIM_DataFile class, while a directory is represented by Win32_Directory. The containment relationship between a directory and a file is expressed via CIM_DirectoryContainsFile association class, which has two properties— GroupComponent and PartComponent, pointing to directory and file objects respectively.

The relationship between a directory and its subdirectories is represented by the Win32_SubDirectory association class with the same properties (GroupComponent and PartComponent) that point to the directory and subdirectory objects respectively. Thus, if you attempt to list the contents of an arbitrary directory, like so

ManagementObject mo = new ManagementObject(@" Win32_Directory='C:TEMP'"); foreach(ManagementObject o in mo.GetRelated("CIM_DataFile", null,null,null," PartComponent"," GroupComponent",false,null)) { Console.WriteLine(o["__PATH"]); }

you will only get a list of data files and any subdirectories will be ignored since the relatedClass parameter limits the scope to objects of CIM_DataFile class. However, since both CIM_DataFile and Win32_Directory share the same parent class, CIM_LogicalFile, the code can be rewritten as follows:

ManagementObject mo = new ManagementObject(@" Win32_Directory='C:TEMP'"); foreach(ManagementObject o in mo.GetRelated("CIM_LogicalFile", null,null,null," PartComponent"," GroupComponent",false,null)) { Console.WriteLine(o["__PATH"]); }

Since files and directories are, in fact, instances of subclasses of the CIM_LogicalFile, this last code example will correctly list the contents of C:TEMP directory, showing not only files, but subdirectories as well.

The last parameter of the GetRelated method, the object of type EnumerationOptions, is fairly interesting. Its purpose is to control certain aspects of the method's behavior, most of which are performance-related. Table 2-2 lists all the public properties of EnumerationOptions type.

Table 2-2: EnumerationOptions Properties

PROPERTY

DESCRIPTION

BlockSize

Integer that controls how WMI returns results. When retrieving related instances as a collection of objects, WMI returns objects in groups of size, specified by this parameter.

DirectAccess

Boolean value that indicates whether a direct access to the provider is requested for a given class, without any regard for its superclasses or subclasses.

EnsureLocatable

Boolean value that indicates whether the returned objects have locatable information. If set to true, this ensures that the system properties, such as __PATH, __RELPATH, and __SERVER, are populated. This property is ignored when enumerating related objects.

EnumerateDeep

Boolean value that indicates whether subclasses of a given class should be included in the result set.

PrototypeOnly

Boolean value that controls partial-instance operations. Some classes have dynamic resource-consuming properties that are expensive to retrieve. This property controls whether all or some properties of a WMI class are returned as a result of enumeration.

ReturnImmediately

Boolean value that controls whether an operation is performed synchronously or asynchronously. If set to true, this property causes the method to return immediately and the actual retrieval of results takes place when the returned collection is iterated through.

Rewindable

Boolean value that indicates whether the returned object collection may be enumerated more than once. If set to false, the underlying objects in a collection become unavailable as soon as the enumerator is released.

UseAmendedQualifiers

Boolean value that indicates whether the returned objects should contain localized data. Localization is discussed later in this chapter.

Timeout

Value of type TimeSpan used to set the timeout value for the operation. When a method returns a collection of objects (as the GetRelated method does) the timeout applies to iterating through the collection, rather than just to the method invocation itself. This property is inherited from the ManagementOptions type.

The EnumerationOptions type has a single parameterized constructor, shown here, which allows you to set all of the object properties at once:

public EnumerationOptions ( ManagementNamedValueCollection context , TimeSpan timeout , int blockSize , bool rewindable , bool returnImmediately , bool useAmendedQualifiers , bool ensureLocatable , bool prototypeOnly , bool directRead , bool enumerateDeep );

Invoking this constructor is trivial, and the only interesting thing about it is its first parameter—the context parameter of the type ManagementNamedValueCollection. The ManagementNamedValueCollection type is just a simple container that is designed to hold name-value pairs. These namevalue pairs are then used to pass additional information to WMI data providers in those cases where standard method parameters are not sufficient. For a SNMP provider, for instance, you may need to pass community strings; this is where the context parameter comes in handy.

You can use the context parameter to initialize the Context property of the ManagementOptions type, which is a parent type for EnumerationOptions and other options types. This parameter provides an excellent generic mechanism for passing provider-specific data as part of method invocations, and it is used extensively by types in the System.Management namespace.

Analyzing Associations Between Objects

Besides inspecting all objects related to a given instance, sometimes you may find it useful to look at the associations that actually link the objects together. As you may remember from Chapter 1, associations are classes that may include properties other than just references to the objects that are related through an association. For instance, the CIM_ProcessExecutable association class, which was designed to express the relationship between an operating system process (Win32_Process) and its components (executable files and dynamic link libraries), has the following definition:

class CIM_ProcessExecutable : CIM_Dependency { CIM_DataFile ref Antecedent = NULL; CIM_Process ref Dependent = NULL; uint32 GlobalProcessCount; uint32 ProcessCount; uint32 ModuleInstance; uint64 BaseAddress; };

In addition to the Antecedent and Dependent properties, which refer to a process and its associated components respectively, the CIM_ProcessExecutable class has a number of other properties:

You will most likely find that CIM_ProcessExecutable associations are worth looking at because they contain a wealth of useful information that is unavailable anywhere else. The simplest way to enumerate associations of a class is to use the parameterless version of GetRelationships method of the ManagementObject type. For example, to list all the associations of a process that are identified by its process ID or handle, you can use the following code:

ManagementObject mo = new ManagementObject("Win32_Process=269"); foreach(ManagementObject o in mo.GetRelationships()) { Console.WriteLine(o["__PATH"]); }

Unfortunately, this code has the same problem as the earlier example for Win32_Service dependencies: it lists all associations regardless of their type. Since a process may be associated with not only executable files and DLLs, but also with the computer system it is running on, and possibly with the threads that comprise the process and other elements, the output of this code fragment is not what you want. To remedy this problem you can use an overloaded version of the GetRelationships method, which takes a single string parameter—the name of the associating class—which should be included in the output:

ManagementObject mo = new ManagementObject("Win32_Process=269"); foreach(ManagementObject o in mo.GetRelationships("CIM_ProcessExecutable")) { Console.WriteLine(o["__PATH"]); Console.WriteLine("GlobalProcessCount: {0}", o["GlobalProcessCount"]); Console.WriteLine("ProcessCount : {0}", o["ProcessCount"]); Console.WriteLine("ModuleInstance : {0}", o["ModuleInstance"]); Console.WriteLine("BaseAddress : {0}", o["BaseAddress"]); }

For more obscure scenarios in which you want to have finer control over the output, the ManagementObject type provides another overload of the GetRelationships method:

public ManagementObjectCollection GetRelationships ( string relationshipClass , string relationshipQualifier , string thisRole , bool classDefinitionsOnly , EnumerationOptions options );

where the parameters are defined as follows:

ManagementObject Modifying Management Data

If you spend enough time wandering around the WMI object model you will eventually attempt to modify some of the WMI object properties. Although modifying object properties is theoretically possible, there is a caveat of which you should be aware.

Identifying Writable Properties

The problem is that not all properties are writable. One approach you can use to identify writable properties is to inspect the property definitions for write qualifiers. In general, a write qualifier indicates that the property is modifiable, however, the absence of this qualifier does not necessarily mean that the property is read-only. It turns out that WMI qualifiers act in an advisory capacity so that the restrictions associated with a particular qualifier are not enforced by WMI. In fact, when delegating a request for an operation to a data provider, WMI completely ignores any qualifiers, and it is up to a provider to carry out the request. This may change in the future releases of WMI. For instance, CIMOM may start checking the class and property qualifiers before it dispatches the requests to data provider to ensure that none of the constraints, imposed by qualifiers, are accidentally violated.

Thus, identifying the writable properties is somewhat empiric—you may try to change the property value and see if a provider completes the operation. In general, WMI system properties are read-only, except for __CLASS, which is writable, but this is only true when you are programmatically creating a class. The nonsystem properties of the vast majority of WMI classes are also mostly read-only; the most notable exceptions are listed in Table 2-3.

Table 2-3: Writable Properties

CLASS NAME

PROPERTY NAME

DESCRIPTION

Win32_ComputerSystem

SystemStartupDelay

Number of seconds before the computer's default operating system is automatically started

SystemStartupSetting

Index of boot.ini entry, which contains the computer's default operating system

Win32_Environment

UserName

Identifier of a user, to which a given environment variable applies

Name

Name of the Windows environment variable

VariableValue

Value of the Windows environment variable

Win32_LogicalDisk

VolumeName

Name of the volume on a logical disk

Win32_OperatingSystem

ForegroundApplicationBoost

Number of points to be added to an application's priority when that application is brought to the foreground

QuantumLength

Number of clock ticks before an application is swapped out

QuantumType

Number that determines whether the system uses fixed or variable length quantums so that foreground applications receive longer quantums

Win32_OSRecoveryConfiguration

AutoReboot

Boolean value that specifies whether a system is automatically rebooted during a recovery operation

DebugFilePath

Path to the debug file that is written if WriteDebugInfo is set to true

KernelDumpOnly

Boolean value that indicates whether only kernel debug information will be written to the debug log file

OverWriteExistingDebugFile

Boolean value that determines whether or not to overwrite an existing debug file

SendAdminAlert

Boolean value that determines whether or not an alert message is sent to the system administrator in the event of an operating system failure

WriteDebugInfo

Boolean value that determines whether or not debug information will be written to a file

WriteToSystemLog

Boolean value that determines whether system failure information will be written to a system log file

Win32_PageFileSetting

Name

String that contains the name of the page file

InitialSize

Number that specifies the size (in MB) of the page file when it is first created

MaximumSize

Number that specifies the maximum allowed size (in MB) of the page file

Win32_Registry

ProposedSize

Number that specifies the maximum size (in MB) of the registry

One of the classes, besides those mentioned in Table 2-3, that has many modifiable properties is Win32_WMISetting. In fact, all of its properties, with the exception of AutorecoverMofs, Caption, BuildVersion, Description, SettingID, DatabaseDirectory, DatabaseMaxSize, InstallationDirectory, LastStartupHeapPreallocation, and MofSelfInstallDirectory, are writable. You will find that having read-write access to WMI settings through the instance of Win32_WMISetting class is very useful because it allows you to programmatically alter the WMI configuration in a controlled manner—a much better alternative than changing the system registry.

Modifying Properties

Modifying the object properties is surprisingly easy: you may simply assign the properties, which are contained in the ManagementObject Properties collection, to the values of your choice. The following fragment, for instance, changes the value of the Win32_WMISetting object's BackupInterval property to 60 seconds:

ManagementObject mo = new ManagementObject("Win32_WMISetting=@"); mo["BackupInterval"] = 60;

For those programming languages that do not support the indexer syntax, the ManagementObject type offers an explicit SetPropertyValue method, which it inherits from its base type, ManagementBaseObject:

ManagementObject mo = new ManagementObject("Win32_WMISetting=@"); mo.SetPropertyValue("BackupInterval", 60);

This method takes two parameters: the name of the property and the object that represents the new property value. In fact, when setting the property value via the indexer as in the example above, the indexer set method internally calls the SetPropertyValue method to carry out the actual update.

Unfortunately, both code examples above, although absolutely correct, do not have the effect that you want—if you check the registry following the execution of either of these fragments, you will notice that the BackupInterval value has not changed. The problem is that, from the WMI prospective, the instance of the WMI object is always in process, or in other words, a client application always operates upon its own local copy of an object. Thus, a read operation always retrieves a property value from a local copy, and a write request always updates the same local copy of the object. In order to propagate the changes to CIM Repository and potentially to a respective data provider, you have to "commit" them. To commit changes to WMI, you must have your application invoke the Put method of the ManagementObject after any update it performs on the instance:

ManagementObject mo = new ManagementObject("Win32_WMISetting=@"); mo["BackupInterval"] = 60; mo.Put();

The fact that WMI always operates on a local copy of an object creates a curious problem. Let's say the current setting of the BackupInterval property of the Win32_WMISetting is 30 minutes (this is the default). What do you think happens when the following code snippet is executed?

ManagementObject mo1 = new ManagementObject("Win32_WMISetting=@"); ManagementObject mo2 = new ManagementObject("Win32_WMISetting=@"); Console.WriteLine("BackupInterval (before update) : {0}", mo2["BackupInterval"]); mo1["BackupInterval"] = 60; mo1.Put(); Console.WriteLine("BackupInterval (after update) : {0}", mo2["BackupInterval"]);

The output may not be what you expect:

BackupInterval (before update) : 30 BackupInterval (after update) : 30

Here you see that even though the changes are committed to WMI with the Put method, the value of the BackupInterval property is still being taken from the local copy of the Win32_WMISetting instance, pointed to by the mo2 object reference. And since this local copy has not changed, the value of the BackupInterval remains the same! Potentially, this could lead to all kinds of integrity issues, such as lost updates and various race conditions.

Unfortunately, unlike most database systems, WMI does not implement any locking mechanism, which might have prevented these kinds of problems. Apart from adding complexity, a locking mechanism for WMI would be impractical for a few reasons. First, locking is notoriously expensive and often results in excessive resource consumption. Second, the fundamental concept behind WBEM, and WMI in particular, is its ability to operate via the Internet or a wide area network (WAN) where the principles of stateless programming have to be followed religiously in order to ensure adequate performance. Implementing any kind of locking mechanism in a stateless environment is complicated, to say the least. Thus, the general philosophy of WMI is to limit the number of modifiable properties to a bare minimum. Instead, in order to alter the state of WMI objects, you should invoke object methods where appropriate. The details of method invocation will be discussed later in "Invoking WMI Object Methods."

Controlling the Scope of Modification

Contrary to what you may expect, the key properties that uniquely identify an object within WMI are not necessarily write-protected. Consider the following scenario:

ManagementObject mo = new ManagementObject(@"__NAMESPACE='Applications'"); mo["Name"] = "NewNamespace"; mo.Put();

This code runs just fine, but the outcome is somewhat unexpected. Instead of renaming the Applications namespace, it creates a brand new namespace, NewNamespace. As confusing as it may look, this behavior is actually logical. The value of the object key property is what uniquely identifies a given instance to WMI. Thus, when the changes are committed, as far as WMI is concerned, the object it is supposed to operate on is identified by the name NewNamespace. Since no such object exists, WMI creates a brand new one.

Although this is a pretty cool way to create new instances of WMI classes, such behavior is a bit unexpected and may lead to obscure and hard-to-find bugs. To reduce the chances of accidentally creating new objects, use the overloaded version of the Put method that the ManagementObject type offers. This version takes a single parameter of type PutOptions. The PutOptions type has a few properties, one of which, Type, is of immediate interest. The data type of this property is the enumeration PutType, which has three members, described in Table 2-4.

Table 2-4: Members of the PutType Enumeration

PUTTYPE MEMBER

DESCRIPTION

CreateOnly

Allows only those operations that result in creating a new object/class

UpdateOnly

Allows only those operations that result in an existing object being updated

UpdateOrCreate

Allows any kind of operation that results in creating a new object or updating the existing one

The Type property of the PutType object can be set via its constructor or it can be assigned after the object is created. In addition to the default parameterless constructor, there are two overloaded versions:

public PutOptions ( ManagementNamedValueCollection context , TimeSpan timeout , bool useAmendedQualifiers , PutType putType ); public PutOptions ( ManagementNamedValueCollection context );

Both constructors take the context parameter, which is used to initialize the Context property of the base type, ManagementOptions. The first version also allows you to set the timeout value of the ManagementOptions type, the localization property useAmendedQualifiers, and, finally, the Type property of the PutOptions type.

To illustrate the effect of restricting the scope of the modifications performed on an instance of WMI class, I have altered the previous code example as follows:

ManagementObject mo = new ManagementObject(@"__NAMESPACE='Applications'"); mo["Name"] = "NewNamespace"; PutOptions po = new PutOptions(); po.Type = PutType.UpdateOnly; mo.Put(po);

Instead of creating a new namespace NewNamespace, this code will throw the ManagementException because the PutOptions object that controls the Put operation does not allow a new object to be created.

Creating New Objects

In those cases where you want to create a new object instance, you should use the Clone method of the ManagementObject type. When you invoke this method on an existing instance of a WMI class, it stamps out a brand new object, which looks exactly like the original one. However, you should note that the Clone method does not save the resulting object to CIM Repository and you must follow it with the Put method:

ManagementObject mo = (ManagementObject) new ManagementObject(@"__NAMESPACE='Applications'").Clone(); mo["Name"] = "NewNamespace"; mo.Put();

The cast to the ManagementObject is necessary since the Clone method is an override of the Clone of the System.Object base type and, by definition, it returns the result of type System.Object.

Again, make sure to modify the key properties of the cloned object before you save it to the repository. If you fail to do so, the original object will be overwritten. To avoid this, use the overloaded version of the Put method, which takes the parameter of type PutOptions as follows:

ManagementObject mo = (ManagementObject) new ManagementObject(@"__NAMESPACE='Applications'").Clone(); mo["Name"] = "NewNamespace"; PutOptions po = new PutOptions(); po.Type = PutType.CreateOnly; mo.Put(po);

This will ensure that only a creation operation succeeds, since WMI will check to see whether an instance of the class with the same key properties already exists and throw an exception if it does.

Yet another method you can use to create new instances of an object, although in a fairly restrictive fashion, is CopyTo. In its simplest form, CopyTo takes a single string parameter that represents the namespace path for a new instance. However, it is important that you understand that CopyTo only lets you copy objects between namespaces; it does not let you change any of the object properties or the class to which the object belongs. As tempting as the following code may look, it will not work—in fact, it will do nothing:

ManagementObject mo = new ManagementObject(@"\. oot:__NAMESPACE='Applications'"); mo.CopyTo(@"\. oot:__NAMESPACE='NewNamespace'");

In this example, it turns out that the class name and the object identity portions of the path that are passed as parameters to the CopyTo method are completely ignored and only the namespace path is used to perform the copy operation. Therefore, the following code will successfully copy the Applications namespace from oot to ootCIMV2:

ManagementObject mo = new ManagementObject(@"\. oot:__NAMESPACE='Applications'"); mo.CopyTo(@"\. ootCIMV2");

The CopyTo method conveniently returns the object of type ManagementPath, which represents the full object path of a newly created instance:

ManagementPath mp = new ManagementObject( @"\. oot:__NAMESPACE='Applications'").CopyTo(@"\. ootCIMV2"); Console.WriteLine(mp.Path);

This code will produce the following output, which indicates that the Applications namespace has been successfully copied to the desired location:

\. ootCIMV2:__NAMESPACE.Name=" Applications"

When you are copying instances of nonsystem classes between namespaces, you must make sure that the class definition of the object exists in the target namespace. For instance, the following code will fail, since the definition for Win32_Process does not exist in oot namespace:

ManagementObject mo = new ManagementObject(@" Win32_Process=269"); mo.CopyTo(@"\. oot");

Finally, to satisfy all developers' tastes, the ManagementObject type offers several overloaded versions of the CopyTo method, three of which are shown below:

public ManagementPath CopyTo ( ManagementPath path ); public ManagementPath CopyTo ( string path , PutOptions options ); public ManagementPath CopyTo ( ManagementPath path , PutOptions options );

Besides providing you with the additional freedom of using the object of type ManagementPath to specify the destination path rather than the string parameter, two of these methods also take a PutOptions argument, which may help ensure the desired semantics of the copy operation.

Removing Objects

If you bothered to compile and execute any of the code above, you must have a few newly created object instances dangling around your WMI namespaces. To clean these up, you may want to use a Delete method of the ManagementObject type:

ManagementObject mo = new ManagementObject(@"\. ootCIMV2:__NAMESPACE='Applications'"); mo.Delete();

The delete operation is atomic and does not have to be followed by the Put method call. However, since Delete affects the CIM Repository rather than the local copy of the object, the object reference is not automatically invalidated following the Delete:

ManagementObject mo = new ManagementObject(@"\. ootCIMV2:__NAMESPACE='Applications'"); mo.Delete(); foreach(PropertyData pd in mo.Properties) { Console.WriteLine("{0} {1}", pd.Name, pd.Value); }

The preceding code works perfectly, despite the fact that the object no longer exists in the CIM Repository. To avoid all kinds of hard-to-find bugs, it is probably the best to set the object reference to null as soon as Delete completes.

The ManagementObject type offers a few overloaded versions of Delete, one of which takes a parameter of type DeleteOptions. The DeleteOptions type is a descendant of the ManagementOptions type, and in the current release of FCL, it does not have any properties of its own. Its only purpose is to hold the provider-specific context data in those cases where such data is necessary.

Comparing Objects

After you spend some time experimenting with new and existing object instances, you will eventually find yourself in dire need of some way to compare them; you can use such a comparison mechanism to determine whether two distinct instances of the ManagementObject type refer to the same WMI object. The basic comparison function, provided by the ManagementObject type, is the CompareTo method, which is inherited from its base type ManagementBaseObject. This method takes two parameters: an object of type ManagementBaseObject to compare the current instance to, and a value of type ComparisonSettings, which is an enumeration type. Depending on the outcome of the comparison operation, this CompareTo method returns either the Boolean TRUE or FALSE value. Normally, the comparison operation takes into account all elements that constitute an object: its class, location, values, and the types of its properties and qualifiers. In certain cases, however, you may find it useful to ignore the object's location or qualifier values. This is exactly what the last parameter of CompareTo method, the comparison mode value of type ComparisonSettings, achieves. The ComparisonSettings type is an enumeration with seven members, listed in Table 2-5.

Table 2-5: Members ComparisonSettings Enumeration

COMPARISONSETTINGS MEMBER

DESCRIPTION

IncludeAll

This comparison mode takes into account all elements of an object, including its location, class, qualifiers and property values.

IgnoreCase

This mode enables a case-insensitive comparison of property and qualifier values. The names of properties and qualifies are always compared in a case-insensitive manner regardless of this flag.

IgnoreClass

This mode enables the comparison of instance information only, assuming that the objects being compared belong to the same class. Although very efficient in terms of performance, this flag may cause the CompareTo method to produce undefined results if the objects do not belong to the same class.

IgnoreDefaultValues

This mode causes the comparison to ignore the default property values. It is not applicable to instance comparison operations.

IgnoreFlavor

This mode ignores qualifier flavors such as propagation and override restrictions, although the qualifiers are still taken into account.

IgnoreObjectSource

This mode ignores the location of the objects—for instance, the namespace and the server they reside on.

IgnoreQualifiers

This mode causes the qualifiers to be completely ignored by the comparison operation.

To illustrate how the CompareTo method works, I am going to compare the instance of __NAMESPACE class, named Applications (which I created earlier with the CopyTo method underneath the ootCIMV2), against the original instance of the Applications namespace in oot:

ManagementObject mo1 = new ManagementObject(@"\. oot:__NAMESPACE='Applications'"); ManagementObject mo2 = new ManagementObject(@"\. ootCIMV2:__NAMESPACE='Applications'"); Console.WriteLine(mo1.CompareTo(mo2, new ComparisonSettings()));

This code will print the false value on the console because the two namespace instances, although absolutely identical, reside in different namespaces. If you want to ignore the location of the objects, you would modify the code as follows:

ManagementObject mo1 = new ManagementObject(@"\. oot:__NAMESPACE='Applications'"); ManagementObject mo2 = new ManagementObject(@"\. ootCIMV2:__NAMESPACE='Applications'"); Console.WriteLine( mo1.CompareTo(mo2, ComparisonSettings.IgnoreObjectSource));

You can combine the members of the ComparisonSettings enumeration to achieve the effect that you desire. For instance, if I have two instances of the __NAMESPACE class—\rootCIMV2:__NAMESPACE='applications' and \root:__NAMESPACE='Applications'—and the values of their Name properties differ only in case, the following code fragment will print True on the console:

ManagementObject mo1 = new ManagementObject(@"\. oot:__NAMESPACE='Applications'"); ManagementObject mo2 = new ManagementObject(@"\. ootCIMV2:__NAMESPACE='applications'"); Console.WriteLine( mo1.CompareTo(mo2, ComparisonSettings.IgnoreObjectSource | ComparisonSettings.IgnoreCase));

In addition to the CompareTo method, the ManagementBaseObject, which is a base type for the ManagementObject, overrides the Equals method of the System.Object type. The Equals method is implemented so that it calls the CompareTo method internally, passing zero as a comparison mode. Since zero is an equivalent of the ComparisonSettings.IncludeAll member, the following two code snippets produce identical results:

mo1.Equals(mo2);

or

mo1.CompareTo(mo2, ComparisonSettings.IncludeAll);

Finally, if you decide to compare two instances of the ManagementObject type using a conventional comparison operator, you have to keep in mind that neither the ManagementBaseObject nor the ManagementObject type provide an overloaded version of this operator, thus such an operation will be treated as a reference rather than an object comparison:

ManagementObject mo1 = new ManagementObject(@"\. oot:__NAMESPACE='Applications'"); ManagementObject mo2 = new ManagementObject(@"\. oot:__NAMESPACE='Applications'"); Console.WriteLine(mo1 == mo2);

This code will print False on the console, since mo1 and mo2 are two distinct object references, even though they point to the same WMI object.

ManagementClass Analyzing the Schema

The CIM object model is complex and programming against it often requires an ultimate understanding of WMI schema—familiarity with class, property, and method semantics as well as detailed knowledge of inter-class relationships.

To facilitate the schema analysis, the System.Management namespace of the FCL offers a ManagementClass type that is designed to represent the most fundamental element of CIM—a WMI class. The ManagementClass type is a subtype of the ManagementObject, as I mentioned earlier in this chapter, and as such, it supports much of the same or similar functionality as the ManagementObject. Conceptually, there are also similarities between the ManagementObject and the ManagementClass since both these types represent some aspects of an arbitrary managed element, depicted by some WMI object. This might be why the designers of the System.Management namespace chose to use the ManagementObject type as a parent for the ManagementClass. However, the purpose of the ManagementClass type is very different from that of ManagementObject; rather than embodying an instance of WMI class, the ManagementClass type characterizes the metadata associated with the instance, or, simply put, its class definition. Thus, the purists of object-oriented design would probably argue that the ManagementClass is not really a ManagementObject. There is some truth to this, and, perhaps a better solution would have been to subclass both ManagementObject and ManagementClass from a common base type—ManagementBaseObject—so that all their common functionality would have been factored into the shared parent type. However, despite this minor design deficiency, the ManagementClass type does its job just fine.

Constructing a ManagementClass Object

Creating an object of type ManagementClass is very similar to creating an instance of the ManagementObject type. In addition to a default parameterless constructor, which creates an unbound instance, the ManagementClass type offers a few other constructors. These, which are just like the constructors of the ManagementObject type, take either a string parameter or an object of type ManagementPath, which represents the respective class path:

ManagementClass mc = new ManagementClass(@"\BCK_OFFICE ootCIMV2:Win32_WMISetting");

or

ManagementPath mp = new ManagementPath(); mp.Server = "BCK_OFFICE"; mp.NamespacePath = @" ootCIMV2"; mp.ClassName = "Win32_WMISetting"; ManagementClass mc = new ManagementClass(mp);

The only difference between this and constructing an instance of a ManagementObject type here is that the path is a class path rather than the object path, and as such, it does not include the object identity component. In fact, an attempt to create an instance of a ManagementClass type using an object path will fail, as shown in the following:

ManagementClass mc = new ManagementClass(@"\BCK_OFFICE ootCIMV2:Win32_WMISetting=@");

The preceding code will throw an ArgumentOutOfRange exception since the constructor is invoked with an object rather than a valid class path parameter.

Retrieving Class Properties

Once created and bound to a respective WMI class definition, an instance of the ManagementClass type provides a convenient vehicle for analyzing the class properties, qualifiers, and methods. Enumerating properties of a WMI class, for instance, is no different from enumerating the properties of an object:

ManagementClass mc = new ManagementClass("Win32_WMISetting"); foreach( PropertyData pd in mc.Properties ) { Console.WriteLine("{0} : {1}", pd.Name, pd.Value ); } }

If you compile and execute this code, you may notice that all the properties of the Win32_WMISetting class, except for one—ASPScriptDefaultNamespace—are empty. This should come as no surprise because the code above operates on a class or schema element rather than on a living instance of an object. Since classes are templates for instances and have no state on their own, in most cases, class properties will have no associated values. There are, however, exceptions, as in the case of the ASPScriptDefaultNamespace property of the Win32_WMISetting. This property has a default value assigned to it as part of the class definition:

class Win32_WMISetting : CIM_Setting { ... string ASPScriptDefaultNamespace = "\\root\cimv2"; ... }

When such a default value is assigned to a property of a class, all the instances of the class, when created, will have their respective properties set to this default value, unless the instance property value is explicitly modified. The default property values, which are specified within the WMI class definition, are accessible through the Value property of the PropertyData object that is contained in the properties collection of the ManagementClass.

The Value property is the only field that has a different meaning depending on whether it represents the property value of an object or of a class. All other members of the PropertyData type follow exactly the same semantics for the ManagementClass as they do for the ManagementObject.

Accessing Method Definitions

Another constituent element of the WMI class definition is a method. A method represents an action that can be carried out either on an instance of the WMI class, or on the entire class of objects. Not all WMI classes define methods but those that do often employ methods as a primary tool for manipulating the object data. In order to create an operating system process through WMI, for instance, it is not enough to instantiate the object of Win32_Process class. Instead, you must have the management application call a static Create method of the Win32_Process. By the same token, just deleting an instance of Win32_Process does not terminate the associated process; instead, you should set up the application so it uses the Terminate method of the Win32_Process object.

The fact that a class declares a method does not automatically mean that a data provider supplies an implementation for that method. When subclassing is being performed, method definitions are always being passed from the superclass to its children, but the actual method implementations are not necessarily being inherited. Typically, each method that has an implementation within a class must have the Implemented qualifier associated with it. If this qualifier is not present, this indicates that the method has no implementation. Therefore, if a subclass redeclares a method that it inherited from its superclass and it omits the Implemented qualifier, the method implementation from the superclass will be used at runtime.

Similar to the methods of a class that was built using one of the modern object-oriented programming languages such as C#, WMI methods can have either static or instance methods. Static methods operate on the WMI class rather than its instance, and therefore, they can only be invoked through the class rather than an instance reference. An example of a static method is the Create method of the Win32_Process class, which is used to launch new operating system processes. Instance methods, on the other hand, need an instance of the WMI class on which to operate. Thus, the Terminate method of the Win32_Process class, which is used to kill a given operating system process, can only be called for a valid instance of the Win32_Process class, which represents a process to be terminated. In WMI, static methods are designated by the Static qualifier; the absence of this qualifier indicates that a method applies to an instance rather than a class.

A method definition consists of three parts: the method name, the method return type, and the method parameters. Although the name of the method and its return type are fairly self-explanatory, method parameters deserve some explanation. In general, defining parameters for a method is somewhat similar to defining the properties of a class—each parameter has a name and an associated data type and the name and the type are subject to the same restrictions as those of class properties (for a list of CIM data types see Table 2-1). Unlike properties, however, parameters have some additional qualities.

First, all parameters are categorized as input, output, or input-output. In WMI, this is designated by In, Out, or In, Out qualifiers respectively.

Second, parameter lists of a particular method often have a certain order. To enforce the ordering and uniquely identify each method parameter, the WMI MOF compiler automatically adds the ID qualifier to each parameter definition within a method unless the ID attribute is explicitly provided in the method declaration. The ID attribute uniquely identifies each parameter's position within the parameter list for a given method: the first parameter is assigned the ID of zero, the second is assigned one, and so on.

Just like properties, method parameters may have default values assigned to them, and it is these values that are used whenever the calling application does not explicitly supply a value.

In FCL, a WMI method definition is represented by an instance of MethodData type. The MethodData is a simple type that has just a few properties; these are listed in Table 2-6.

Table 2-6: MethodData Properties

PROPERTY

DESCRIPTION

Name

A string that represents the name of the method.

Origin

A string that represents the name of the class that defines the method.

Qualifiers

A collection of QualifierData objects that represent the qualifiers that are defined for the method.

InParameters

An object of type ManagementBaseObject that represents the input parameters for the method.

OutParameters

An object of type ManagementBaseObject that represents the output parameters for the method.

The fact that input and output method parameters are represented by objects of type ManagementBaseObject that are referred to by InParameters and OutParameters properties of MethodData is somewhat interesting. Logically, you might expect method parameters to be described by a collection of PropertyData objects, each representing a single parameter, but instead, these parameters are packaged into the Properties collection of a ManagementBaseObject, which is returned by InParameters and OutParameters properties.

Why would the designers of the System.Management namespace choose to wrap the parameters collection with an instance of ManagementBaseObject, which does not seem to add value? The answer becomes clear once you understand how method parameters are defined in CIM Repository. It turns out that the WMI parameter definitions for a given method are described in a repository by an instance of a system class __PARAMETERS. This is really an abstract class that is used to store method parameter definitions. In its initial form, the __PARAMETERS class has only the standard system properties, but whenever a method with its associated parameter definitions is recorded into the repository, WMI creates an instance of the __PARAMETERS class and dynamically adds properties, each of which represents a single method parameter. This instance of the __PARAMETERS class is embodied by the ManagementBaseObject, which is returned by the InParameters and OutParameters properties of the MethodData type.

Yet, another interesting design choice is the existence of two separate objects of type ManagementBaseObject that are referred to by the InParameters and OutParameters properties of the MethodData instance. Apparently, having a single parameters collection so that each parameter is explicitly marked as input, output, or both, might have been a better approach because it would certainly simplify the coding. It turns out that such design has to do with the way parameters are defined in the repository. For those methods that have both input and output parameters, WMI creates two instances of the __PARAMETERS class that represent the method's input and output respectively. The downside of such a design is the fact that some parameters—those that are used as both input and output— are duplicated. As a result, they appear in the properties collections of both input and output instances of ManagementBaseObject.

Method parameters are identified by name and are specified within the WMI method declaration. As a result, each PropertyData object that represents a single method parameter will have its Name property set appropriately. The method return type, however, does not have an explicit name assigned to it as part of the method definition. To handle the return values, WMI adds a special property, called ReturnValue, to the properties collection of the ManagementBaseObject.

You can access the MethodData objects, which describe the methods of a particular class, through the Methods property of the ManagementClass object, as shown here:

ManagementClass mc = new ManagementClass("Win32_Process"); foreach(MethodData md in mc.Methods) { Console.WriteLine(md.Name); }

If you have a MethodData object that describes a particular method, it is easy to enumerate all the method parameters:

ManagementClass mc = new ManagementClass("Win32_Process"); foreach(MethodData md in mc.Methods) { ManagementBaseObject pin = md.InParameters; if ( pin != null ) { foreach(PropertyData pd in pin.Properties) { Console.WriteLine(pd.Name); } } ManagementBaseObject pout = md.OutParameters; if ( pout != null ) { foreach(PropertyData pd in pout.Properties) { Console.WriteLine(pd.Name); } } }

Checking for null values here is necessary because for methods with no input parameters, the InParameters property of the MethodData object will be set to null.

Another thing to keep in mind is that input-output parameters will appear in the Properties collections of both input and output ManagementBaseObjects. Thus, there is a chance that the preceding code example will print these parameters twice. To ensure that each parameter is listed only once, you may want to do something like this:

using System.Collections; ... ManagementClass mc = new ManagementClass("Win32_Process"); MethodData md = mc.Methods["Create"]; Hashtable ht = new Hashtable(); ManagementBaseObject pin = md.InParameters; if ( pin != null ) { foreach(PropertyData pd in pin.Properties) ht.Add(pd.Name, pd); } ManagementBaseObject pout = md.OutParameters; if ( pout != null ) { foreach(PropertyData pd in pout.Properties) if ( !ht.ContainsKey(pd.Name) ) ht.Add(pd.Name, pd); } foreach(object o in ht.Values) { PropertyData pd = (PropertyData)o; Console.WriteLine("{0} {1}", pd.Name, pd.Type); }

First, all the input parameters are loaded into a hash table, which is used here as a temporary container of PropertyData objects. Then, each output parameter is added to the hash table, but only if a parameter with the same name is not already contained in the hash. When this code finishes iterating through the Properties collection of the output ManagementBaseObject, the hash table will contain a single PropertyData object for each of the method parameters. This may not be the most efficient way to eliminate duplicates, but it certainly serves its purpose.

You may find it confusing that the ManagementObject type, and therefore, its subtype ManagementClass, offers a method called GetMethodParameters. This method returns an object of type ManagementBaseObject so that the Properties collection of the returned object describes the method parameters. Thus, you would imagine that the following is a more concise alternative to the code above:

ManagementClass mc = new ManagementClass("Win32_Process"); foreach(MethodData md in mc.Methods) { ... ManagementBaseObject mb = mc.GetMethodParameters(md.Name); if (mb != null ) { foreach(PropertyData pd in mb.Properties) { Console.WriteLine("{0} {1}", pd.Name, pd.Value); } } }

Unfortunately, this code will not produce the correct results, because the GetMethodParameters method returns only the input parameters. The reason for this is that this method is not intended to retrieve the schema information. Instead, it was designed to help invoke the WMI object methods. (Method invocation will be described in details later in this chapter.) Therefore, in order to enumerate all parameters of a given method, you have no other choice but to iterate through both the input and the output property collections.

Retrieving Class, Property, and Method Qualifiers

Finally, the last important elements of the WMI schema, which are easily accessible through an interface that is provided by the ManagementClass type, are the class, property, and method parameter qualifier declarations. As was briefly mentioned earlier in this chapter, qualifiers are special designations that are attached to various elements of the WMI schema in order to communicate the precise details of an element usage to schema consumers. For instance, in order to specify those properties of a WMI class that uniquely identify its instances, schema designers attach a Key qualifier to each property that makes up the key for that class. In the current release of WMI, qualifiers are strictly informational, meaning that the semantics specified by qualifiers are not enforced by WMI. As an example, Read qualifiers, designating properties of a class as read-only, do not make these properties write-protected, and it is up to the provider to ensure the integrity of the data. Thus, the primary purpose of qualifiers is to communicate some additional schema element usage details to the management applications, which are capable of interpreting the qualifiers.

The qualifier's definition consists of three parts: the qualifier name, the qualifier value, and one or more qualifier flavors. The flavor acts as a "meta-qualifier" providing additional information about an associated qualifier. For instance, a flavor indicates whether a qualifier can be localized. Besides localization, the most important pieces of information communicated by qualifier flavors are the rules for propagating the associated qualifier from the class to its subclasses and instances and the rules that specify whether a subclass or instance can override a qualifier. Table 2-7 lists all the defined qualifier flavors and their meanings.

Table 2-7: Qualifier Flavors

FLAVOR

DESCRIPTION

Amended

Indicates that the associated qualifier is not required as part of the class definition and can be moved to the amendment class. The amendment class is used for localization and will be discussed in detail later in this chapter.

DisableOverride

Indicates that a class, derived from the current class or an instance of the current class, may not override the associated qualifier.

EnableOverride

Indicates that a class, derived from the current class or an instance of the current class, may override the associated qualifier. This is a default.

NotToInstance

Indicates that the associated qualifier is not propagated to the instances of the current class. This is a default.

NotToSubclass

Indicates that the associated qualifier is not propagated to the subclasses of the current class. This is a default.

ToInstance

Indicates that the associated qualifier is propagated to the instances of the current class.

ToSubclass

Indicates that the associated qualifier is propagated to the subclasses of the current class.

Although a qualifier is always assigned a value, it is never explicitly associated with a particular data type. Instead, qualifiers are implicitly typed based on their value and, if no initial value is specified, a qualifier's value is assumed to be the Boolean TRUE value.

Earlier we mentioned that qualifiers are represented in the FCL by the QualifierData type. This is a very simple type that has just a few properties, which are listed in Table 2-8.

Table 2-8: QualifierData Properties

QUALIFIERDATA PROPERTY

DESCRIPTION

Name

Name of the qualifier.

Value

Value of the qualifier.

IsLocal

Boolean, indicates whether the qualifier is declared locally or propagated from the base class.

IsAmended

Boolean, indicates whether the qualifier is amended.

This corresponds to the Amended qualifier flavor.

IsOverridable

Boolean, indicates whether subclasses and instances of the class are allowed to override the qualifier. This corresponds to EnableOverride/DisableOverride qualifier flavors.

PropagatesToInstances

Boolean, indicates whether the qualifier propagates to instances of the class. Corresponds to ToInstance/NotToInstance qualifier flavors.

PropagatesToSubclasses

Boolean, indicates whether the qualifier propagates to subclasses of the class. Corresponds to ToSubclass/NotToSubclass qualifier flavors.

The QualifierData is a universal type that represents any kind of WMI qualifiers, regardless of whether these qualifiers are associated with a class, a property, a method, or method parameters. In fact, the QualifierData object that represents a property qualifier is indistinguishable from an object that represents, say, a class qualifier. The only thing that sets these two apart is the Qualifiers collection that holds them. Thus, all class qualifiers are contained in the Qualifiers collection of the ManagementClass object and can be enumerated as follows:

ManagementClass mc = new ManagementClass("Win32_Process"); foreach(QualifierData qd in mc.Qualifiers) { Console.WriteLine("{0} {1}", qd.Name, qd.Value); }

Property qualifiers, on the other hand, relate to a particular property; as a result, they are contained in the Qualifiers collection of their respective PropertyData object, as shown here:

ManagementClass mc = new ManagementClass("Win32_Process"); foreach(PropertyData pd in mc.Properties) { ... foreach(QualifierData qd in pd.Qualifiers) { Console.WriteLine("{0} {1}", qd.Name, qd.Value ); } ... }

By the same token, qualifiers that are associated with methods are contained in the Qualifier collection of their respective MethodData object. The instances of the MethodData type represent individual methods of a WMI class, and they can be accessed through the Methods property of the ManagementClass type:

ManagementClass mc = new ManagementClass("Win32_Process"); foreach(MethodData md in mc.Methods) { ... foreach(QualifierData qd in md.Qualifiers) { Console.WriteLine("{0} {1}", qd.Name, qd.Value ); } ... }

Finally, qualifiers for method parameters are held in the Qualifiers collection of the PropertyData object that represents the method parameter. PropertyData objects that describe the parameters of a method can be accessed through the InParameters and OutParameters properties of the MethodData object as shown here:

ManagementClass mc = new ManagementClass("Win32_Process"); foreach(MethodData md in mc.Methods) { ... if (md.InParameters != null ) { foreach(PropertyData pd in md.InParameters.Properties) { foreach(QualifierData qd in pd.Qualifiers) { Console.WriteLine("{0} {1}", qd.Name, qd.Value ); } } } if (md.OutParameters != null ) { foreach(PropertyData pd in md.OutParameters.Properties) { foreach(QualifierData qd in pd.Qualifiers) { Console.WriteLine("{0} {1}", qd.Name, qd.Value ); } } } ... }

This code, which is similar to the code that I used earlier to iterate though the collections of method parameters, reads the InParameters and OutParameters properties of the MethodData object and then iterates through the Properties collection of the resulting ManagementBaseObject. There, it prints all the qualifiers that are associated with a given property on the system console. At this point you must check whether InParameters and OutParameters properties point to valid instances of the ManagementBaseObject—if, for instance, a method has no input parameters, the InParameters property of its MethodData object will be set to null.

Just like the property values, qualifier values are not necessarily scalar. Some qualifiers may contain arrays of values—for instance, a standard qualifier ValueMap, which defines a set of allowable values for a given property or method parameter, is obviously an array. Thus, my code snippets, shown earlier, will not be able to correctly print out the values of array qualifiers. Unfortunately, since qualifiers are not explicitly typed, the QualifierData type, unlike the PropertyData type, does not have properties that reflect the underlying data type of the qualifier, nor do they indicate whether it is an array or scalar. Thus, a different approach should be used:

ManagementClass mc = new ManagementClass("Win32_Process"); foreach(QualifierData qd in mc.Qualifiers) { Console.WriteLine("{0} {1}", qd.Name, FormatQualifier(qd)); } ... public static object FormatQualifier(QualifierData pd) { if (pd.Value.GetType().IsArray) { StringBuilder sb = new StringBuilder(); sb.Append(Environment.NewLine); foreach(object o in (Array)pd.Value) { sb.Append(o.ToString()); sb.Append(Environment.NewLine); } return sb; } else { return pd.Value; } } }

Here I use the fact that the qualifier value, which is represented by the object of type System.Object, has an underlying type. This type can be accessed via the GetType method of System.Object. The resulting object of type System.Type exposes the IsArray property, which allows me to determine whether the qualifier value is indeed an array.

Analyzing the Schema A Complete Example

To conclude the discussion of the various elements that constitute a WMI class definition, I will show you a complete example. The example program ClassDef.exe (Listing 2-4) takes a single command line parameter—a path to a WMI class—and prints out the full class definition on the system console. The output bears strong resemblance to MOF class definitions; however, I did not attempt to produce a syntactically correct MOF schema. Frankly, the only reasons I chose to use the MOF-like notation were for readability and brevity—other formats could easily have been either overwhelmingly verbose or too confusing.

Listing 2-4: Sample Program ClassDef.exe.

class PrintClassDefinition { static void Main(string[] args) { if (args.Length < 1) { Console.WriteLine("usage: ClassDef "); return; } ManagementClass mc = new ManagementClass(args[0]); // process class qualifiers foreach(QualifierData qd in mc.Qualifiers) Console.WriteLine(FormatQualifier(qd,0)); Console.WriteLine(@" class {0} : {1} {{", mc["__CLASS"], mc["__SUPERCLASS"]); // process properties foreach(PropertyData pd in mc.Properties) Console.WriteLine(FormatProperty(pd,3) + ";"); // process methods foreach(MethodData md in mc.Methods) Console.WriteLine(FormatMethod(md,3) + ";"); Console.WriteLine("}"); } static object FormatQualifier(QualifierData qd, int indent) { StringBuilder sb = new StringBuilder(); sb.Append(' ', indent); sb.AppendFormat("[{0}={1}", qd.Name, FormatValue(qd.Value)); if (qd.PropagatesToInstance) sb.Append("ToInstance"); if (qd.PropagatesToSubclass) sb.Append("ToSubclass"); if (qd.IsAmended) sb.Append("Amended"); if(qd.IsOverridable) sb.Append("Overridable"); return sb.Append("]"); } static object FormatProperty(PropertyData pd, int indent) { StringBuilder sb = new StringBuilder(); // process property qualifiers foreach(QualifierData qd in pd.Qualifiers) { sb.Append(FormatQualifier(qd,indent)); sb.Append(Environment.NewLine); } sb.Append(' ',indent); sb.AppendFormat("{0} {1}", pd.Type.ToString() + (pd.IsArray ? "[]" : string.Empty), pd.Name); if ( pd.Value != null ) sb.AppendFormat("={0}", FormatValue(pd.Value)); return sb; } static object FormatMethod(MethodData md, int indent) { StringBuilder sb = new StringBuilder(); // process method qualifiers foreach(QualifierData qd in md.Qualifiers) { sb.Append(FormatQualifier(qd,3)); sb.Append(Environment.NewLine); } // eliminate duplicate in/out parameters SortedList sl = new SortedList(); if (md.InParameters != null) foreach(PropertyData pd in md.InParameters.Properties) sl[pd.Qualifiers["ID"].Value] = pd; if (md.OutParameters != null) foreach(PropertyData pd in md.OutParameters.Properties) if (pd.Name != "ReturnValue") sl[pd.Qualifiers["ID"].Value] = pd; // format method return type and method name sb.Append(' ',indent); sb.AppendFormat("{0} {1}(", ((PropertyData)md.OutParameters.Properties["ReturnValue"]).Type, md.Name); if (sl.Count > 1 ) { sb.Append(Environment.NewLine); string[] arr = new string[sl.Count]; int i = 0; foreach(PropertyData pd in sl.Values) arr[i++] = FormatProperty(pd, indent+3).ToString(); sb.Append(string.Join(","+Environment.NewLine, arr)); sb.Append(Environment.NewLine); sb.Append(' ',indent); } return sb.Append(")"); } static object FormatValue(object v) { if (v.GetType().IsArray) { string[] arr = new string[((Array)v).GetUpperBound(0)+1]; Array.Copy((Array)v, arr, ((Array)v).GetUpperBound(0)+1); return "{"+ string.Join(",", arr) + "}"; } else { return v; } } }

The program in Listing 2-4 is fairly straightforward and uses most of the constructs that were discussed earlier in this chapter. The Main method of PrintClassDefinition class iterates through the qualifiers, properties, and methods of a given class and then it invokes the helper functions FormatQualifier, FormatProperty, and FormatMethod to produce printable representation of the respective elements of WMI class definition. These helper functions are also self-explanatory, and the only one that deserves mentioning, is, perhaps, FormatMethod.

As you may remember, enumerating method arguments is a bit tricky because there are two collections that contain input and output parameters respectively. Additionally, there is a chance that some parameters that act as both input and output are duplicated in each of the collections. In one of the code examples above, we used Hashtable to temporarily store the PropertyData objects representing the parameters, and eliminate duplicates based on the parameter name. FormatMethod method takes a different approach. Instead of using Hashtable, it loads both input and output parameters into a SortedList that is keyed by the value of the ID qualifier that is attached to each parameter. Besides eliminating duplicates, this approach has the advantage of automatically sorting the parameter list based on the ID qualifier, thus ensuring that parameters are printed out in a correct order. One of the parameters, ReturnValue, receives special treatment for two reasons:

Although quite useful, this program has a few deficiencies. For instance, if you compare the output produced by ClassDef.exe against the MOF class definitions that are output by various WMI tools, such as the CIM Studio MOF generation wizard, you will notice that, for most WMI classes, our program lists more properties and methods than the actual MOF schema. The explanation for this is simple: ClassDef.exe does not differentiate between inherited and locally defined properties and methods. This is easily fixable—all I have to do is check the IsLocal property of each class element and suppress printing if the respective element is not defined locally. Yet another, more involved problem is localizable schema elements, which this program does not take into account. The solution to this one, although fairly simple, will not be shown here. Instead, the in-depth discussion of WMI localization and internationalization issues will be presented in "Localization and Internationalization Issues" later in this chapter.

ManagementClass Analyzing Relationships Between Schema Elements

As you found out earlier in this chapter, objects related to a given instance of a WMI class can be retrieved using the GetRelated method of the ManagementObject type. Obtaining the class definitions of related classes, however, is slightly more involved than retrieving the instances of these classes.

Retrieving Related Classes

You may remember that the ManagementObject type has an overloaded version of the GetRelated method, which, among other parameters, takes a Boolean flag that specifies whether this method returns live object instances or class definitions. Thus, the definitions of classes, related to Win32_Process class may be retrieved as follows:

ManagementObject mo = new ManagementObject(@" Win32_Process=269"); foreach(ManagementBaseObject o in mo.GetRelated( null, null, null, null, null, null, true, null)) { Console.WriteLine(o["__PATH"]); }

Here, the seventh parameter to the GetRelated method—the Boolean flag—is set to true, indicating that the method is required to return just the schema information rather than instances.

The output produced by this code verifies that the objects retrieved are indeed classes rather than instances:

\BCK_OFFICEROOTCIMV2:Win32_ComputerSystem \BCK_OFFICEROOTCIMV2:CIM_DataFile

Naturally, since the ManagementClass type inherits the GetRelated method from its parent, ManagementObject, you may assume that the following code will work just fine:

ManagementClass mc = new ManagementClass(@" Win32_Process"); foreach(ManagementBaseObject o in mc.GetRelated( null, null, null, null, null, null, true, null)) { Console.WriteLine(o["__PATH"]); }

Unfortunately, instead of listing all classes related to Win32_Process, the code will throw a ManagementException, complaining about an invalid query.

It turns out that in order to retrieve all classes or instances that satisfy a particular criteria (in this case, classes related to Win32_Process), the CIM Repository must be queried using a special query language, WQL. Thus, when the GetRelated method is invoked, it constructs an instance of the RelationshipQuery type (covered in Chapter 3), which in turn assembles a WQL query, based on the parameters passed to GetRelated. While the detailed coverage of WQL will not be provided until Chapter 3, I will cover at least the basic WQL query types here to make sure you understand how the GetRelated method works.

When it comes to querying for related classes, which are often referred to in WMI documentation as associators, there are several basic types of WQL queries. You choose the proper query type based on whether a requesting application is attempting to retrieve WMI classes or instances. Table 2-9 lists all the valid relationship query types.

Table 2-9: Association Query Types

QUERY TYPE

QUERY ENDPOINT

COMMENTS

Normal

Instance or class

Analyzes only the instances of association classes. The result may contain both instances and classes.

ClassDefsOnly

Instance

Analyzes only the instances of association classes. The result contains only class definitions.

SchemaOnly

Class

Analyzes only the association classes. The result set contains only class definitions.

Until now, we have seen only the normal and ClassDefsOnly queries. The problem with these two query types is that in order for them to produce meaningful results, there should be at least one WMI class instance involved. This is not the case with classes, since classes may not relate to instances and may only be associated with other classes. Thus, the only query type that is appropriate for retrieving the definitions of classes that are related to a given class, is a SchemaOnly type. Unfortunately, none of the GetRelated method versions supports the SchemaOnly queries, which renders this method useless for working with classes.

To solve this problem, ManagementClass type defines another method, GetRelatedClasses, which is specifically designed to construct SchemaOnly queries, regardless of its arguments. Thus, using the simplest overloaded form of GetRelatedClasses method, I can rewrite the previous code example as follows:

ManagementClass mc = new ManagementClass(@" Win32_Process"); foreach(ManagementBaseObject o in mc.GetRelatedClasses()) { Console.WriteLine(o["__PATH"]); }

This code finally works and produces the following output:

\BCK_OFFICEROOTCIMV2:Win32_ComputerSystem

Unlike the output of the GetRelated method invoked for an instance of the Win32_Process class, this output does not include the class definition for CIM_DataFile. In order to understand what is happening here, you need to look at the definitions of the WMI association classes involved:

class CIM_ProcessExecutable : CIM_Dependency { CIM_DataFile ref Antecedent = NULL; CIM_Process ref Dependent = NULL; ... } class Win32_SystemProcesses : CIM_SystemComponent { Win32_ComputerSystem ref GroupComponent = NULL; Win32_Process ref PartComponent = NULL; }

The first association class, CIM_ProcessExecutable, is used to express the relationship between the operating system process and the executable files that make up the process image. The second, Win32_SystemProcess, depicts the connection between a process and a computer system it is running on.

The interesting fact here is that the CIM_ProcessExecutable association does not reference the Win32_Process class; instead, it references its parent, CIM_Process. Strictly speaking, an instance of the Win32_Process class is also an instance of the CIM_Process; that is why, when it is analyzing the actual association objects, the GetRelated method retrieves all the objects that are linked through these associations—regardless of whether the relationship is express via superclass references.

The GetRelatedClasses method, on the other hand, does not analyze the association objects, but rather the association classes. In this case, you will find that considering the relationships at the superclass level is dangerous. This is because it may lead you to include those associations that are never instantiated or, in other words, those that never exist in real life. Therefore, the GetRelatedClasses method would only retrieve classes that are related to the current class through associations that reference the actual classes rather than their superclasses.

Yet another thing to watch out for is duplicate class objects, which GetRelatedClasses method may potentially return. Let us consider the following scenario:

ManagementClass mc = new ManagementClass(@" CIM_ComputerSystem"); foreach(ManagementBaseObject o in mc.GetRelatedClasses()) { Console.WriteLine(o["__PATH"]); }

You may notice that CIM_OperatingSystem is included twice in the output produced by this code:

\BCK_OFFICEROOTCIMV2:CIM_OperatingSystem \BCK_OFFICEROOTCIMV2:CIM_SystemResource \BCK_OFFICEROOTCIMV2:CIM_DMA \BCK_OFFICEROOTCIMV2:CIM_IRQ \BCK_OFFICEROOTCIMV2:CIM_MemoryMappedIO \BCK_OFFICEROOTCIMV2:CIM_FileSystem \BCK_OFFICEROOTCIMV2:CIM_OperatingSystem \BCK_OFFICEROOTCIMV2:CIM_SoftwareElement \BCK_OFFICEROOTCIMV2:Win32_SoftwareElement

The problem here is that there are two different association classes that link the CIM_ComputerSystem with the CIM_OperatingSystem:

class CIM_InstalledOS : CIM_SystemComponent { CIM_ComputerSystem ref GroupComponent = NULL; CIM_OperatingSystem ref PartComponent = NULL; ... } class CIM_RunningOS : CIM_Dependency { CIM_OperatingSystem ref Antecedent = NULL; CIM_ComputerSystem ref Dependent = NULL; }

These two associations are designed to express different kinds of relationships between classes. The CIM_InstalledOS depicts the containment relationship, while the CIM_RunningOS is a dependency association. Fortunately, the ManagementClass type provides an overloaded version of the GetRelatedClasses method, which allows the output to be filtered based on a certain criteria:

public ManagementObjectCollection GetRelatedClasses ( string relatedClass , string relationshipClass , string relationshipQualifier , string relatedQualifier , string relatedRole , string thisRole , EnumerationOptions options );

where the parameters are defined as follows:

Using this version of the GetRelatedClasses method, the preceding code can be rewritten to only return the classes that are engaged in a containment relationship with the CIM_ComputerSystem class so that the latter acts as a container:

ManagementClass mc = new ManagementClass(@" CIM_ComputerSystem"); foreach(ManagementBaseObject o in mc.GetRelatedClasses( null," CIM_SystemComponent",null,null,null," GroupComponent", null)) { Console.WriteLine(o["__PATH"]); }

Here we request all classes related to the current class via the containment association CIM_SystemComponent, or an association derived from it, where the current class plays a role of a container—GroupComponent. The output correctly lists the CIM_OperatingSystem class only once:

\BCK_OFFICEROOTCIMV2:CIM_OperatingSystem \BCK_OFFICEROOTCIMV2:CIM_SystemResource \BCK_OFFICEROOTCIMV2:CIM_DMA \BCK_OFFICEROOTCIMV2:CIM_IRQ \BCK_OFFICEROOTCIMV2:CIM_MemoryMappedIO \BCK_OFFICEROOTCIMV2:CIM_FileSystem

Finally, there is another overloaded version of the GetRelatedClasses method that takes a single string parameter, which represents the class name of which the resulting elements should be members, or from which they should be derived:

public ManagementObjectCollection GetRelatedClasses ( string relatedClass );

Although this version of the method has limited usefulness, it may come in handy in certain situations. For instance, when retrieving classes related to Win32_ComputerSystem, the output will include Win32_WMISettings class, which does not really represent a managed element and is usually of no concern to management applications. To ensure that the output contains just those classes that correspond to managed elements in the enterprise, the following code may be used:

ManagementClass mc = new ManagementClass(@" Win32_ComputerSystem"); foreach(ManagementBaseObject o in mc.GetRelatedClasses( "CIM_LogicalElement")) { Console.WriteLine(o["__PATH"]); }

Since the Win32_WMISettings class is not a subclass of CIM_LogicalElement, it will not be a part of the output produced by this code.

Retrieving Association Classes

Every once in a while, a managed application will be interested in retrieving the class definitions for the association classes used to link the current class to other elements in the CIM model. For reasons outlined earlier, the GetRelationships method inherited from ManagementObject will not work for ManagementClass. Instead, the ManagementClass type provides its own implementation, called GetRelationshipClasses, which is capable of issuing SchemaOnly queries. Thus, to list all association classes that reference the CIM_ComputerSystem class, we can use the following code:

ManagementClass mc = new ManagementClass(@" CIM_ComputerSystem"); foreach(ManagementBaseObject o in mc.GetRelationshipClasses()) { Console.WriteLine(o["__PATH"]); }

Just like the GetRelatedClasses method, GetRelationshipClasses has several overloaded versions, two of which are shown here:

public ManagementObjectCollection GetRelationshipClasses ( string relationshipClass );

where the parameter is defined as follows:

and

public ManagementObjectCollection GetRelationshipClasses ( string relationshipClass , string relationshipQualifier , string thisRole , EnumerationOptions options );

where the parameters are defined as follows:

Thus, the following example will print names of all dependency association classes that reference CIM_OperatingSystem:

ManagementClass mc = new ManagementClass(@" CIM_OperatingSystem"); foreach(ManagementBaseObject o in mc.GetRelationshipClasses( "CIM_Dependency")) { Console.WriteLine(o["__PATH"]); }

And yet another example will list only those dependencies in which the CIM_OperatingSystem class acts as a dependent class:

ManagementClass mc = new ManagementClass(@" CIM_OperatingSystem"); foreach(ManagementBaseObject o in mc.GetRelationshipClasses( "CIM_Dependency", null, "Dependent", null)) { Console.WriteLine(o["__PATH"]); }

Traversing Class Hierarchies

Besides being able to navigate through the relationships in a CIM model, it is sometimes necessary to obtain the definitions of classes, which are either super- or subclasses of a given WMI class. Retrieving the definition of an immediate superclass is easy:

ManagementClass mc = new ManagementClass(@" CIM_ComputerSystem"); ManagementClass smc = new ManagementClass(mc["__SUPERCLASS"].ToString()); Console.WriteLine(smc.Path);

Here I simply use the fact that the system property __SUPERCLASS of any WMI class contains the name of the immediate superclass. I can even trace the entire inheritance hierarchy for a given class:

ManagementClass mc = new ManagementClass(@" CIM_ComputerSystem"); foreach( string s in (string[])mc["__DERIVATION"]) { ManagementClass smc = new ManagementClass(s); Console.WriteLine(smc.Path); }

Again, another system property, __DERIVATION, which contains a list of all the superclasses for a given class, comes to the rescue.

However, the situation is different when it is required to enumerate the subclasses of a given class since there is no system property that holds the list of subclasses. To solve this problem, the ManagementClass type offers the GetSubclasses method, which, when invoked for a particular class, returns a collection of classes that are derived from this class. In its simplest form, the GetSubclasses method takes no parameters:

ManagementClass mc = new ManagementClass(@" CIM_ComputerSystem"); foreach(ManagementClass dmc in mc.GetSubclasses()) { Console.WriteLine(dmc["__PATH"]); }

Unfortunately, the output produced by this code may surprise you:

\BCK_OFFICEROOTCIMV2:CIM_UnitaryComputerSystem

Here, instead of retrieving all subclasses of CIM_ComputerSystem, the GetSubclasses method only returns its immediate subclass CIM_UnitaryComputerSystem. It turns out that in order to retrieve a complete list of all subclasses, another overloaded version of the GetSubclasses method should be used. If you remember the EnumerationOptions type, described in Table 2-2, you may recall that one of its properties, EnumerateDeep, controls whether the enumeration is recursive. Thus, the following code should do the trick:

ManagementClass mc = new ManagementClass(@" CIM_ComputerSystem"); EnumerationOptions eo = new EnumerationOptions(); eo.EnumerateDeep = true; foreach( ManagementClass dmc in mc.GetSubclasses(eo)) { Console.WriteLine(dmc["__PATH"]); }

This code correctly prints the paths of all subclasses of the CIM_ComputerSystem on the console:

\BCK_OFFICEROOTCIMV2:CIM_UnitaryComputerSystem \BCK_OFFICEROOTCIMV2:Win32_ComputerSystem

Our discussion of inter-element relationships within the CIM model would not be complete without pointing out how to enumerate instances of a particular WMI class. ManagementClass makes the task of retrieving all instances for a given class surprisingly easy:

ManagementClass mc = new ManagementClass(@" Win32_ComputerSystem"); foreach(ManagementObject o in mc.GetInstances()) { Console.WriteLine(o["__PATH"]); }

Here the parameterless version of the GetInstances method is used to enumerate the instances of the Win32_ComputerSystem class. The parameterless GetInstances method behaves similarly to the GetSubclasses method—it only retrieves objects that are of the same class as the class for which the method is invoked. For instance, if called for the CIM_ComputerSystem class, the method will return an empty collection. In order to retrieve all instances that belong to the current class or a class derived from the current class, the overloaded version of the GetInstances method should be used:

ManagementClass mc = new ManagementClass(@" CIM_ComputerSystem"); EnumerationOptions eo = new EnumerationOptions(); eo.EnumerateDeep = true; foreach( ManagementObject o in mc.GetInstances(eo)) { Console.WriteLine(o["__PATH"]); }

This code will produce the same output as the previous example where the GetInstances method was invoked for the Win32_ComputerSystem class:

\BCK_OFFICEROOTCIMV2:Win32_ComputerSystem.Name=" BCK_OFFICE"

Invoking WMI Object Methods

Most of the code examples presented earlier in this chapter dealt mainly with reading and analyzing the management data that is accessible through WMI objects and classes. Although the ability to retrieve the data is certainly crucial for virtually any management application, sometimes you may need to alter some information or carry out an action so that your changes are reflected within the management domain. Creating and killing processes, starting and stopping services, and compressing files are all examples of the day-to-day management tasks that any robust management system is expected to support. Although in some cases you will find it possible to create new WMI objects and alter certain management properties, such actions are likely to only affect the CIM Repository and may not be propagated to respective managed elements. Thus, you will find that creating an instance of the Win32_Process class will not launch the new operating system process, and setting the Compressed property of CIM_DataFile object to True will not actually compress the file.

The good news is that WMI does support a plethora of management activities that result in physical changes to the underlying managed environment. However, in order to effect these changes, you have to resort to invoking WMI object methods.

Method Fundamentals

Many WMI classes declare methods; however, the implementation for these methods may not always be available. Typically, only the methods marked with the Implemented qualifier within a given class are considered to have an implementation. A subclass that inherits an implemented method from its superclass may choose to provide its own implementation, in which case, the subclass's method should be marked as Implemented. If you omit the Implemented qualifier, the superclass's implementation will be invoked at runtime.

Depending on the semantics of a particular method, you may invoke it against a WMI class or an instance of the class. Methods that are designed to be called through a class are called static methods, a concept that should be familiar to anyone who is proficient in at least one mainstream object-oriented programming language, such as C++ or C#. Interestingly, WMI's treatment of static methods is more similar to C# rather than C++—in other words, if you attempt to invoke a static method against an instance of a WMI class, you will get a ManagementException.

To identify a method as static, the method definition should contain the Static qualifier. You will find that static methods are often used to construct new instances of a WMI class. For example, using the static Create method of the Win32_Process class is the only way to create a Win32_Process object that is bound to an operating system process. Such static methods, which are responsible for constructing new WMI objects, are similar to the constructors that are used by many object-oriented languages.

If you want to indicate that a method creates new objects, WMI requires that the method definition contain a Constructor qualifier. You may wonder whether CIMOM automatically invokes constructor methods whenever a new WMI class instance is created. Unfortunately, this is not the case—the Constructor qualifier is purely informational and is completely ignored by WMI. Because the construction process for the majority of managed elements is fairly involved, you will find that charging CIMOM with the responsibility of automatically calling object constructors will significantly increase the implementation complexity while providing little value.

To compliment the constructor methods, many WMI classes declare methods, which are responsible for tearing down the unneeded instances. Such methods, often referred to as destructors, are functionally very similar to C++ destructors or C# finalizers. A typical destructor is an instance, rather than a static method, because its purpose is to tear down the object against which it is invoked. Just as the constructors are marked with Constructor qualifiers, the destructor methods should have the Destructor qualifier attached to the method declaration. Again, this qualifier is informational and completely ignored by CIMOM.

The method search algorithm, employed by CIMOM at runtime, depends mainly on where a particular method is implemented in the CIM class hierarchy. In order to fully understand how WMI executes static and instance methods, look at the following example:

class A { sint32 Foo(); } class B : A { [Implemented] sint32 Foo(); } class C : B { sint32 Foo(); } class D : C { [Implemented] sint32 Foo(); }

Here, the instance method, Foo, is first declared in class A and implemented in its subclass B. Class C inherits the method from B but does not provide the implementation.

Finally, the last subclass in a hierarchy, D, overrides the method implementation with its own. When the method Foo is called against an instance of one of the classes in this hierarchy, WMI starts with the class of the current instance and continues up the inheritance tree looking for the method implementation. Thus, if the method is invoked against an instance of D, the implementation provided by D is used at runtime. However, if the method is called for an instance of class C, WMI will invoke the implementation supplied by B since C does not provide its own method implementation.

Interestingly, in those cases when an instance for which a method is called is accessed through its superclass, WMI invokes a correct implementation based on the class to which the instance actually belongs. You may remember that the GetInstances method of ManagementClass type may return instances of not only the current class, but also all of its subclasses, if the appropriate enumeration options are specified. Thus, it is entirely possible that an instance of class D is accessed through class A, and in this case, WMI will use the method implementation provided by D. Finally, if the object for which the method is invoked happens to be an instance of class A, the invocation will fail because A does not supply the implementation for Foo and does not have any superclasses that may provide this implementation.

The search algorithm is very different for static methods. In fact, there is no algorithm; instead, WMI always attempts to execute a static method using the implementation provided by the invoking class. Thus, if the class does not supply an implementation, the method invocation will fail. For instance, if Foo above were a static method, it would only be possible to invoke this method for classes B and D, since these are the only classes that supply the implementations for Foo.

Calling Instance Methods

Invoking WMI methods is fairly straightforward. The ManagementObject type offers a convenient, self-explanatory method, InvokeMethod, which, in its simplest form, takes a string parameter that represents the name of the method to invoke and an object array that contains the method parameters.

Before you attempt to execute an arbitrary method, you must know what parameters the method expects. There are many ways to look up a method signature, and one option is to use the ClassDef.exe program that was developed earlier in this chapter. Once you know the method parameter list, invoking the method is trivial:

ManagementObject mo = new ManagementObject("Win32_Process=100"); Object r = mo.InvokeMethod("Terminate", new object [] {32}); Console.WriteLine("Terminate() returned {0}", r);

As you may have already guessed, the code above binds to an instance of the Win32_Process class, identified by its process ID, and calls its Terminate method, which effectively kills the process. The Win32_Process Terminate method has the following declaration:

[Destructor, Implemented] uint32 Terminate([In] uint32 Reason);

It takes a single input parameter—an unsigned integer that represents the exit code for the process to be terminated—and returns an unsigned integer value, which indicates whether the method succeeded or failed. Thus, the preceding code snippet calls the InvokeMethod method, passing the name of the method to invoke and an object array with a single element—a value 32 that will be used as an exit code for the terminated process. Upon completion, InvokeMethod outputs the return status of the Terminate method as a value of type object.

Things get a bit more complicated when a method has output parameters. For instance, consider the declaration for the GetOwner method of the Win32_Process class:

[Implemented] uint32 GetOwner([Out] string User, [Out] string Domain);

Besides the unsigned integer value that represents the return code of the method, this declaration returns two output parameters: the name of the user to which a process belongs, and the domain of the user. In order to retrieve the values of the output parameters, the method should be invoked as follows:

ManagementObject mo = new ManagementObject("Win32_Process=100"); object[] parms = new object [] {null, null}; Object r = mo.InvokeMethod("GetOwner", parms); Console.WriteLine("GetOwner() returned {0}", r); Console.WriteLine("User: {0} Domain: {1}", parms[0], parms[1]);

Here, the object array is first initialized with two null values that act as placeholders for the user and domain output parameters respectively. This array is then passed as a parameter to GetOwner method, and upon the method completion, it will contain the values of the process owner and the domain. Note that the number of elements in the parameter array must be equal to the number of all input and output method parameters. Thus, the following code will throw an IndexOutOfRangeException:

ManagementObject mo = new ManagementObject("Win32_Process=100"); object[] parms = new object [] {}; Object r = mo.InvokeMethod("GetOwner", parms );

Calling Static Methods

Executing a static method is not very much different. For instance, given the following declaration of the Win32_Process constructor method Create:

[Constructor, Static, Implemented] uint32 Create( [In] string CommandLine, [In] string CurrentDirectory, [In] Object ProcessStartupInformation, [Out] uint32 ProcessId );

the following code will do the trick:

ManagementClass mc = new ManagementClass("Win32_Process"); object[] parms = new object [] { @" C:WINNTNOTEPAD.EXE C:BOOT.INI", ".", null, null}; Object r = mc.InvokeMethod("Create", parms ); if ( (uint)r == 0 ) Console.WriteLine("Process {0} successfully created", parms[3]);

The only principal difference here is that the Create method is invoked against a class rather than against an instance.

Identifying Method Parameters Programmatically

You will find that having to look up the method declaration in order to determine its parameter list is a bit annoying. In fact, sometimes you might want to identify method parameters programmatically based on some kind of metadata. As you may remember, the MethodData object, accessible through the Methods collection of the ManagementClass type, has two properties: InParameters and OutParameters. These describe the inputs and outputs of a method, respectively. The InParameters and OutParameters properties return objects of ManagementBaseObject type, which correspond to instances of WMI __PARAMETERS system class that is used to describe the formal arguments of a method. Thus, it is possible to iterate through the Properties collections of these parameter classes and programmatically construct the object array that represents the method parameters. There is, however, a better way to deal with argument lists. The ManagementObject type provides an overloaded version of InvokeMethod method, which takes a ManagementBaseObject that contains the method parameter values. Therefore, the code above can be rewritten as follows:

ManagementClass mc = new ManagementClass("Win32_Process"); ManagementBaseObject parms = ((MethodData)mc.Methods["Create"]).InParameters; parms["CommandLine"] = @" C:WINNTNOTEPAD.EXE C:BOOT.INI"; parms["CurrentDirectory"] = "."; ManagementBaseObject r = mc.InvokeMethod("Create", parms, null); if ( (uint)r["ReturnValue"] == 0 ) Console.WriteLine("Process {0} successfully created", r["ProcessID"]);

This code first reads the InParameters property of the Create method of Win32_Process class. The ManagementBaseObject, returned by the InParameters property, contains one property for each input parameter of the Create method. The code then sets each property of the input object to appropriate values, calls InvokeMethod , and passes it the input ManagementBaseObject, which contains neatly packaged parameter values. The third parameter to the InvokeMethod method is just an object of type InvokeMethodOptions—a derivative of the ManagementOptions type that is used to set the timeout for the operation and pass the context values to the data provider.

Note that the ManagementBaseObject passed to InvokeMethod represents only the input parameters of a method. Upon completion, the InvokeMethod returns another instance of ManagementBaseObject; this time it contains the output parameter values. As I mentioned earlier, every method is guaranteed to return at least one output value: the return code of the invoked method that is identified by the name ReturnValue. The preceding code checks the ReturnValue to ensure that the Create method succeeded, and then it prints out the process ID of a newly created process by reading the value of the ProcessID property of the output ManagementBaseObject.

It is not a good idea to use the ManagementBaseObject, which is pointed to by the InParameters property of the MethodData, to pass input parameters to a method. The purpose of InParameters and OutParameters properties is to describe the method parameter list and help you analyze the class and method definitions. Thus, if you use the InParameters to pass the arguments to a method and forget to clear out its property values when you are finished, the next time you print out the class definition you may be fooled into thinking that some method parameters are assigned default values. A much better way to retrieve the ManagementBaseObject that represents the input parameters of a method is by using the GetMethodParameters method of the ManagementObject type, as shown here:

ManagementClass mc = new ManagementClass("Win32_Process"); ManagementBaseObject parms = mc.GetMethodParameters("Create"); parms["CommandLine"] = @" C:WINNTNOTEPAD.EXE C:BOOT.INI"; parms["CurrentDirectory"] = "."; ManagementBaseObject r = mc.InvokeMethod("Create", parms, null); if ( (uint)r["ReturnValue"] == 0 ) Console.WriteLine("Process {0} successfully created", r["ProcessID"]);

This method takes a single string argument—the name of the method—and returns a ManagementBaseObject, which represents the input parameters of this method. In addition to being cleaner and easier to use, GetMethodParameters is also safer because it internally creates a new instance of the ManagementBaseObject specifically for the purpose of housing the method parameters.

Until now, I deliberately avoided mentioning the third parameter of the Create method of the Win32_Process class—ProcessStartupInformation. If you are familiar with the Win32 API, you will correctly guess the purpose of this parameter: it specifies the main window properties of a new process. In fact, this parameter directly corresponds to the STARTUPINFO structure, which is declared in Winbase.h and used as a parameter to Win32 API CreateProcess function:

typedef struct _STARTUPINFO { DWORD cb; LPTSTR lpReserved; LPTSTR lpDesktop; LPTSTR lpTitle; DWORD dwX; DWORD dwY; DWORD dwXSize; DWORD dwYSize; DWORD dwXCountChars; DWORD dwYCountChars; DWORD dwFillAttribute; DWORD dwFlags; WORD wShowWindow; WORD cbReserved2; LPBYTE lpReserved2; HANDLE hStdInput; HANDLE hStdOutput; HANDLE hStdError; } STARTUPINFO, *LPSTARTUPINFO;

You may think that in order to specify the startup properties of a process, it is enough to pass a reference to this structure as a parameter to the Win32_Process Create method:

ManagementClass mc = new ManagementClass("Win32_Process"); ManagementBaseObject parms = mc.GetMethodParameters("Create"); STARTUPINFO si = new STARTUPINFO(); si.lpTitle = "Started through WMI"; parms["CommandLine"] = @" C:WINNTSYSTEM32CMD.EXE"; parms["CurrentDirectory"] = "."; parms["ProcessStartupInformation"] = si; ManagementBaseObject r = mc.InvokeMethod("Create", parms, null);

Unfortunately, this will not work. First, when you closely inspect the System.Management namespace you will find that it does not reveal the STARTUPINFO type or any other type that bears even a slight resemblance to the STARTUPINFO structure. Even if you define this structure yourself, the code will still fail.

The solution to this problem becomes obvious if you revisit the Create method's declaration:

uint32 Create( [CIMTYPE=string, In] string CommandLine, [CIMTYPE=string, In] string CurrentDirectory, [CIMTYPE=object:Win32_ProcessStartup, In] object ProcessStartupInformation, [CIMTYPE=uint32, Out] uint32 ProcessId );

It turns out that ProcessStartupInformation is expected to be an object of type Win32_ProcessStartup. For this, the CIM Repository contains a class, called Win32_ProcessStartup, that has the following definition:

class Win32_ProcessStartup : Win32_MethodParameterClass { uint32 CreateFlags; uint32 PriorityClass; string EnvironmentVariables[]; string WinstationDesktop; string Title; uint32 X; uint32 Y; uint32 XSize; uint32 YSize; uint32 XCountChars; uint32 YCountChars; uint32 FillAttribute; uint16 ShowWindow; uint16 ErrorMode = 0; };

This class matches the STARTUPINFO structure fairly closely. Thus, the following code will correctly set the title of the CMD.EXE window to a "Created through WMI" string:

ManagementClass mc = new ManagementClass("Win32_Process"); ManagementBaseObject parms = mc.GetMethodParameters("Create"); ManagementBaseObject si = new ManagementClass("Win32_ProcessStartup").CreateInstance(); si["Title"] = "Started through WMI"; parms["CommandLine"] = @" C:WINNTSYSTEM32CMD.EXE"; parms["CurrentDirectory"] = "."; parms["ProcessStartupInformation"] = si; ManagementBaseObject r = mc.InvokeMethod("Create", parms, null);

Specialized parameter classes, similar to Win32_ProcessStartup, are used extensively throughout the WMI object model. For instance, the Win32_Share Create method that is used to create file shares takes an Access parameter of type Win32_SecurityDescriptor, which represents the permissions, owner, and access capabilities for the shared resource. The Win32_SecurityDescriptor class corresponds to the Win32 SECURITY_DESCRIPTOR structure. The interesting thing about this class is that three of its properties—Group, Owner and SACL—are themselves references to objects of type Win32_Trustee and Win32_ACE respectively.

Asynchronous Programming

WMI can be slow. Not because of its design deficiencies or implementation problems, but simply because it often operates in a vastly distributed environment and handles unwieldy amounts of management data. Just imagine a program that compresses all text files exceeding a certain size limit on hundreds of computers that are connected via a WAN and you will understand why some WMI operations take forever to complete. Although it is possible to optimize the performance of WMI programs, there are still some factors that are beyond the programmer's control, such as the state and the speed of the network. Thus, users of interactive management applications may have to learn how to deal with the frustration that results from staring at the computer screen while waiting for some lengthy WMI action to complete.

Although you will find it difficult to guarantee consistent, subsecond response times for all WMI operations, you may be able to make the problem transparent, or at least less visible to the users. Most WMI operations can be carried out asynchronously, so the main program thread is not tied up until the operation completes. By utilizing the asynchronous programming techniques, you can build a powerful management application that is capable of carrying out multiple actions simultaneously in background and letting the users do other things while the WMI processes their requests.

The following WMI operations can be performed both synchronously and asynchronously:

Therefore, virtually every operation that has been discussed earlier in this chapter, as well as most of the actions discussed in the subsequent chapters, have asynchronous equivalents.

Introducing the ManagementOperationObserver Type

The System.Management type that makes asynchronous operations possible is called ManagementOperationObserver. It is a very simple type, but when you pass it as a parameter to a WMI operation, it monitors the progress and raises events based on the state of the operation. There are four events that are available through the ManagementOperationObserver:

The ObjectReady is fairly straightforward. To facilitate the handling of this event, the System.Management namespace supplies a delegate ObjectReadyEventHandler, which takes a parameter of type ObjectReadyEventArgs. Every time the event handler is invoked, the NewObject property of the ObjectReadyEventArgs parameter will point to an object that is returned by the asynchronous data retrieval operation.

To handle the ObjectPut event, you should use the ObjectPutEventHandler delegate, which takes a parameter of type ObjectPutEventArgs. Then when the event handler is invoked, the Path property of the ObjectPutEventArgs parameter will contain the path to the newly created or updated WMI object.

The remaining two events—Completed and Progress—are intended to communicate the status of the operation back to the client, rather than just deliver the results. Generally, status reporting in WMI is optional and is only enabled if the client application sets the WBEM_FLAG_SEND_STATUS flag while issuing an asynchronous call. This flag causes WMI to register the client that receives the intermediate status updates via its implementation of the IWbemObjectSink::SetStatus method. You may expect one of the management option types, such as ObjectGetOptions, to have a SendStatus property that controls whether WMI reports on the operation progress; however, this is not the case. Instead, System.Management seems to enable the intermediate status reporting unconditionally. Thus, you can count on receiving the Completed event for every asynchronous operation that you invoke. Raising the Progress event, however, is up to the provider, and since not every provider is capable of intermediate status reporting, the Progress event is somewhat unreliable.

The handler for the Progress event, delegate ProgressEventHandler, receives a parameter of type ProgressEventArgs, which houses extended information that describes the progress of the operation. The ProgressEventArgs type has three properties: Current, UpperBound, and Message. The first two are integer values that represent the total amount of work to be done and the amount of work already completed, respectively. The last property, Message, may contain a textual message that describes the operation's status.

The Completed event is a bit more complex. Its handler, represented by delegate CompletedEventHandler, also receives a parameter of type CompletedEventArgs, which contains the extended status information about the completion of the asynchronous operation. The CompletedEventArgs type has two properties: Status and StatusObject. The Status is simply an error code that is defined by the ManagementStatus enumeration. The StatusObject, however, is an object of type ManagementBaseObject that is bound to an instance of the WMI class __ExtendedStatus. The __ExtendedStatus class, which is used by WMI to report the detailed status and error information back to the client application, has the following definition:

class __ExtendedStatus : __NotifyStatus { string ProviderName; string Operation; string ParameterInfo; string Description; };

Table 2-10 provides a detailed description of each property of the __ExtendedStatus class.

Table 2-10: __ExtendedStatus Class Properties

PROPERTY

DESCRIPTION

ProviderName

A string that represents the name of the WMI data provider that carried out the operation. If the error or status change is not generated by a provider, this property will be set to a string "Windows Management".

Operation

A string that describes the operation that was in progress when an error or status change was raised. Typically, this property will contain the name of the WMI API method that was invoked to carry out the operation, such as ExecQuery.

ParameterInfo

A string that describes the parameters to the operation that was in progress when an error or status change was raised. For instance, when you attempt to retrieve a nonexistent class, this property will be set to the name of the requested class.

Description

A String that provides additional details regarding the error or operation status.

ManagementOperationObserver is capable of monitoring nearly every aspect of an asynchronous operation and providing rich feedback to the appli-cation clients.

Invoking Asynchronous Operations

Now you will take a look at how an asynchronous operation is invoked. The following snippet of code demonstrates the mechanics of the simplest asynchronous call: it is used to retrieve a single WMI object:

class Monitor { bool bComplete = false; bool bError = false; public void OnCompleted(object sender, CompletedEventArgs ea) { if ( ea.Status != ManagementStatus.NoError ) { bError = true; Console.WriteLine( "Error occurred: {0}", ea.StatusObject["Description"]); Console.WriteLine("Provider: {0}", ea.StatusObject["ProviderName"]); Console.WriteLine("Operation: {0}", ea.StatusObject["Operation"]); Console.WriteLine("ParameterInfo: {0}", ea.StatusObject["ParameterInfo"]); } bComplete = true; } public bool IsComplete { get { return bComplete; } } public bool IsError { get { return bError; } } public static void Main(string[] args) { Monitor m = new Monitor(); ManagementOperationObserver ob = new ManagementOperationObserver(); ob.Completed += new CompletedEventHandler(m.OnCompleted); ManagementObject mo = new ManagementObject("Win32_Process=316"); mo.Get(ob); while(!m.IsComplete) { // do something while waiting System.Threading.Thread.Sleep(1000); } if(!m.IsError) Console.WriteLine("Retrieved {0}", mo["__PATH"]); } }

You may remember that instantiating a ManagementObject will not automatically bind it to an underlying WMI object. Instead, the binding occurs implicitly whenever a property of a ManagementObject instance is first accessed, or explicitly when a Get method is called. Get has an overloaded version that takes a parameter of type ManagementOperationObserver. When it is invoked with an instance of ManagementOperationObserver, Get returns immediately, and it's the ManagementOperationObserver that monitors the operation and raises the Completed event whenever the binding is finished. The Completed event handler first checks the status of the operation by comparing the Status field of its CompletedEventArgs parameter object against the NoError member of the ManagementStatus enumeration. Then it sets the flag to indicate that the action is completed.

Enumerating Objects Asynchronously

Enumerating through a collection of objects or classes asynchronously is a bit more involved. As opposed to their synchronous counterparts, collection retrieval methods, such as GetInstances, GetRelated, and so on, do not just return a collection of objects when called in asynchronous fashion. Instead, the ManagementOperationObserver, which these methods take as a parameter, raises the ObjectReady event for each object that is returned by the corresponding method:

class Monitor { bool bComplete = false; public void OnObjectReady(object sender, ObjectReadyEventArgs ea) { ManagementBaseObject mb = ea.NewObject; Console.WriteLine("Retrieved {0}", mb["__PATH"]); } public void OnCompleted(object sender, CompletedEventArgs ea) { if ( ea.Status != ManagementStatus.NoError ) { Console.WriteLine( "Error occurred: {0}", ea.StatusObject["Description"]); Console.WriteLine("Provider: {0}", ea.StatusObject["ProviderName"]); Console.WriteLine("Operation: {0}", ea.StatusObject["Operation"]); Console.WriteLine("ParameterInfo: {0}", ea.StatusObject["ParameterInfo"]); } bComplete = true; } public bool IsComplete { get { return bComplete; } } public static void Main(string[] args) { Monitor m = new Monitor(); ManagementOperationObserver ob = new ManagementOperationObserver(); ob.Completed += new CompletedEventHandler(m.OnCompleted); ob.ObjectReady += new ObjectReadyEventHandler(m.OnObjectReady); ManagementClass mc = new ManagementClass("Win32_Process"); mc.GetInstances(ob); while(!m.IsComplete) { // do something while waiting System.Threading.Thread.Sleep(1000); } } }

Here, the ObjectReady event handler is doing most of the work. Whenever the ObjectReady event is raised, its handler is invoked with a parameter of type ObjectReadyEventArgs so that the NewObject property of this parameter points to an object that is returned by the GetInstances method.

Handling Errors

Interestingly, the error handling code that constitutes most of the Completed event handler is actually quite useless. For instance, if you attempt to bind to a nonexistent object, a ManagementException will be thrown and the event handler will never be called. This seems to be the result of a lazy initialization technique employed by types in the System.Management namespace—whenever a method is invoked, the code will always check to see if a requesting object is bound to an underlying WMI object or class. Thus, asynchronous error handling involving the __ExtendedStatus object packaged within the CompletedEventArgs makes most sense when you are running WMI WQL queries. Although the detailed treatment of WQL and System.Management query facilities will be presented in the next chapter, the following code snippet shows a situation when the error handling technique, similar to the one just described, is actually useful:

class Monitor { bool bComplete = false; public void OnObjectReady(object sender, ObjectReadyEventArgs ea) { ManagementBaseObject mb = ea.NewObject; Console.WriteLine("Retrieved {0}", mb["__PATH"]); } public void OnCompleted(object sender, CompletedEventArgs ea) { if ( ea.Status != ManagementStatus.NoError ) { Console.WriteLine( "Error occurred: {0}", ea.StatusObject["Description"]); Console.WriteLine("Provider: {0}", ea.StatusObject["ProviderName"]); Console.WriteLine("Operation: {0}", ea.StatusObject["Operation"]); Console.WriteLine("ParameterInfo: {0}", ea.StatusObject["ParameterInfo"]); } bComplete = true; } public bool IsComplete { get { return bComplete; } } public static void Main(string[] args) { Monitor m = new Monitor(); ManagementOperationObserver ob = new ManagementOperationObserver(); ob.Completed += new CompletedEventHandler(m.OnCompleted); ob.ObjectReady += new ObjectReadyEventHandler(m.OnObjectReady); ManagementObjectSearcher ms = new ManagementObjectSearcher( new SelectQuery("Win64_Process")); ms.Get(ob); while(!m.IsComplete) { // do something while waiting System.Threading.Thread.Sleep(1000); } } }

This code attempts to asynchronously retrieve all instances of the nonexistent Win64_Process WMI class using a WQL SELECT query. When run, it will produce the following output on the system console:

Error occurred: Provider: WinMgmt Operation: ExecQuery ParameterInfo: select * from Win64_Process

Modifying Objects Asynchronously

The technique you would use to modify or delete WMI objects or classes asynchronously is very similar to the one employed for asynchronous data retrieval. The only difference is that ManagementOperationObserver will raise a different event (ObjectPut rather than ObjectReady) when it completes the modification operation. The following code updates the BackupInterval property of Win32_WMISetting object using the asynchronous version of the Put method:

class Monitor { bool bComplete = false; public void OnObjectPut(object sender, ObjectPutEventArgs ea) { Console.WriteLine("Updated {0}", ea.Path); } public void OnCompleted(object sender, CompletedEventArgs ea) { if ( ea.Status != ManagementStatus.NoError ) { Console.WriteLine( "Error occurred: {0}", ea.StatusObject["Description"]); Console.WriteLine("Provider: {0}", ea.StatusObject["ProviderName"]); Console.WriteLine("Operation: {0}", ea.StatusObject["Operation"]); Console.WriteLine("ParameterInfo: {0}", ea.StatusObject["ParameterInfo"]); } bComplete = true; } public bool IsComplete { get { return bComplete; } } public static void Main(string[] args) { Monitor m = new Monitor(); ManagementOperationObserver ob = new ManagementOperationObserver(); ob.Completed += new CompletedEventHandler(m.OnCompleted); ob.ObjectPut += new ObjectPutEventHandler(m.OnObjectPut); ManagementObject mo = new ManagementObject("Win32_WMISetting=@"); mo["BackupInterval"] = 120; mo.Put(ob); while(!m.IsComplete) { // do something while waiting System.Threading.Thread.Sleep(1000); } } }

Here the event handler for the ObjectPut event receives a parameter of type ObjectPutEventArgs, which has a Path property that is set to the full object path of the WMI object affected by the update operation.

Calling Methods Asynchronously

Obviously, WMI methods can also be invoked asynchronously. However, what is not quite obvious is how the output parameter of a method is communicated back to the client since the asynchronous InvokeMethod returns immediately. It turns out that asynchronous method invocation will trigger the ObjectReady event, and the ObjectReadyEventArgs parameter that is passed to the event handler will house an instance of the __PARAMETERS class that contains the values of the output parameters:

class Monitor { bool bComplete = false; public void OnObjectReady(object sender, ObjectReadyEventArgs ea) { ManagementBaseObject mb = ea.NewObject; foreach(PropertyData pd in mb.Properties) { Console.WriteLine("{0} {1}", pd.Name, pd.Value); } } public void OnCompleted(object sender, CompletedEventArgs ea) { if ( ea.Status != ManagementStatus.NoError ) { Console.WriteLine( "Error occurred: {0}", ea.StatusObject["Description"]); Console.WriteLine("Provider: {0}", ea.StatusObject["ProviderName"]); Console.WriteLine("Operation: {0}", ea.StatusObject["Operation"]); Console.WriteLine("ParameterInfo: {0}", ea.StatusObject["ParameterInfo"]); } bComplete = true; } public bool IsComplete { get { return bComplete; } } public static void Main(string[] args) { Monitor m = new Monitor(); ManagementOperationObserver ob = new ManagementOperationObserver(); ob.Completed += new CompletedEventHandler(m.OnCompleted); ob.ObjectReady += new ObjectReadyEventHandler(m.OnObjectReady); ManagementClass mc = new ManagementClass("Win32_Process"); ManagementBaseObject parms = mc.GetMethodParameters("Create"); parms["CommandLine"] = @" c:winntsystem32 otepad.exe c:oot.ini"; parms["CurrentDirectory"] = "."; mc.InvokeMethod(ob, "Create", parms, null); while(!m.IsComplete) { // do something while waiting System.Threading.Thread.Sleep(1000); } } }

When the ObjectReady event handler is invoked, it will print out the values of output parameters on the system console as follows:

ProcessId 247 ReturnValue 0

You may remember that there is an alternative version of InvokeMethod method that takes an array of objects that represents both input and output method parameters so that the values of the output parameters are stored in the same array when the method finishes. I also pointed out that you have to supply placeholders for output parameters in order to make sure that InvokeMethod does not throw an exception while it is attempting to store the output values into the array:

ManagementObject mo = new ManagementObject("Win32_Process=100"); object[] parms = new object[] {null, null}; mo.InvokeMethod("GetOwner", parms); Console.WriteLine("User: {0} Domain: {1}", parms[0], parms[1]);

You can also call this version of the InvokeMethod asynchronously, but if you do, the output parameter values will not be stored back into the object array. Instead, the operation will trigger the ObjectReady event and provide the values of the output parameters via the instance of the __PARAMETERS class that is packaged into the ObjectReadyEventArgs. Thus, if you assume that ob represents an instance of ManagementOperationObserver, the following code is correct and will produce the results you want:

ManagementObject mo = new ManagementObject("Win32_Process=100"); mo.InvokeMethod(ob, "GetOwner", null);

Alternatively, you can use this invocation technique:

ManagementObject mo = new ManagementObject("Win32_Process=100"); mo.InvokeMethod(ob, "GetOwner", new object[] {});

Finally, if an asynchronous method invocation results in an error, the error will be communicated back to the client either through the ReturnValue property of the __PARAMETERS object, which is passed to the ObjectReady event handler; or through the __ExtendedStatus object, which is passed as part of the CompletedEventArgs parameter to the Completed event handler. For instance, if you modify the Monitor class example in an attempt to call the Win32_Process Create method so that its CommandLine parameter refers to a nonexistent executable or does not constitute a valid operating system command:

mc.InvokeMethod(ob, "Create", new object[] {"C: otepad.exe", ".", null, null});

the ObjectReady event handler will print the following on the console, indicating that the Create method failed:

ProcessId ReturnValue 9

However, if you try to execute a static method such as Create against an instance of the WMI class, or an attempt to call an instance method for a class:

ManagementClass mc = new ManagementClass("Win32_Process"); mc.InvokeMethod(ob, "GetOwner", new object[] {});

the ObjectReady event will never fire and the Completed event handler will print the following on the console:

Error occurred: Provider: WinMgmt Operation: ExecMethod ParameterInfo: Win32_Process

Lastly, if you call a nonexistent method, the InvokeMethod will throw a ManagementException prior to actually initiating an asynchronous operation.

Localization and Internationalization Issues

Localization is a very popular buzzword that is often thrown around when people are talking about software systems. Generally, to localize an application, you need to translate all the text strings associated with it, such as various labels, messages, and so on, into a format and language that the end user understands, and you need to provide input and output facilities that utilize such format and language. In order to satisfy the localization requirements, software vendors often create localized versions of their products that are suitable for specific cultures.

The concept of globalization, which usually presumes that a given software system is capable of supporting multilingual users and data simultaneously, is related to localization. Rather than producing several culture-specific versions of the software, vendors of globalized systems usually offer a single package that configures itself dynamically based on the language and culture preferences of the user. Both localization and globalization are often referred to as internationalization.

Just like many other software systems that are marketed and used worldwide, Windows has provisions for supporting multilingual users. The operating system is available in many localized versions, also known as single user interface (SUI) versions, so that the only difference between versions is the language used in the resources section of each binary file. There is also a globalized version of Windows, known as the Multilanguage User Interface (MUI), which allows the users to set up their UI according to their culture and preferences, assuming that the resources that correspond to their language of choice are installed on their system.

WMI and Internationalization

Because WMI is the premier management system for Windows platforms, it is expected to run on both single- and multi-UI versions of the OS, and it has to support both localization and globalization. Simply put, this means that WMI needs to be capable of localizing its schema as well as its object property values, and it needs to have provisions for building globalized management applications.

Generally, localization revolves around those elements of the schema that communicate some information to the end user. Such elements, often referred to as displayable aspects of the schema, are typically contained in the qualifiers (such as property of method descriptions, for instance). Thus, localization does not apply to the actual identifier strings that are used to name classes, properties, or methods; instead it is relevant solely to schema qualifiers. Although it would be better if string properties were localizable too, at this time WMI does not provide any direct way to localize the property values.

The idea behind WMI internationalization facilities is based on the concept of a master class. A master class is a complete definition for a given class comprised of both localized and locale-neutral elements. At runtime, a master class is dynamically assembled as a union of two distinct class definitions: basic and amendment. All locale-neutral elements of a class definition are segregated into a basic class, while all localizable components constitute an amendment class. As opposed to a basic class, an amendment class is abstract and does not correspond to a real WMI class. Instead, it contains a subset of the properties of a master class that have localizable qualifiers. Since both basic and amendment classes share the same name, all amendment class definitions are placed in a separate namespace that is subordinate to the namespace of a basic class. In fact, there are multiple subordinate namespaces, each corresponding to a particular locale and containing the respective amendment class definitions. The naming convention for these child namespaces is MS_XXXX, where XXXX is a hexadecimal value that represents a particular Win32 Locale ID (LCID). For instance, if a WMI installation contains classes that are amended for German and English locales, its namespace hierarchy may look like the following:

ootCIMV2 ootCIMV2ms_409 ootCIMV2ms_407

where 409 and 407 are locale identifiers for English and German locales respectively.

Retrieving Localized Schema Elements

Such structure makes it easy for the WMI runtime to dynamically merge the localized and locale-neutral elements of a class definition before it returns the information to the client application. However, such merging of basic and amended class definitions does not occur automatically. For instance, most properties have a Description qualifier that provides a textual description, that clarifies the usage or purpose of the property. For apparent reasons, such description is locale-specific and is not part of the basic class definition. Thus, the code, like the following, which attempts to retrieve the value of the Description qualifier for the Handle property of Win32_Process class, will fail:

ManagementClass mc = new ManagementClass("Win32_Process"); Console.WriteLine(mc.GetPropertyQualifierValue("Handle", "Description"));

The preceding code will throw a ManagementException that complains about a nonexistent qualifier. Obviously, this code retrieves just the basic class definition, which excludes all localizable property qualifiers, such as Description.

To enable the runtime merging of basic and amended class definitions, you must use a special flag, WBEM_FLAG_USE_AMENDED_QUALIFIERS. This flag is exposed through the UseAmendedQualifiers property of the ObjectGetOptions class, which controls the retrieval of the management data:

ManagementClass mc = new ManagementClass("Win32_Process"); mc.Options.UseAmendedQualifiers = true; Console.WriteLine(mc.GetPropertyQualifierValue("Handle", "Description"));

Assuming that your locale is set to English (0x409) and that the ootCIMV2ms_409 namespace contains the localized definition for the Handle property of the Win32_Process class, this code will correctly print the property description on the system console:

A string used to identify the process. A process ID is a process handle.

The approach is similar when you iterate through collections of management objects. You may remember that the options object that controls the enumeration operations is called EnumerationOptions. By setting its UseAmendedQualifiers property to true, you can tell WMI to return localizable information for each object output by the enumeration:

ManagementClass mc = new ManagementClass("CIM_Process"); EnumerationOptions eo = new EnumerationOptions(); eo.UseAmendedQualifiers = true; foreach(ManagementClass m in mc.GetSubclasses(eo)) { Console.WriteLine(m.GetPropertyQualifierValue("Handle", "Description")); }

It is possible to determine which qualifiers are localizable by inspecting the IsAmended property of the QualifierData type. Thus, the following code prints out all localizable property qualifiers, along with the associated property names, for the Win32_Process class:

ManagementClass mc = new ManagementClass(@" Win32_Process"); mc.Options.UseAmendedQualifiers = true; foreach(PropertyData pd in mc.Properties) { foreach(QualifierData qd in pd.Qualifiers) if(qd.IsAmended) Console.WriteLine("[{1}] {0}", pd.Name, qd.Name); }

Note that the IsAmended property only returns TRUE for localizable qualifiers if the class definition is retrieved with the UseAmendedQualifiers property set to true. Therefore, if you comment out the line in the preceding code that sets the UseAmendedQualifiers property of ObjectGetOptions, this code will produce no output because IsAmended will always return FALSE.

Updating Localized Schema Elements

If you stumble upon another options type, PutOptions, you may notice that it also has a property named UseAmendedQualifiers. Having to set the flag to get amended qualifier values when you are reading the data is understandable, but what does it have to do with update operations? To find out the answer to this question, consider the following scenario:

ootCIMV2: [locale(0x409)] class Foo { [key] sint32 fooID; string fooName; } ootCIMV2ms_409: [amendment, locale(0x409)] class Foo { [Description("This is a key property."):amended] sint32 fooID; [Description("This is a name property."):amended] string fooName; }

Here, the basic class definition for Foo, which has two properties, resides in the ootCIMV2 namespace. This definition is amended for the English locale with a copy of Foo that is located in the ootCIMV2ms_409 namespace so that both of the Foo properties are given localized Description qualifiers. In order to create an instance of Foo and save it to CIM Repository, you may write the following code:

ManagementClass mc = new ManagementClass("Foo"); mc.Options.UseAmendedQualifiers = true; ManagementObject mo = mc.CreateInstance(); mo["fooID"] = 123; mo["fooName"] = "Foo"; try { mo.Put(); } catch(ManagementException ex) { Console.WriteLine(ex.Message); }

This legitimate looking code will actually throw a ManagementException and the catch block will print the following message on the system console:

An amended object cannot be put unless WBEM_FLAG_USE_AMENDED_QUALIFIERS is specified.

It turns out that if you retrieve an amended class and spawn its instance, the instance will then contain all the amended qualifiers from the amendment class. This new instance cannot be stored into a namespace that contains the basic class definition unless the amended qualifiers are stripped. This is where the PutOptions type comes in handy:

ManagementClass mc = new ManagementClass("Foo"); mc.Options.UseAmendedQualifiers = true; ManagementObject mo = mc.CreateInstance(); mo["fooID"] = 123; mo["fooName"] = "Foo"; PutOptions po = new PutOptions(); po.UseAmendedQualifiers = true; try { mo.Put(po); } catch(ManagementException ex) { Console.WriteLine(ex.Message); }

This code snippet will correctly remove the amended qualifiers and save the new instance into the ootCIMV2 namespace.

Error Handling

All types in the System.Management namespace adhere to the .NET error handling model, which means that all errors are communicated back to the client via exceptions rather than via method return values or error codes. In fact, I should say that WMI errors are communicated via an exception since there is only one exception type—ManagementException—that all System.Management types use. ManagementException is a derivative of a standard System.Exception class and, as such, it inherits a few useful properties: Message, Source, TargetSite, and StackTrace. For all intents and purposes, ManagementException behaves just like any other .NET exception type, and that is why I deliberately ignored the subject of error handling up until now. After all, the following boilerplate error handling code should look familiar to just about anyone with some .NET programming experience:

try { ManagementObject mo = new ManagementObject("Win32_Process=100"); Console.WriteLine(mo["ExecutablePath"]); } catch(ManagementException ex) { Console.WriteLine("*** ERROR OCCURRED ***"); Console.WriteLine("Source: {0} Description: {1}", ex.Source, ex.Message); Console.WriteLine(ex.StackTrace); }

Working with the ManagementException Type

Although these standard exception properties already provide a wealth of useful information, the ManagementException type has additional fields that allow you to look even more deeply into the problem. One of these fields is ErrorCode, which contains the actual WMI return code of an operation that caused an exception. This is a property of type ManagementStatus, which is an enumeration that contains all the currently defined WMI error codes. Second, there is an ErrorInformation property, which refers to an object of type ManagementBaseObject. As you may have guessed, it contains an instance of the __ExtendedStatus class that was described earlier in this chapter (see Table 2-10). Thus, by interrogating the properties of the __ExtendedStatus object, you can obtain very detailed information that is pertinent to what caused the exception. For instance, an attempt to bind to a nonexistent instance of the Win32_Process class:

try { ManagementObject mo = new ManagementObject("Win32_Process=999999"); Console.WriteLine(mo["ExecutablePath"]); } catch(ManagementException ex) { foreach(PropertyData pd in ex.ErrorInformation.Properties) { Console.WriteLine("{0} {1}", pd.Name, pd.Value); } }

will yield the following output:

Description Operation GetObject ParameterInfo Win32_Process.Handle=999999 ProviderName CIMWin32 StatusCode

As opposed to standard exception properties, which present an exception in .NET-centric fashion, __ExtendedStatus object reflects the WMI view of the problem. For instance, its Operation property usually contains a name of a WMI COM API method, which was executing when the exception was thrown.

On those rare occasions when a problem is internal to a given System.Management type and has nothing to do with WMI, some other exception besides ManagementException may be thrown. Thus, if you try to create an instance of the ManagementObject type by providing a class rather than an object path to its constructor, the constructor will throw the all-too-familiar ArgumentOutOfRange exception. Note that in this case, the exception will be thrown by the constructor rather than by any subsequent code that attempts to use a newly created instance. Since all management types use the lazy initialization technique and do not access WMI until first used by a management application, you may deduce that the source of this exception is the constructor code itself rather than WMI. The bottom line is that any exception of a type other than ManagementException is the result of a usage error rather than a system error, and as such, it should not be handled by an application catch block.

Using Strongly Typed Wrappers for WMI Classes

If you have object-oriented programming experience and you have played with System.Management namespace types, you will eventually attempt to build wrappers for the most commonly used WMI classes. Although ManagementClass and ManagementObject types are extremely powerful generic constructs for dealing with all kinds of management data, their generic nature makes programming somewhat more complex. The problem is that these types are late-bound, or, in other words, they are not automatically and permanently associated with arbitrary WMI classes or objects. For instance, a Process class that is bound to the Win32_Process WMI class would significantly simplify the programming model and reduce chances for errors:

Process ps = new Process(100); Console.WriteLine(ps.ExecutablePath); uint id; uint r = ps.Create(@" c:winnt otepad.exe", ".", null, out id);

Thus, if you are striving to reap the benefits of strongly typed programming, you may produce a wrapper for the Win32_Process class that is similar to the following:

public class Process { private ManagementObject obj; public Process(uint Handle) { obj = new ManagementObject("Win32_Process=" + Handle); } public string ExecutablePath { get { return (string)obj["ExecutablePath"]; } } public uint Create( string CommandLine, string CurrentDirectory, ManagementBaseObject ProcessStartupInformation, out uint ProcessID) { ManagementClass mc = new ManagementClass("Win32_Process"); ManagementBaseObject parms = mc.GetMethodParameters("Create"); parms["CommandLine"] = CommandLine; parms["CurrentDirectory"] = CurrentDirectory; parms["ProcessStartupInformation"] = ProcessStartupInformation; ManagementBaseObject r = mc.InvokeMethod("Create", parms, null); ProcessID = (uint)r["ProcessID"]; return (uint)r["ReturnValue"]; } }

The good news is that building wrappers like this by hand is not necessary. The designers of the System.Management namespace, anticipating the demand for strongly typed management objects, built the powerful tools needed to auto-generate the strongly typed code.

Using MgmtClass exe to Generate WMI Class Wrappers

Perhaps the easiest way to produce a strongly typed wrapper for an arbitrary WMI class is by using the MgmtClassGen.exe utility that ships as part of the .NET SDK. This is a command line tool that can be invoked as follows:

MgmtClassGen.exe []

When this is invoked with a single parameter—the name of the WMI class for which to generate a strongly typed wrapper—MgmtClassGen.exe simply outputs a file, called .CS (where is an actual name of the WMI class less the schema prefix) into the current directory. If you want, you can alter the default behavior of the tool via several command line options, listed in Table 2-11.

Table 2-11: MgmtClassGen.exe Command Line Options

OPTION

DESCRIPTION

/l

Instructs MgmtClassGen.exe to use a particular programming language when generating the strongly typed class definition. Currently, available language options are CS (C#), VB (Visual Basic) and JS (JavaScript). If this option is omitted, C# code is generated by default.

/m

Specifies the name of the server where the target WMI class is located. By default, MgmtClassGen.exe connects to the local machine.

/n

Sets the path to the WMI namespace that contains the target WMI class. If this option is omitted, the tool assumes that the target WMI class resides in the ootCIMV2 namespace.

/o

Specifies the name of the .NET namespace into which the generated class should be placed. If this option is omitted, MgmtClassGen.exe generates the namespace prefix using the WMI namespace and class schema prefix. Thus, for the Win32_Process class that resides in ootCIMV2, the generate namespace prefix will be ROOT.CIMV2.Win32.

/p

Sets the path to the file to which the generated code should be saved. If this path is omitted, the tool outputs the file into the current directory. By default, the name of the output file is generated based on the name of the target WMI class less the schema prefix. Thus, for the Win32_Process class, the default output file name will be Process.CS.

/pw

When logging on to a remote computer using the /m option, this option specifies a password to be used for establishing the connection.

/u

When logging on to a remote computer using /m option, this option specifies a user account to be used for establishing the connection.

/?

This option instructs MgmtClassGen.exe to print its usage guidelines on the console and exit.

Using Server Explorer to Generate WMI Class Wrappers

Yet another tool that makes generating the strongly typed wrappers for WMI classes even easier is the Server Explorer Management Extension in Visual Studio .NET, shown on Figure 2-1.

Figure 2-1: Server Explorer Management Extension

This extension can be accessed through the ViewServer Explorer menu option in Visual Studio .NET. In order to generate a wrapper for, say, the Win32_Process class, you should follow these steps:

  1. Select a target server using the Tree View of the Server Explorer.
  2. Navigate to the Processes node.
  3. Right-click on Processes and select Generate Managed Class from the pop-up menu.

The generated class is then added to the current project.

Generating WMI Class Wrappers Programmatically

Although you will find that generating strongly typed wrappers with MgmtClassGen.exe and the Server Explorer extension is very easy and trouble-free, sometimes such an approach lacks flexibility. To satisfy all tastes, the designers of System.Management namespace equipped the ManagementClass type with the GetStronglyTypedClassCode method, which allows you to create the wrappers programmatically. The first form of the GetStronglyTypedClassCode method takes three parameters: a designation for the programming language for which the code is to be generated, the path to the output file, and the .NET namespace into which the newly generated class is to be placed. Thus, to generate a wrapper for the Win32_Process class, you would use the following code:

ManagementClass mc = new ManagementClass("Win32_Process"); if(mc.GetStronglyTypedClassCode(CodeLanguage.CSharp, @" C: empProcess.CS", "WMI")) Console.WriteLine("Successfully generated code for Win32_Process"); else Console.WriteLine("Failed to generate code for Win32_Process");

Here, the first parameter of enumeration type CodeLanguage instructs the method to use C# as an output programming language. Currently, the CodeLanguage enumeration contains just three language designations: CSharp, VB, and JScript (for C#, Visual Basic and JavaScript respectively). The second parameter—path to the output file—is a string that contains either an absolute or a relative file path. If just the file name is supplied, the file will be created in the current working directory. Note that you cannot pass null or an empty string as a file name parameter to GetStronglyTypedClassCode and have the file name defaulted to the name of WMI class. If a valid file name is not supplied, the method will throw ArgumentOutOfRangeException. Finally, the last parameter is a string that represents a .NET namespace into which the resulting wrapper class should be placed. The method accepts an empty string as a placeholder, in which case the namespace will be defaulted to the WMI namespace name plus the schema name. Thus, for the Win32_Process class, which is residing in the ootCIMV2 namespace, the default .NET namespace name will be ROOT.CIMV2.Win32.

If you are adventurous and looking for even more flexibility, you may want to turn to an overloaded version of the GetStronglyTypedClassCode method. Instead of saving the generated code to a file, this method returns an object of type CodeDom.CodeTypeDeclaration. CodeDom is a .NET namespace that contains a slew of types that are used to generate code in a variety of programming languages. The CodeTypeDeclaration object that is returned by the GetStronglyTypedClassCode method represents a language-independent declaration for a .NET wrapper class that is suitable for being fed to a .NET language compiler or code generator. For instance, the following example does the same thing as the previous code snippet—it saves the generated code to a file—but this time, it does so using the code generation facilities of CodeDom namespace:

using System; using System.Management; using System.CodeDom; using System.CodeDom.Compiler; using Microsoft.CSharp; ... CodeTypeDeclaration dc = new ManagementClass("Win32_Process").GetStronglyTypedClassCode(false,false); CodeCompileUnit cu = new CodeCompileUnit(); cu.Namespaces.Add(new CodeNamespace("WMI")); cu.Namespaces[0].Types.Add(dc); CodeDomProvider prov = new CSharpCodeProvider(); ICodeGenerator gen = prov.CreateGenerator(); System.IO.TextWriter wr = new System.IO.StreamWriter(@" C:TEMPProcess.cs", false, System.Text.Encoding.ASCII, 1024); CodeGeneratorOptions gco = new CodeGeneratorOptions(); gen.GenerateCodeFromCompileUnit(cu,wr,gco);

Although the particulars of the functionality afforded by the CodeDom namespace types are outside the scope of this book, the preceding code sequence is fairly self-explanatory.

  1. First, this code creates an instance of the ManagementClass type that is bound to the Win32_Process WMI class.
  2. Then it invokes its GetStronglyTypedClassCode method to produce a CodeTypeDeclaration for the wrapper class.

GetStronglyTypedClassCode takes two boolean parameters: a flag that indicates whether a class for managing WMI system properties should be included in the declaration, and another flag that states whether the generated wrapper should be capable of managing the system properties. Once the CodeTypeDeclaration is available, the code follows these steps:

  1. It creates the CodeCompileUnit (a unit of compilation) and adds a namespace called WMI to its namespaces collection.
  2. After that, it adds the CodeTypeDeclaration to the collection of types within that namespace. At this point, the CodeCompileUnit contains a complete code graph that is suitable for compilation or code generation.
  3. The code example then creates an instance of the code provider. In this case, it is a provider that is capable of generating and compiling the C# code.
  4. Finally, the CodeCompileUnit is fed into the GenerateCodeFromCompileUnit method of the code generator along with an instance of TextWriter, which is opened over an output file named C: empProcess.cs.

When the GenerateCodeFromCompileUnit method finishes, the output file contains a complete source code for the Win32_Process wrapper class.

One apparent benefit that you get if you use this version of the GetStronglyTypedClassCode method is the ability to manipulate the properties of CodeTypeDeclaration and thus analyze or alter the wrapper code before it is generated. A less apparent, but probably even more exciting advantage of this approach is that you get the ability to substitute a custom code provider in place of the standard C#, Visual Basic, or JavaScript provider. Thus, by using the System.CodeDom.Compiler.CodeDomProvider abstract class as a base, you can implement a code provider that can generate and compile code in virtually any programming language.

Regardless of how you generate a wrapper class source code for a given WMI class the code is the same. In fact, both the MgmtClassGen.exe and the Server Explorer extension internally use the GetStronglyTypedClassCode method of the ManagementClass to output the wrapper class definitions. The generated code is fairly straightforward, and it uses primarily the functionality described in this chapter. As a result, you should be able to figure out all implementation details easily. There are, however, a few caveats of which you should be aware:

  1. It is entirely possible that the name of a method or property of the generated class is a key (a reserved word) in the target programming language. In this case, the code generation facility will change the name of the respective property or method to avoid the collision.
  2. Typically, GetStronglyTypedClassCode will generate get and set accessors for each WMI class property that does not have a Read qualifier. You may remember that properties marked with Read qualifier are considered read-only; therefore, for these properties only the get accessor will be generated.
  3. Some WMI class properties may have Values or ValueMap qualifiers that indicate sets of permissible values for the property. In such cases, GetStronglyTypedClassCode will create an enumeration type with individual members that represent each of the values given by the Values or ValueMap qualifier. The data type of the property will then be set to the generated enumeration type.
  4. There is a subtle difference in how the code generation facility handles those WMI classes that are marked in the CIM Repository as singletons. Typically, the generated class will have a default parameterless constructor and a parameterized constructor. The default constructor does nothing. The parameterized constructor takes a parameter that represents a key property of the WMI class and internally creates a bound instance of the ManagementObject type. For singletons, however, the default parameterless constructor is coded to bind the class to a single instance of the underlying WMI object.

You will find that generating wrappers for WMI classes is extremely helpful even if you do not intend to use the generated code in your management application. You should definitely look at the generated source code; not only does it significantly simplify the programming, but it can be used as an excellent source of information on System.Management type's usage patterns

Summary

This chapter has provided a comprehensive and detailed overview of the fundamental functionality afforded by the types in System.Management namespace. Although some more esoteric details of WMI programming, such as querying the CIM Repository with WQL and handling WMI events, will be covered in subsequent chapters, having studied the material in this chapter, you should be capable of building nearly any kind of management application. More specifically, you should be proficient at

Armed with such a thorough understanding of this functionality, you are now ready to delve into the somewhat more complex aspects of System.Management programming that are covered in the next few chapters.

Категории