Querying WMI

Highlights

You will find that navigating through the CIM object model is not always straightforward and often requires rather elaborate coding. When you know the identity of a WMI object in question in advance, you can bind to it by just instantiating a ManagementObject with an appropriate object path parameter. The situation is different, however, if a criterion for retrieving the WMI object is not based on the object's identity but rather on its other, non-key properties. For instance, if you are charged with the task of compressing all files that exceed 10 MB in size on a given system, you may crank out the following code:

ManagementClass mc = new ManagementClass("CIM_DataFile"); foreach(ManagementObject mo in mc.GetInstances()) { if((ulong)mo["FileSize"] > 10490000) { uint r = (uint)mo.InvokeMethod("Compress", new object[] {}); Console.WriteLine("Compressed {0} ({1})", mo["FileName"], r); } }

Here the identity of a file—its name, which is necessary in order to bind the instance of the ManagementObject to a corresponding WMI object directly— is not known up-front, hence your need to iterate through all objects of type CIM_DataFile to check whether the FileSize property exceeds the imposed limit. When you are looking at this code, the first thing that comes to mind is its relative inefficiency. Even though the number of large files on an average system is fairly small when compared to the overall count of all file system objects, you still need to examine all instances of CIM_DataFile to identify those few that match your search criteria. But before you can inspect the file size, you need to create a ManagementObject and bind it to an instance of a WMI class, which can be quite expensive, even for objects on a local system. Worse yet, in a distributed environment, such code is guaranteed to exhibit unacceptable performance, in addition to generating excessive and unnecessary network traffic.

Another negative factor commonly associated with searching the CIM model for classes and objects is code complexity. Although the snippet above is not particularly intricate, the level of complexity tends to increase as the criteria you use to retrieve objects becomes more sophisticated. For instance, if you have a set of compound selection criteria, based on the non-key properties of several related objects, your code may be extremely convoluted and error-prone.

The good news is that WMI is perfectly capable of doing all the dirty work involved in locating the objects directly based on the values of their non-key properties. The technique for doing this, however, is quite different from the CIM navigation procedures that were outlined in the previous chapter. Specifically, WMI has to be told how to locate the objects in question via a particular notation known as a query.

Clearly, there is a parallel between querying an object model such as CIM and querying a relational database. Just like the relations or database tables, WMI objects have "columns," which are referred to as properties in object-oriented parlance, and, just like the tables, the objects are linked together via a well-defined set of relationships. Thus, in theory, you can easily adopt the notation that is used to express relational queries to query the CIM object model. For instance, if you assume that Structured Query Language (SQL), utilized in relational queries, is used to query CIM, the following query may return all objects that represent files with a size exceeding the 10 MB limit:

SELECT * FROM CIM_DataFile WHERE FileSize > 10490000

This approach is far superior to iterating through a collection of all file objects on a given system. Not only is it more concise, but it is also more efficient, because only the objects that satisfy the search criteria are returned to the caller.

In fact, the query language used by WMI, called WMI Query Language (WQL), is a subset of ANSI SQL. Although it is arguable that the latter is the best query language, especially when it comes to object models, SQL brings quite a few benefits to the table. First, because it is one of the earlier query notations, it enjoys unparalleled popularity and is well understood by software developers. Its straightforward English-like grammar is another factor that makes it easy to learn and use. Finally, SQL has been abused so many times and found in so many unexpected applications, that it would be a sin not to adopt it for WMI.

WQL, when used with the WMI query facilities, is extremely effective and often indispensable, especially when it comes to building sophisticated management tools. The System.Management namespace houses a handful of types that make querying WMI easy while providing almost unlimited flexibility and power. However, to query WMI effectively, you need to understand WQL and the query processing facilities pretty well. This knowledge is exactly what this chapter provides.

WQL Overview

Although the official WMI documentation claims that WQL is a subset of ANSI SQL, this is not entirely true. Even though both query languages are conceptually similar and some WQL constructs bear a strong syntactic resemblance to analogous features of SQL, WQL adds a slew of operators and statements that do not exist in ANSI SQL. Thus, from a syntactical standpoint, WQL is not a subset but rather a variation of SQL; or in other words, it is a SQL-like query language.

Unlike SQL, which offers extensive functionality to support data modification operations, WQL is a data-retrieval language. In other words, it is simply not possible to make any changes to either the WMI schema or the management data via WQL because the language does include any data-or schema-manipulation commands. Nevertheless, when it comes to retrieving the data, WQL really shines.

As you learned earlier, two broad classes of information are available through WMI: management data, which is housed by instances of WMI classes, and schema information, which describes the structure of management objects. In reality, WMI maintains an additional category of information—event data, which enables management applications to receive notifications whenever certain changes are made to the management data or schema. WQL, therefore, is designed to facilitate three classes of queries: data queries that return the management data, schema queries that return the schema elements, and, finally, event queries, which register event consumers. Although I will briefly discuss the syntax used for event queries, the primary focus of this chapter is on data and schema queries. I will cover the details of WMI event mechanism, as well as the specifics of how you register and handle events, in Chapter 4.

As you will see shortly, the System.Management namespace offers a range of types that make assembling and processing data, schema, and event queries easier. Although you will find that writing a program to test WQL queries is fairly trivial, sometimes you will want to have a tool that is capable of executing the queries on users behalf and displaying the results. This is exactly what the WQL Query Builder tool, which is included in the WMI CIM Studio that is distributed as part of WMI SDK, is designed to accomplish.

The WQL Query Builder can be accessed by clicking the Query Builder button that is located in the upper-right corner of the CIM Studio screen, just above the object viewer pane. Figure 3-1 shows what the Query Builder looks like, once activated.

Figure 3-1: WQL Query Builder

To execute a query using WQL Query Builder, simply type the query text into the Query textbox and click the Execute button. The results will appear in the object viewer pane of the CIM Studio. In addition to being a versatile query-testing tool, Query Builder has some basic query management facilities. Thus, you may save a query by assigning a name to it and clicking the Save button. You can recall a saved query later so that you can edit or execute it. Despite its mini-malistic user interface, WQL Query Builder is very useful for query debugging or, simply for ensuring the validity of WQL queries before you use them in a management application.

SELECT Statement

Perhaps, one of the qualities of WQL that makes it so attractive is its simplicity. The entire language consists of just three statements: SELECT, ASSOCIATORS OF, and REFERENCES OF. Depending on the syntax options you choose, you may use these to retrieve data, schema, or event information. Out of these three, SELECT is the most powerful and the most versatile data retrieval command; it is typically used to satisfy most of the querying needs. SELECT is also easier to understand, due to its conceptual and syntactic resemblance to its SQL counterpart.

Listing 3-1 shows a semiformal definition of the WQL SELECT statement.

Listing 3-1: WQL SELECT Statement

statement ::= SELECT FROM [WITHIN ] [WHERE ] [GROUP ]; property_list ::= '*' | [,...]; class_name ::= | 'meta_class'; property_name ::= | .; selection_criteria ::= [AND ...] | [OR ...]; grouping_criteria ::= WITHIN [BY ] [HAVING 'NumberOfEvents' ]; expression ::= ; operator ::= '=' | '!=' | '<>' | '<' | '>' | '>=' | '<=' | 'IS' | 'IS NOT' | 'ISA';

SELECT Statement Basics

The following line of code illustrates what a simplest form of the SELECT statement may look like:

SELECT ExecutablePath, ProcessID FROM Win32_Process

This is a data query, which simply returns the values of the ExecutablePath and ProcessID properties of every Win32_Process object on a given system. In cases when more then just a few object properties are of interest, instead of spelling out the property names, you may use an * as a shortcut—it stands for all properties of a given object:

SELECT * FROM Win32_Process

However, the SELECT statement is not very useful unless it lets you limit the result set to certain objects or classes that satisfy a given selection criteria. In WQL you achieve this by applying an appropriate WHERE clause to the SELECT statement. Thus, to retrieve a list of Win32_Service objects that have an automatic startup mode, you may use the following query:

SELECT * FROM Win32_Service WHERE StartMode = 'Auto'

In addition, multiple conditions may be grouped together with AND or OR clauses. For instance, the following query will retrieve all instances of the Win32_Service class that have an automatic startup mode but are not currently running:

SELECT * FROM Win32_Service WHERE StartMode = 'Auto' AND State = 'Stopped'

Just like in regular SQL, WQL's AND clause has precedence over the OR clause. For example, the following query, which attempts to identify all services with an automatic startup mode that are in either a stopped or an unknown state, will not be processed correctly:

SELECT * FROM Win32_Service WHERE StartMode = 'Auto' AND State = 'Stopped' OR State = 'Unknown'

Instead of returning just the autostart services in the stopped or unknown state, the preceding code will return all services that are in the unknown state as well as the autostart services in the stopped state. You can change the precedence of OR and AND clauses by parenthesizing an appropriate expression:

SELECT * FROM Win32_Service WHERE StartMode = 'Auto' AND (State = 'Stopped' OR State = 'Unknown')

You may invert the meaning of a particular expression by applying the NOT clause. For instance, the following WQL will produce a list of all Win32_Service objects that do not have an automatic start mode:

SELECT * FROM Win32_Service WHERE NOT StartMode = 'Auto'

WQL is rather strict when it comes to parsing the expressions that represent the query selection criteria. An arithmetic expression, for instance, may not appear as part of the selection criteria. Thus, the following query will be flagged as invalid:

SELECT * FROM Win32_Process WHERE WorkingSetSize >= (1000*5 + 20000)

By the same token, evaluating the properties of an object based on an arithmetic expression that involves its other properties is impossible. For example, the following, seemingly innocent query, which attempts to identify those instances of the Win32_Process class where the process working set size exceeds 60 percent of its maximum working set, will fail:

SELECT * FROM Win32_Process WHERE WorkingSetSize >= (MaximumWorkingSetSize * 0.60)

In general, it is safe to say that any useful WQL search expression should have a property name on one side and a constant on the other. Basically, the data type of a constant should correspond to the data type of a property involved in the expression. Thus, if you attempt to evaluate a condition where a numeric property is compared against a string constant, you will get an error. The comparison operator you choose to use in the expression should also be dictated by the data type of the object property involved. Although it is possible to apply > or < operators to string properties, functionally, it may not make much sense. For instance, the following query retrieves all instances of Win32_Service class where the string that represents the start mode property value is greater than the string 'Auto', when sorted in alphabetical order:

SELECT * FROM Win32_Service WHERE StartMode > 'Auto'

Although this query parses and even produces results, its functional value is questionable.

All string comparisons in WQL are case-insensitive. Thus, all following queries produce equivalent results:

SELECT * FROM Win32_Service WHERE StartMode = 'auto' SELECT * FROM Win32_Service WHERE StartMode = 'AUTO' SELECT * FROM Win32_Service WHERE StartMode = 'Auto'

When it comes to querying WMI based on the values of Boolean object properties, some restrictions apply. First, you can only use the =, and !=, or <>operators in queries that involve Boolean properties. Using any other operator will make no sense, thus WMI will flag any such query as an error. Second, you can only compare Boolean properties against appropriate constants that represent Boolean values. You can use predefined true and false keywords or the integer constants 1 and 0, where 1 corresponds to true and 0 corresponds to false. Therefore, you can use either of the following two queries to select all currently running services:

SELECT * FROM Win32_Service WHERE Started = true SELECT * FROM Win32_Service WHERE Started = 1

Another keyword that can be used to replace a constant in WQL search expression is NULL. A NULL, just like an infamous relation database NULL, represents the absence of a value, and, in the context of WMI, it may be used to identify those properties that are empty. In an attempt to be ANSI-compliant, WQL offers special IS or IS NOT operators that should be used in all expressions that involve NULL values. The alternative syntax, however, which uses an equal sign, is supported as well. Thus, both of the following queries correctly list all disk drives for which the file system cannot be determined:

SELECT * FROM Win32_LogicalDisk WHERE FileSystem IS NULL SELECT * FROM Win32_LogicalDisk WHERE FileSystem = NULL

Note that both IS and IS NOT operators may only be used in expressions that involve NULL values. If these operators are applied to constants other than NULL, they cause WMI to reject the query as invalid. For example, the following attempt to find all disk drives that have the NTFS file system will result in error:

SELECT * FROM Win32_LogicalDisk WHERE FileSystem IS "NTFS"

Now that you have experimented with WQL queries for a while, you may notice that the default behavior for any query is to return not only the instances of the class that are specified in the FROM clause, but also all the instances of its subclasses. The following query, for example, will return objects of type Win32_Service, Win32_ApplicationService, Win32_SystemDriver, and so on, because all of these classes inherit from the CIM_Service class:

SELECT * FROM CIM_Service

However, sometimes, you do not want this behavior, because the objective of a given query may be to select only those subclasses of a particular class that belong to one or more specified classes. Thus, the intent of the last query example may be only to return those service objects that belong to either Win32_Service or Win32_ApplicationService classes, but not to the Win32_SystemDriver class. To achieve this, you have to resort to using the system properties of a class. As you may remember, a class of an object is identified by its __CLASS system property; therefore, the following query will limit the result set based on the specified class name:

SELECT * FROM CIM_Service WHERE __CLASS = 'Win32_Service' OR __CLASS = 'Win32_ApplicationService'

You can query by system properties other than __CLASS. The following query, for instance, will correctly retrieve an instance of the Win32_Process class identified by its __RELPATH system property:

SELECT * FROM Win32_Process WHERE __RELPATH = 'Win32_Process.Handle="100"'

Note that when you query using __RELPATH, you must spell out the name of the key property of an object (Handle in the example above). When processing WQL queries, WMI will not automatically expand the path string, therefore, unless you supply the fully expanded value, this query will not return any results.

Interestingly, if you try to query using some system properties, such as __PATH or __NAMESPACE, the query will be flagged by WMI as invalid. The reason for such a restriction is that WQL queries operate in a certain namespace to which the client application is connected; or in other words, they are bound to a certain location. Thus, if you allow queries that peruse the object location information, you will introduce ambiguity. The special treatment that __PATH and __NAMESPACE properties receive may seem a bit inconsistent; however, it doesn't really matter because querying by these properties certainly does not make much sense—it is much easier to retrieve the object directly.

Frankly, __CLASS is the only system property that seems to be useful when it comes to querying. If you find yourself attempting to construct a query that retrieves an object based on the values of system properties other than __CLASS, you may want to rethink your approach and see whether querying is really necessary.

Another peculiar aspect of WMI behavior is how it treats the system properties of an object that are returned by a query. Normally, if the * placeholder is used as a property list in a SELECT statement, each object, returned by a query, will have all of its system properties properly populated. However, if a SELECT statement contains a list of nonsystem object properties to be returned, the only system properties that are populated by WMI for each output object are __CLASS, __RELPATH, __GENUS, and __PROPERTY_COUNT. Note that the __PROPERTY_COUNT will reflect the actual number of the properties selected rather than the total number of properties in the class definition. This behavior is logical because * stands for all properties—system and nonsystem. To further illustrate this point, look at the following example:

SELECT __PATH FROM Win32_Process

Although this query does not use the *, the returned object will contain the correctly populated __PATH property. The only conclusion that you can draw from this is that certain system properties, such as __PATH, __NAMESPACE, and a few more, are only returned when you request them either explicitly or implicitly with *. This is the default behavior of WMI query processing engine, but you may alter it by passing a special flag, WBEM_FLAG_ENSURE_LOCATABLE, to IWbemServices::ExecQuery method. You will see how to set this flag through the EnumerationOptions type later in this chapter (see "Using ManagementObjectSearcher").

Using SELECT for Embedded Object Queries

If you take a close look at Listing 3-1 (which appeared earlier in this section), you may notice that object property names may be prefixed with object identifier names. At first, this may not make much sense, because, unlike SQL, WQL disallows joins between multiple objects. When processing joins, the ability to use prefixes to distinguish between the properties of the multiple objects involved is certainly valuable. However, for WQL queries, which seem to always operate on a single object or class, prefixing property names with object identifiers looks like an unnecessary complication. Moreover, if you attempt to run the following query, WMI will reject it as invalid:

SELECT Win32_Process.Name FROM Win32_Process WHERE Win32_Process.Handle = 100

It turns out that, even though the joins are disallowed, WMI can process certain queries where multiple objects are involved. It is entirely possible that an arbitrary object contains one or more other objects, often referred to as embedded objects. Consider the following scenario:

class Foo { [key] string Name; string Description; uint32 ID; }; class Bar { [key] uint32 ID; Foo EmbedObj; };

Here class Bar contains an embedded object of type Foo. If an instance of Bar exists and houses an embedded instance of Foo, such that the ID property of Foo is 1, you may use the following query:

SELECT * FROM Bar WHERE EmbedObj.ID = 1

This query retrieves the instance of Bar based on the value of the property of its embedded object Foo.

Be careful when you retrieve objects based on the properties of their embedded objects. Normally, WMI ensures the validity of a query by checking on the existence of all properties referenced in the WHERE clause and SELECT property list. These checks, however, are not carried out for the properties of an embedded object. Thus, a query that references the nonexistent properties of an embedded object in its WHERE clause will not fail; it will simply return no results:

SELECT * FROM Bar WHERE EmbedObj.Handle = 1

The situation is a bit more interesting when properties of embedded objects are referenced in the SELECT property list rather than in its WHERE clause. It turns out that the embedded object property name portion of the fully qualified name is simply ignored. Thus, the following two queries will produce identical results:

SELECT EmbedObj FROM Bar SELECT EmbedObj.ID FROM Bar

In both cases, the output object contains a single property, EmbedObj, that refers to an embedded instance of Foo with all its properties fully populated.

Finally, there are cases when you want to retrieve an object based on the class of its embedded object rather than on the values of the embedded object properties. One approach that comes to mind right away is something similar to the following code:

SELECT * FROM Bar WHERE EmbedObj.__CLASS = 'Foo'

Although querying by the value of the __CLASS system property of an embedded object is a viable solution that will produce the correct results, there is a better tactic for achieving the same goal. In fact, WQL has a special operator, ISA, that solves the problem neatly and effectively. Although ISA has several applications when it comes to data queries, it can be used as follows:

SELECT * FROM Bar WHERE EmbedObj ISA 'Foo'

In reality, the ISA operator does more then the previous query, which used the __CLASS system property of an embedded object in its search criteria. In addition to retrieving all instances of Bar, which contain embedded objects of type Foo, ISA will also return all Bar objects that are housing embedded objects that are derived from Foo. Let us consider the following situation:

class Foo { [key] string Name; string Description; uint32 ID; }; class FooDerived : Foo { uint32 Property1; uint32 Property2; }; class Bar { [key] uint32 ID; object EmbedObj; };

Given that multiple instances of Bar exist and that some of these instances house an embedded instance of Foo while others contain embedded objects of type FooDerived, the following query will return all Bar objects:

SELECT * FROM Bar WHERE EmbedObj ISA 'Foo'

Such an effect cannot be achieved by querying based on the value of the __CLASS system property of an embedded object. You may be a bit surprised to see fictitious classes Foo and Bar being used to illustrate the concept of querying by properties of an embedded object. The reason that I do not refer to any of the elements of the standard CIM model is that embedded objects are rarely exploited, and it seems that a typical out-of-the-box WMI installation does not contain any classes that house embedded objects, with the exception of some specialized entities that represent method parameters, events, and status objects. However, if you remember the discussion of object associations, you may disagree with this statement—associations do indeed contain properties that refer to other objects. As a result, it should be possible to issue the following query to retrieve all instances of the Win32_AccountSID association based on the Name property of its embedded object of type Win32_Account:

SELECT * FROM Win32_AccountSID WHERE Element.Name = 'Administrator'

As attractive as this code may look, it is not going to work; WMI will flag this query as invalid. The problem here is that association objects, such as Win32_AccountSID, do not contain embedded objects—they contain object references. An object reference is, essentially, a pointer to an instance of another object that exists independently from the association object. Thus, retrieving associations with the SELECT statement based on the properties of the associated objects is simply not supported. There is, however, another approach to achieving the same goal, which will be discussed later in this chapter (see REFERENCES OF Statement).

Using SELECT for Schema Queries

All the query examples I have shown you this far in this chapter have been data queries that were designed to retrieve instances of certain WMI classes. Although facilitating the retrieval of management data is one of the primary goals of WQL, sometimes retrieving the WMI schema information is of equal importance. Coincidentally, the very same SELECT statement that you use in data queries is fully equipped for dealing with the schema. As a matter of fact, querying for class definitions is more straightforward than retrieving the management data, although there are a few subtle details of which you should be aware.

Perhaps, the main difference between data and schema queries is the class name used in the FROM clause of the SELECT statement. When you are querying for the schema information rather than using a WMI class name, you should use a special class, meta_class. For instance, the following query returns all class definitions within a given namespace:

SELECT * FROM meta_class

Just like in data queries, the result set can be limited by applying an appropriate WHERE clause. There is, however, a difference; you may no longer use the property values in your search criteria. This is where the already familiar ISA operator comes to the rescue:

SELECT * FROM meta_class WHERE __this ISA 'CIM_Service'

Here a special __this keyword is used to designate a target class for the query. In this case, the ISA operator does the same thing it does for data queries—it instructs WMI to return definitions for all classes that are of type CIM_Service as well as those that are its subclasses. Note that you must always use the ISA operator when you are querying using the __this keyword; WMI will complain if you attempt to use the equality operator.

There are cases, when it is necessary to retrieve a definition for a specific class, rather than a class and all its subclasses. The ISA operator is not going to work in this situation; to solve this problem, WQL offers an alternative approach:

SELECT * FROM meta_class WHERE __CLASS = 'Win32_Process'

It turns out that the system properties of a class can be used in schema queries in the same way that these properties are used in data queries. Thus, the query above retrieves the definition for the Win32_Process class using its __CLASS system property. Conceivably, system properties other than __CLASS can be used to query for class definition. The following query, for instance, selects definitions for all classes that are subclasses of the CIM_Service:

SELECT * FROM meta_class WHERE __SUPERCLASS = 'CIM_Service'

Just as it is the case with data queries, using system properties such as __PATH or __NAMESPACE in the WHERE clause causes WMI to reject the query as invalid.

If you take another look at Listing 3-1, you will certainly notice that certain elements of the SELECT statement, such as WITHIN, GROUP, and HAVING clauses, have not been discussed here. These constructs are intended to facilitate event queries and are not used to retrieve either management data or schema definitions. Therefore, the detailed discussion of these clauses is deferred to Chapter 4.

ASSOCIATORS OF Statement

As I already mentioned, the WQL SELECT statement does not support joins between multiple objects. At first this may seem to be a severe restriction, but if you think about it, you will realize that it is a necessity when it comes to querying object models such as CIM. In a typical relational database environment, the relationships between individual entities are not very clearly defined. Although it is possible to identify a relationship by defining primary and foreign keys for database objects, this information is of an advisory nature and does not usually affect the behavior of SQL queries. Thus, you can relate two or more seemingly unrelated objects in a single query.

The situation is entirely different for CIM. Here, the relationships between managed entities are modeled as associations, which are themselves objects. As a result, these relationships are very clearly defined and strictly enforced. Thus, you can achieve an effect similar to that of a conventional SQL join, although the mechanics of it are quite different. Rather than using the SELECT statement for this purpose, WQL provides a special construct, ASSOCIATORS OF, which allows you to navigate the association links and retrieve objects or classes that are logically related to a given entity.

If you remember the discussion of the GetRelated and GetRelatedClasses methods from Chapter 2, this prelude may sound familiar. In fact, both these methods internally construct WQL ASSOCIATORS OF query to retrieve classes or instances, related to a given object.

The formal syntax of the ASSOCIATORS OF statement is shown on Listing 3-2.

Listing 3-2: ASSOCIATORS OF Statement

statement ::= ASSOCIATORS OF {} [WHERE] [AssocClass = ] [RequiredAssocQualifier = ] [RequiredQualifier = ] [ResultClass = ] [ResultRole = ] [Role = ] [SchemaOnly] [ClassDefsOnly]

The most basic form of the ASSOCIATORS OF statement is as follows:

ASSOCIATORS OF {Win32_Service='WinMgmt'}

Without the WHERE clause, the ASSOCIATORS OF returns all instances of WMI classes that are associated with a given object. For instance, the preceding example returns all objects that are somehow related to an instance of the Win32_Service class, which represents the WMI service WinMgmt.

Note that, just like the SELECT statement, the ASSOCIATORS OF statement requires that a relative object path be used to identify an instance for which related objects are retrieved. As I already mentioned, WQL queries exhibit location affinity and cannot retrieve the information that resides in any other namespace than the current one. Thus, the following query, which specifies the full object path, is invalid:

ASSOCIATORS OF {\BCK_OFFICE ootCIMV2:Win32_Service='WinMgmt'}

It is however, perfectly legal to use a relative class path, rather than object path, in the context of the ASSOCIATORS OF statement. For example, the following line of code is a valid WQL statement:

ASSOCIATORS OF {Win32_Service}

The only problem is that this statement will not produce any results. As you may remember from the previous chapter, in order to retrieve WMI instance data, you need to build a query around an object rather than around a class (see Table 2-9). However, if your objective is to retrieve a class definition rather than its instances, a class name may act as a parameter to the ASSOCIATORS OF statement. The only caveat is that you need to explicitly inform WMI that your intention is to perform a schema rather than a data query. Thus, in order to produce meaningful output, you should rewrite the preceding query as follows:

ASSOCIATORS OF {Win32_Service} WHERE SchemaOnly

Here the SchemaOnly flag of the WHERE clause advises WMI that a query is to be executed against the schema rather than against the data, so the output should include classes that are related to Win32_Service rather than instances. Interestingly, there is another flag, ClassDefsOnly, that has a similar effect. It instructs WMI to only include the class definitions in the result set. The difference is that ClassDefsOnly does not make a query into a schema query; instead, it is just a convenience feature that allows a data query to produce schema output. To see this point further illustrated, consider the following schema query:

ASSOCIATORS OF {Win32_Process} WHERE SchemaOnly

As expected, this query returns a class definition for Win32_ComputerSystem—the only class that Win32_Process is associated with via a containment association Win32_SystemProcesses. A conceptually similar query using ClassDefsOnly flag may be written as follows:

ASSOCIATORS OF {Win32_Process=100} WHERE ClassDefsOnly

This query returns two class definitions: Win32_ComputerSystem and CIM_DataFile. This result is quite peculiar because a CIM schema does not contain an association class that links the latter with Win32_Process. However, a superclass of Win32_Process, CIM_Process, is linked to the CIM_DataFile through the CIM_ProcessExecutable association class. Since the second query is really a data query, it analyses instances rather than classes and returns related classes based on actual rather than schema relationships.

These two flags are not interchangeable: you can only use ClassDefsOnly with data queries where the object path points to an instance rather than a class; and SchemaOnly is only valid when you apply the ASSOCIATORS OF statement to a class.

The remaining subclauses of the WHERE clause allow you to further control the behavior of the ASSOCIATORS OF statement and are very similar to the parameters of the GetRelatedClasses method, discussed previously. The AssocClass keyword specifies the name of an association class or the class from which an association is derived. This association connects the current object or class with its related instances or classes. For instance, to retrieve all instances associated with a given instance of Win32_Service through an association of type CIM_SystemComponent, you may use the following query:

ASSOCIATORS OF {Win32_Service='WinMgmt'} WHERE AssocClass = CIM_SystemComponent

You may use the Role keyword to further restrict the output by specifying the role of the current object or class in an association. Thus, to retrieve just those related objects that act as containers for Win32_Service instances, you would use the following query:

ASSOCIATORS OF {Win32_Service='WinMgmt'} WHERE AssocClass = CIM_SystemComponent Role=PartComponent

This query requests WMI to return all the instances that are linked to a given instance of the Win32_Service class via a CIM_SystemComponent contaiment association so that the Win32_Service object is pointed to by the PartComponent association endpoint, or in other words, the Win32_Service object is being contained. You can express the same query differently using the ResultRole keyword, which specifies the role of the result object or class in an association:

ASSOCIATORS OF {Win32_Service='WinMgmt'} WHERE AssocClass = CIM_SystemComponent ResultRole=GroupComponent

Here the output contains all the objects that are connected to a given instance of Win32_Service via the CIM_SystemComponent containment association and act in a capacity of a container—in other words, all those objects that are being pointed to by a GroupComponent endpoint of an association.

The ResultClass keyword requests that the output only include classes or objects that belong to or are derived from a given class. For example, you may remember that Win32_Service objects are associated with an instance of the Win32_ComputerSystem class via containment associations of type CIM_SystemComponent. Thus, to retrieve an instance of the Win32_ComputerSystem that is connected to a given Win32_Service class, you may use the following query:

ASSOCIATORS OF {Win32_Service='WinMgmt'} WHERE ResultClass = Win32_ComputerSystem

Interestingly, the ResultClass keyword cannot be used with the ClassDefsOnly flag; if you make any attempt to do so, WMI will reject the query as invalid. As restrictive as it may sound, this limitation is actually quite logical—if the result class is known in advance, there are better ways to retrieve its schema.

Finally, the RequiredQualifier and RequiredAssocQualifier indicate that a result class or an association class must have certain qualifiers to warrant inclusion in the result set:

ASSOCIATORS OF {Win32_Service='WinMgmt'} WHERE RequiredAssocQualifier = Association

Here the query retrieves all objects related to a given instance of the Win32_Service class through an association class, which is explicitly tagged with the Association qualifier.

Thanks to the versatility of the ASSOCIATORS OF statement, relationship queries may grow fairly complex, thus allowing you fine control over the output. However, certain syntax restrictions do apply. First, the only WQL comparison operation allowed in ASSOCIATORS OF queries is =. For instance, the following query, which attempts to use a '!=' operator, will be flagged by WMI as invalid:

ASSOCIATORS OF {Win32_Process=100} WHERE RequiredQualifier != Dynamic

Second, compound conditionals that contain AND, OR, and NOT operators are disallowed. Furthermore, although it is legal to use several of the above-mentioned keywords in a single query, given that these keywords are not mutually exclusive, the only thing that may appear as a separator between individual criteria is white space. Thus, both queries below are invalid and will be rejected by WMI:

ASSOCIATORS OF {Win32_Process=100} WHERE ResultClass=CIM_DataFile AND Role=Dependent ASSOCIATORS OF {Win32_Process=100} WHERE ResultClass=CIM_DataFile, Role=Dependent

REFERENCES OF Statement

Just like GetRelationships and GetRelationshipClasses methods discussed in the previous chapter, the REFERENCES OF statement is designed to retrieve instances or definitions of association classes that refer to a given WMI object or class. Truth be told, the REFERENCES OF statement is the underpinning of the GetRelationships and GetRelationshipClasses—both of these methods internally construct the REFECENCES OF query based on the input parameters.

The formal definition of the REFERENCES OF statement is shown on Listing 3-3.

Listing 3-3: REFERENCES OF Statement

statement ::= REFERENCES OF {} [WHERE] [RequiredQualifier = ] [ResultClass = ] [Role = ] [SchemaOnly] [ClassDefsOnly]

The WHERE clause is optional, so the following statement will return all instances of association classes that refer to an instance of Win32_Process:

REFERENCES OF {Win32_Process=100}

Similar to the ASSOCIATORS OF, the REFERENCES OF may take either an object or a class path as a parameter, as long as it is a relative rather than absolute path. To obtain meaningful results when you are using a class path, you should specify the WHERE clause with SchemaOnly keyword:

REFERENCES OF {Win32_Process} WHERE SchemaOnly

This will effectively turn this into a schema query so that the association class definitions rather than instances will be returned.

Another way to obtain classes instead of instances is by using the ClassDefsOnly keyword. However, you should remember that using ClassDefsOnly in a data query is not equivalent to using a schema query. Thus, the preceding query, which is a true schema query, will return the definitions for only those associations that have the Win32_Process class as one of their end-points. In this particular case, the only association class that is returned is Win32_SystemProcesses. The following statement, however, behaves differently; in addition to returning the definition of Win32_SystemProcesses, it also returns the CIM_ProcessExecutable class:

REFERENCES OF {Win32_Process=100} WHERE ClassDefsOnly

The difference in output is explained by the fact that the latter query examines instance rather than schema data and therefore returns actual associations even though the association endpoints may be defined as superclasses of a given class. Thus, the CIM_ProcessExecutable association has a CIM_Process rather than a Win32_Process as one of its endpoints, but since there are outstanding instances of it that reference subclasses of the CIM_Process, it is picked up by the query.

To further restrict the output, the REFERENCES OF statement offers the ResultClass keyword, which allows you to retrieve only the specified association classes. For instance, to get all instances of the CIM_ProcessExecutable association class or association instances that are derived from this class so that these associations refer to a given instance of Win32_Process, you may use the following query:

REFERENCES OF {Win32_Process=100} WHERE ResultClass = CIM_ProcessExecutable

The ResultClass keyword is not compatible with the ClassDefsOnly flag and any attempt to use these two keywords together in a single query will be flagged as an error.

Another subclause, which may be used to gain more control over the output of the REFERENCES OF statement, is Role. This keyword indicates that the class or object to which the statement applies should play a certain role in the association. Thus, in order to retrieve all associations where an arbitrary Win32_Process object acts as a dependent, you should use the following query:

REFERENCES OF {Win32_Process=100} WHERE Role=Dependent

Here the role of an object is identified by the name of the property of an association class that represents an association endpoint. The query returns instances of the CIM_ProcessExecutable association, which links Win32_Process objects with instances of CIM_DataFile, so that the Win32_Process object is connected to the endpoint that is designated by the Dependent property of the association class.

Finally, the RequiredQualifier keyword lets you further filter the result set based on the qualifiers that are defined for certain association classes. Hence, the following query returns just the associations that are tagged with Association qualifier:

REFERENCES OF {Win32_Process=100} WHERE RequiredQualifier = Association

Similar to the ASSOCIATORS OF statement, the syntax of the REFERENCES OF statement is fairly restrictive; compound conditionals are disallowed and the only legal comparison operator is =.

System Management Query Support Types

To facilitate all aspects of WQL query handling, the designers of the .NET system management framework outfitted the System.Management namespace with several query support types. Besides aiding with query-based data retrieval, these types provide backing for other operations such as query assembly, parsing, and analysis. The remainder of this chapter is a comprehensive overview of the WQL query support functionality that is afforded by the System.Management namespace types.

Using ManagementObjectSearcher

ManagementObjectSearcher is the centerpiece of the WQL query handling machinery that is available within the System.Management namespace. This type is very straightforward and easy to use because it has a single purpose—to execute queries. To see how trivial it is to execute an arbitrary WQL query, take a look at the following snippet of code:

ManagementObjectSearcher ms = new ManagementObjectSearcher( "SELECT * FROM Win32_Process"); foreach(ManagementObject mo in ms.Get()) { Console.WriteLine(mo["__PATH"]); }

This code first creates an instance of ManagementObjectSearcher by invoking one of its constructors, which takes a WQL query string as a parameter. Once the ManagementObjectSearcher is constructed and ready to use, the code calls its Get method and iterates through the returned ManagementObjectCollection using the foreach loop.

The WQL query, when passed as an argument to the ManagementObjectSearcher constructor, does not have to be a SELECT statement— the ASSOCIATORS OF and REFERENCES OF queries are handled in exactly the same fashion:

ManagementObjectSearcher ms = new ManagementObjectSearcher("ASSOCIATORS OF {Win32_Process=100}"); foreach(ManagementObject mo in ms.Get()) { Console.WriteLine(mo["__PATH"]); }

The same goes for schema queries. The only thing that sets a schema query apart from its data counterpart is the query statement itself, as well as the fact that the ManagementObjectCollection, which is returned by the Get method, will contain classes rather than instances:

ManagementObjectSearcher ms = new ManagementObjectSearcher( "SELECT * FROM meta_class WHERE __this ISA 'Win32_Process'"); foreach(ManagementClass mo in ms.Get()) { Console.WriteLine(mo["__PATH"]); }

As I briefly mentioned before, WQL queries themselves may not include any information, that specifies the location of a WMI class or object of interest. In other words, the only piece of class or object identification allowed in a query is either the relative object path or the class name. Therefore, the earlier examples would always query the \rootCIMV2 namespace on the local machine, which is not always the behavior you want. One way to overcome this limitation is to use the DefaultPath static property of the ManagementPath type to change the global default namespace path (see Chapter 2 for details):

ManagementPath.DefaultPath = new ManagementPath(@"\BCK_OFFICE ootCIMV2"); ManagementObjectSearcher ms = new ManagementObjectSearcher( "SELECT * FROM Win32_Process"); foreach(ManagementObject mo in ms.Get()) { Console.WriteLine(mo["__PATH"]); }

This code does achieve the objective of producing a list of all the processes that are running on the remote system BCK_OFFICE. This approach, however, is less than perfect because the default namespace setting is changed at the global level and will affect not only this query, but all subsequent operations as well. Although you can save the initial default setting and restore it when the query completes, doing something like that is not only error-prone, but also just plain ugly.

There is a much better solution, which comes in a form of an overloaded constructor method for the ManagementObjectSearcher type. This constructor takes two string parameters: a scope, which is essentially a namespace path, and a WQL query. Hence, the previous example can be rewritten as follows:

ManagementObjectSearcher ms = new ManagementObjectSearcher(@"\BCK_OFFICE ootCIMV2", "SELECT * FROM Win32_Process"); foreach(ManagementObject mo in ms.Get()) { Console.WriteLine(mo["__PATH"]); }

The namespace path can also be set using the Scope property of the ManagementObjectSearcher type. This property refers to an object of type ManagementScope, which is used to control the WMI connection establishment process. Since it is not my intention to discuss the functionality afforded by the ManagementScope type until Chapter 8, I will just say that it allows you to set the namespace path for subsequent operations. Thus, the following snippet of code produces exactly the same results as the previous example:

ManagementObjectSearcher ms = new ManagementObjectSearcher( "SELECT * FROM Win32_Process"); ms.Scope.Path = new ManagementPath(@"\BCK_OFFICE ootCIMV2"); foreach(ManagementObject mo in ms.Get()) { Console.WriteLine(mo["__PATH"]); }

As you may remember from our discussion of the WQL SELECT statement, queries that explicitly specify object properties to be selected return partially populated objects. Thus, certain system properties of objects returned by such query will not be populated. Let us take a look at the following code:

ManagementObjectSearcher ms = new ManagementObjectSearcher( "SELECT ExecutablePath FROM Win32_Process WHERE Handle = 100"); foreach(ManagementObject mo in ms.Get()) { foreach(PropertyData pd in mo.SystemProperties) { Console.WriteLine("{0} {1}", pd.Name, pd.Value); } }

The output shows that all system properties that identify the location of a returned object, such as __SERVER, __NAMESPACE, and __PATH, are blank.

You can alter this default behavior by supplying an instance of the EnumerationOptions type to the constructor of the ManagementObjectSearcher. You may recall that the EnumerationOptions type allows you to control different aspects of an enumeration operation such as the block size of the read operation, the inclusion of localized information in the output, and more. Although most of the functionality afforded by this type has already been discussed in Chapter 2, one of its properties, EnsureLocatable, has an interesting effect on the results of a WQL query. By setting this option to the TRUE value, you may instruct WMI to always populate those system properties that identify the location of an object. Hence, the following code correctly prints out the values of location-specific system properties:

EnumerationOptions eo = new EnumerationOptions(); eo.EnsureLocatable = true; ManagementObjectSearcher ms = new ManagementObjectSearcher(null, "SELECT ExecutablePath FROM Win32_Process WHERE Handle = 100", eo); foreach(ManagementObject mo in ms.Get()) { foreach(PropertyData pd in mo.SystemProperties) { Console.WriteLine("{0} {1}", pd.Name, pd.Value); } }

Here the instance of the EnumerationOptions with the EnsureLocatable flag turned on is passed as a last argument to the constructor of the ManagementObjectSearcher. Note that this version of the constructor takes three parameters: a scope string that contains the namespace path, the query string, and the EnumerationOptions object. When you are connecting to a default namespace on a local machine, a null value can be substituted for the scope string.

You can achieve the same result slightly more easily by using the Options property of the ManagementObjectSearcher, which refers to an object of type EnumerationOptions. Thus, the previous example can be simplified as follows:

ManagementObjectSearcher ms = new ManagementObjectSearcher( "SELECT ExecutablePath FROM Win32_Process WHERE Handle = 100"); ms.Options.EnsureLocatable = true; foreach(ManagementObject mo in ms.Get()) { foreach(PropertyData pd in mo.SystemProperties) { Console.WriteLine("{0} {1}", pd.Name, pd.Value); } }

Finally, just like the other .NET system management types, the ManagementObjectSearcher has built-in provisions for processing WQL queries in an asynchronous fashion. The usage pattern for an asynchronous query execution is similar to that of a direct object retrieval—it revolves around the ManagementOperationObserver object, passed as a parameter to the overloaded version of the Get method. For example, the following code asynchronously retrieves all instances of the Win32_Process class:

class Monitor { bool bComplete = false; public bool Completed { get { return bComplete; } set { bComplete = value; } } public void OnCompleted(object sender, CompletedEventArgs ea) { Completed = true; } public void OnObjectReady(object sender, ObjectReadyEventArgs ea) { Console.WriteLine(ea.NewObject["__PATH"]); } public static void Main(string[] args) { Monitor mo = new Monitor(); ManagementOperationObserver ob = new ManagementOperationObserver(); ob.Completed += new CompletedEventHandler(mo.OnCompleted); ob.ObjectReady += new ObjectReadyEventHandler(mo.OnObjectReady); ManagementObjectSearcher ms = new ManagementObjectSearcher( "SELECT * FROM Win32_Process"); ms.Get(ob); while(!mo.Completed) { System.Threading.Thread.Sleep(1000); } } }

This code creates an instance of ManagementOperationObserver, sets up the handlers for its Completed and ObjectReady events, and then invokes the asynchronous version of the ManagementObjectSearcher Get method. The results of the SELECT query are delivered one-by-one to the ObjectReady event handler and are accessed through the NewObject property of the ObjectReadyEventArgs object.

Using Query Types

Besides aiding with query execution, the System.Management namespace provides several types for query parsing and analysis. At the root of the type hierarchy there is an abstract type, called ManagementQuery, which is designed to serve as a basis for deriving more specialized management query types. In addition to a few protected methods for parsing the query text, ManagementQuery defines two public properties: QueryLanguage and QueryString. These two properties are all that ManagementObjectSearcher needs to process the query. In fact, if you look at the signature of the IWbemServices::ExecQuery method that is called internally by ManagementObjectSearcher, you will notice that this method expects the language identifier and the query text as its first two parameters. Thus, it seems like implementing a custom query type is a fairly simple matter:

class MyQuery : ManagementQuery { public MyQuery(string lang, string query) { QueryLanguage = lang; QueryString = query; } public override object Clone() { return this.MemberwiseClone(); } }

Note that you need to supply an implementation for Clone method of ICloneable interface, since ManagementQuery, which includes the interface, leaves this method unimplemented.

Theoretically, it should be possible to pass this custom query object to the overloaded constructor ManagementObjectSearcher instead of just the plain query string:

MyQuery query = new MyQuery("WQL", "SELECT * FROM Win32_Process"); ManagementObjectSearcher ms = new ManagementObjectSearcher(query);

Although ManagementObjectSearcher does have a constructor that takes a query object rather than the plain query string, the code above will not compile. The problem is that this constructor, rather than accepting an object that descends from ManagementQuery directly, expects an instance of ObjectQuery type, which is a subclass of ManagementQuery.

As you may remember, WMI supports three general categories of queries: data, schema, and event queries. ManagementObjectSearcher is designed to handle only those queries that return objects or class definitions—data or schema queries—and is unsuitable for processing event queries. Thus, the System.Management namespace offers two specialized subclasses of ManagementQuery: ObjectQuery and EventQuery. These subclasses are intended to handle object and event queries respectively. To enforce this separation of duties, the constructor of ManagementObjectSearcher expects the parameter of type ObjectQuery, while the constructors of ManagementEventWatcher (a type that is dedicated to handling WMI events) accept parameters of type EventQuery. The details of event handling as well as the functionality afforded by EventQuery and ManagementEventWatcher types, will be discussed in the next chapter.

ObjectQuery is a very simple type that is similar to the MyQuery type shown earlier. Besides the QueryLanguage and QueryString properties that it inherited from ManagementQuery, it has three constructor methods: a parameterless one, one that takes a query string parameter, and another one that takes a string parameter, which represents the query language, as well as the query string. The following snippet of code illustrates how ObjectQuery can be used with ManagementObjectSearcher:

ObjectQuery query = new ObjectQuery("WQL", "SELECT * FROM Win32_Process"); ManagementObjectSearcher ms = new ManagementObjectSearcher(query); foreach(ManagementObject mo in ms.Get()) { Console.WriteLine(mo["__PATH"]); }

Note that passing the WQL language identifier to the ObjectQuery constructor is optional. To save yourself some typing, you may choose to use a single-parameter constructor, which defaults the language to WQL:

ObjectQuery query = new ObjectQuery("SELECT * FROM Win32_Process"); ManagementObjectSearcher ms = new ManagementObjectSearcher(query); foreach(ManagementObject mo in ms.Get()) { Console.WriteLine(mo["__PATH"]); }

Interestingly, the ability to specify the query language has little value because the only supported language for querying WMI is WQL. It is conceivable, however, that future releases of WMI will support alternative query languages, such as XPath, for instance. Until then, explicitly setting the query language is not required and is best avoided. In fact, the only logical reason for having a generic, language-agnostic object query type seems to be the flexibility of being able to provide specialized, language-specific query types. In other words, ObjectQuery is not intended to be used directly and instead, it should serve as a base class for specialized types, that incorporate features, pertinent to a specific query language. Currently, the only available specialization of ObjectQuery is WqlObjectQuery, but it is entirely possible that in the near feature the System.Management namespace may include something like XPathObjectQuery.

WqlObjectQuery is not much different from its parent and does not offer any new features. Instead, it restricts the functionality of ObjectQuery by making its QueryLanguage property read-only, thus preventing a user from overriding the default value WQL set by the constructor of ManagementQuery. With that in mind, you can use WqlObjectQuery to rewrite the previous as follows:

WqlObjectQuery query = new WqlObjectQuery("SELECT * FROM Win32_Process"); ManagementObjectSearcher ms = new ManagementObjectSearcher(query); foreach(ManagementObject mo in ms.Get()) { Console.WriteLine(mo["__PATH"]); }

As you can see, neither ObjectQuery nor WqlObjectQuery really aid with query parsing or analysis, and as a result, they seem to add little value to query handling. You may find this somewhat puzzling since there is no obvious advantage in using these types versus a plain query string. The reason for their use, once again, is specialization—these types are not intended to be used directly, rather they are designed to serve as base classes for other types that zero in on particular categories of WQL queries.

As mentioned earlier, WQL is a simple language consisting of just three statements. The syntactic structure of two of them, ASSOCIATORS OF and REFERENCES OF, is very similar, so similar, in fact, that you can use a common algorithm to parse. The SELECT statement, on the other hand, is strikingly different and necessitates a special parsing logic. Thus, rather than trying to shoehorn the parsing code for the entire WQL language into a single class, the designers of the System.Management namespace created three separate subclasses of WqlObjectQuery—SelectQuery, RelatedObjectQuery, and RelationshipQuery— each of which is responsible for handling a specific WQL statement.

Using SelectQuery Type

As its name implies, the first of these three types, SelectQuery, is dedicated to handling WQL SELECT queries. SelectQuery makes parsing the query text and breaking it into individual components extremely easy:

SelectQuery query = new SelectQuery( "SELECT * FROM Win32_Process WHERE Handle = 100"); Console.WriteLine("Class Name: {0}", query.ClassName); Console.WriteLine("Condition: {0}", query.Condition); Console.WriteLine("Selected Properties:"); foreach(string prop in query.SelectedProperties) { Console.WriteLine(" {0}", prop); } Console.WriteLine("Schema Query: {0}", query.IsSchemaQuery);

Here, as soon as the constructor of SelectQuery is invoked, it calls the setter routine for the QueryString property; this in turns calls the internal method ParseQuery, which is responsible for breaking the query text into individual components. Once the SelectQuery object is constructed, these components are accessible through its ClassName, Condition, SelectedProperties, and IsSchemaQuery properties. The ClassName and Condition properties are self-explanatory—they contain the name of the WMI class and the query WHERE criteria respectively. IsSchemaQuery is just a Boolean flag that indicates whether it is a data (FALSE) or schema (TRUE) query. Finally, SelectedProperties is a string collection that houses the names of the WMI object properties that are returned by the query. With that in mind, the output, produced by the code above, should come as no surprise:

Class Name: Win32_Process Condition: Handle = 100 Selected Properties: Schema Query: False

Actually, let me take that back—the output is somewhat surprising because the SelectedProperties collection seems to be empty. The query ues the * placeholder so that it is logical to expect the SelectedProperties collection to contain the names of all properties of Win32_Process class. The problem is that the SelectQuery type does not interact with WMI by itself and, therefore, it is not capable of fetching the class definition in order to expand the * placeholder. Thus, it is safe to assume that the empty SelectedProperties collection implies that all of the object properties are to be returned by the query. Conversely, if you explicitly mention the WMI object properties in the SELECT property list, the SelectedProperties collection will be correctly populated:

SelectQuery query = new SelectQuery( "SELECT Handle, ExecutablePath FROM Win32_Process WHERE Handle = 100"); Console.WriteLine("Selected Properties:"); foreach(string prop in query.SelectedProperties) { Console.WriteLine(" {0}", prop); }

Thus, this code will produce the following output:

Selected Properties: Handle ExecutablePath

The string parameter, taken by the constructor of the SelectQuery type, does not necessarily have to be a WQL query. Supplying just a class name will yield the same results, hence the following code will return all instances of Win32_Process class:

SelectQuery query = new SelectQuery("Win32_Process"); ManagementObjectSearcher ms = new ManagementObjectSearcher(query); foreach(ManagementObject mo in ms.Get()) { Console.WriteLine(mo["__PATH"]); }

If you construct an instance of SelectQuery using a class name and then read its QueryString property, you will see a valid WQL query:

SelectQuery query = new SelectQuery("Win32_Process"); Console.WriteLine(query.QueryString);

This code will produce the following output:

select * from Win32_Process

Judging from the output, you may assume that the constructor of the SelectQuery type always assembles a valid WQL SELECT statement when it is invoked with the class name parameter. This, however, is not quite the case; the constructor simply sets the ClassName property. In fact, the SELECT statement only gets built whenever the QueryString property is referenced—it is the get property routine of the QueryString that invokes the internal BuildQuery method that is responsible for assembling a valid SELECT statement from individual components.

A query may not necessarily need to be assembled in order for ManagementObjectSeracher to process it. Whenever the Get method of ManagementObjectSearcher is invoked, it analyzes a query to establish the optimum execution strategy. It does this by following these steps:

  1. First, it checks whether a query has selection criteria. In other words, it checks whether the query's Condition property is populated.
  2. Then it looks at the SelectedProperties collection to find out whether it is empty (whether all or just some WMI object properties are requested).
  3. Finally, it checks the EnumerationOptions object associated with ManagementObjectSeracher to determine if the EnumerateDeep flag is set. In the context of a data query, this flag indicates whether an enumeration operation returns instances of all subclasses of a given class or just the instances of its immediate subclasses.

If a query satisfies all three of these criteria—it has no condition, it selects all properties, and it has its EnumerateDeep flag set to TRUE—the Get method does not even attempt to execute it via IWbemServices::ExecQuery. Instead, it calls the IWbemServices::CreateInstanceEnum method. This method, which expects a class name rather than a WQL statement, is just a more efficient way to achieve the same results. In effect, using this method means that even if a SelectQuery object is instantiated with a valid WQL SELECT statement, this SELECT is not used, unless it violates one of the three criteria just mentioned.

Thus, you have probably realized that using data queries without the WHERE clause is not such a good idea. There are more efficient and certainly cleaner ways of producing the same results. For instance, you may remember the GetInstances method of ManagementClass type that was described in the previous chapter. This method does essentially the same thing as the query I was just discussing—it retrieves all instances of a particular WMI class. Interestingly, its implementation is also very similar: it calls IWbemServices::CreateInstanceEnum passing it the name of the WMI class. Hence, the following code fragment is functionally identical to the previous example, based on SelectQuery, but it is much cleaner and a bit more efficient:

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

Here the intention is clear because the code is much more readable. A small performance gain is achieved since there is no need to parse and analyze the query string. Note that you must pass the EnumerationOptions object with EnumerateDeep property set to TRUE to the GetInstances method to correctly emulate the behavior of a WQL SELECT query. By default, data queries always return instances of all the subclasses of a given class.

You may think that setting the QueryString property to a class name rather than a WQL statement will have an effect similar to that of calling the constructor of SelectQuery with a class name parameter. This, however, is not the case, and the following code will throw ArgumentException:

SelectQuery query = new SelectQuery(); query.QueryString = "Win32_Process";

Here the exception is thrown by the internal method ParseQuery that is invoked by the property set routine for QueryString. Apparently, a query has to be validated in order to be parsed and a mere class name does not constitute a valid WQL statement. It is, however, perfectly legal to use the ClassName property in the same fashion. Thus, the following code will work, correctly retrieving all instances of the Win32_Process class:

SelectQuery query = new SelectQuery(); query.ClassName = "Win32_Process"; ManagementObjectSearcher ms = new ManagementObjectSearcher(query); foreach(ManagementObject mo in ms.Get()) { Console.WriteLine(mo["__PATH"]); }

This effectively means that you can easily construct a query from its individual components without resorting to ugly text manipulation. This may come in very handy, especially when you are building graphical applications where certain controls on the GUI correspond to the elements of a WQL SELECT statement. The following code builds a valid and fairly complex WQL query:

SelectQuery query = new SelectQuery(); query.ClassName = "Win32_Service"; query.Condition = "StartMode = 'Auto' AND State = 'Stopped'"; query.SelectedProperties.Add("Name"); query.SelectedProperties.Add("DisplayName"); query.SelectedProperties.Add("StartMode"); Console.WriteLine(query.QueryString);

The output, produced by this code will be the following:

select Name, DisplayName, StartMode from Win32_Service where StartMode = 'Auto' AND State = 'Stopped'

For convenience, SelectQuery provides a constructor that takes two parameters: the name of the WMI class and the query condition. Thus, the process of assembling a query can be simplified as follows:

SelectQuery query = new SelectQuery("Win32_Service", "StartMode = 'Auto' AND State = 'Stopped'"); Console.WriteLine(query.QueryString);

Yet, another constructor allows you to specify a list of selected properties in addition to the class name and condition:

SelectQuery query = new SelectQuery("Win32_Service", "StartMode = 'Auto' AND State = 'Stopped'", new string[] {"Name", "DisplayName", "StartMode"}); Console.WriteLine(query.QueryString);

Contrary to what you may think, SelectQuery does not automatically detect schema queries. This is mildly disappointing since all it takes is scanning the query text for the presence of the meta_class keyword. Unfortunately, if you intend to construct a schema query, it has to be stated explicitly:

SelectQuery query = new SelectQuery(true, "__this ISA 'CIM_Process'"); Console.WriteLine(query.QueryString);

Here the first parameter to the constructor is a Boolean flag that indicates that a schema query is required. The second argument is not a query, rather it is a query selection criteria. Note that if you attempt to pass a full query instead of just the condition, SelectQuery will assemble an invalid WQL statement since it does not check whether the second argument starts with SELECT.

A schema query does not have to be marked as such in order to produce the expected results. The following code fragment will correctly return all class definitions for the CIM_Process and its descendants, although the IsSchemaQuery property of the SelectQuery object will return FALSE:

SelectQuery query = new SelectQuery( "SELECT * FROM meta_class WHERE __this ISA 'CIM_Process'"); ManagementObjectSearcher ms = new ManagementObjectSearcher(query); foreach(ManagementClass mc in ms.Get()) { Console.WriteLine(mc["__PATH"]); }

A query can also be marked as a schema query by setting its IsSchemaQuery property to TRUE. This very interesting effect is illustrated by the following code snippet:

SelectQuery query = new SelectQuery("CIM_Process"); query.IsSchemaQuery = true; Console.WriteLine(query.QueryString);

You may rightfully expect to see a query assembled as follows:

SELECT * FROM meta_class WHERE __this ISA 'CIM_Process'

Curiously, the output, produced by this code, is totally different and may not seem very logical at first:

SELECT * FROM meta_class

It turns out that once a query is marked as schema query, the class name passed to the constructor is essentially ignored. Moreover, even if you pass a valid WQL statement with no WHERE criteria to the constructor, it will be ignored as well. Thus, the following code will produce exactly the same output:

SelectQuery query = new SelectQuery("SELECT * FROM Win32_Process"); query.IsSchemaQuery = true; Console.WriteLine(query.QueryString);

The most interesting thing, however, happens if you create a SelectQuery object by passing a WQL SELECT with a WHERE clause to its constructor. Consider the following code:

SelectQuery query = new SelectQuery( "SELECT * FROM Win32_Process WHERE Handle = 100"); query.IsSchemaQuery = true; Console.WriteLine(query.QueryString);

Surprisingly, this code is going to output the following query:

select * from meta_class where Handle = 100

This one is not even a valid schema query and it will generate an exception if you attempt to execute it. However, if a SelectQuery object is created with a valid schema query, the output will be a valid schema query:

SelectQuery query = new SelectQuery( "SELECT * FROM meta_class WHERE __this ISA 'CIM_Process'"); query.IsSchemaQuery = true; Console.WriteLine(query.QueryString);

Whenever the IsSchemaQuery property of the SelectQuery object is assigned, its set property routine calls an internal function BuildQuery, that is responsible for generating a query string. When invoked with the IsSchemaQuery property set to TRUE, BuildQuery simply strings together the constant string select * from meta_class and the contents of the Condition property of the SelectQuery object. So, you should be careful when you are creating schema queries because the query string that is assembled by the SelectQuery type may not always be what you expect. Perhaps, the safest way to achieve the right results is by using a SelectQuery constructor, which takes a Boolean IsSchemaQuery flag and the query condition as parameters.

Finally, if you are interested in the internals of query processing, you should know that schema queries are processed similarly to data queries. If a query satisfies the same three criteria—it has no WHERE clause, it selects all object properties, and it is processed with the EnumerateDeep flag set to TRUE—it will not be executed via the IWbemServices::ExecQuery method. Just as it is the case with data queries, for the sake of efficiency, ManagementObjectSearcher will call the IWbemServices::CreateClassEnum method. The IWbemServices::CreateClassEnum takes a class name parameter, which specifies a name of the superclass for all returned classes. Thus, theoretically, you should be able to set the ClassName property of a schema query object and then retrieve just the subclasses of a given class. In other words, the following code should output just the subclasses of CIM_Process:

SelectQuery query = new SelectQuery(); query.IsSchemaQuery = true; query.ClassName = "CIM_Process"; ManagementObjectSearcher ms = new ManagementObjectSearcher(query); ms.Options.EnumerateDeep = true; foreach(ManagementClass mc in ms.Get()) { Console.WriteLine(mc["__PATH"]); }

However, if you run this code, you will see that it outputs all classes in the current namespace, just as a condition-less schema query does. It turns out that a constructor of ManagementObjectSearcher clones the query object by invoking its Clone method. Clone checks the IsSchemaQuery property of its object and ensures that a cloned instance has a blank ClassName property in case it is a schema query. This phenomena is illustrated by the following code:

SelectQuery query = new SelectQuery(); query.IsSchemaQuery = true; query.ClassName = "CIM_Process"; ManagementObjectSearcher ms = new ManagementObjectSearcher(query); query = (SelectQuery)ms.Query; Console.WriteLine(query.ClassName);

The bottom line is that when the Get method of ManagementObjectSearcher is invoked, it calls IWbemServices::CreateClassEnum with a blank class name parameter; this results in all classes being returned by the enumeration. As strange as it may sound, this is done to ensure that the behavior is consistent with the normal behavior of a schema query without a WHERE clause. After all, if you want to retrieve just the subclasses of a given class, you should turn to the GetSubclasses method of the ManagementClass type that was described in the previous chapter.

The SelectQuery type is, perhaps, the most interesting of the three types representing the respective WQL statements. The remaining two types—RelatedObjectQuery and RelationshipQuery—are fairly simple and self-explanatory, mostly due to the more rigid syntax of the ASSOCIATORS OF and REFERENCES OF statements. Both of these types are structured in such a way that their properties map one-to-one to the similarly named syntax elements of their respective WQL statement.

Using the RelatedObjectQuery Type

RelatedObjectQuery represents the WQL ASSOCIATORS OF statement. Just like a SelectQuery, an instance of RelatedObjectQuery can be created by passing a query string to its constructor:

RelatedObjectQuery query = new RelatedObjectQuery( @"ASSOCIATORS OF {Win32_Service='WinMgmt'} WHERE ResultClass = Win32_ComputerSystem"); ManagementObjectSearcher ms = new ManagementObjectSearcher(query); foreach(ManagementObject mo in ms.Get()) { Console.WriteLine(mo["__PATH"]); }

Once a query object is created, the query text is parsed and corresponding object properties are set accordingly. Thus, if you attempt to print out the SourceObject and RelatedClass properties of the preceding RelatedObjectQuery, the output will look like the following:

Related Class: Win32_ComputerSystem Source Object: Win32_Service='WinMgmt'

Note that, unlike SelectQuery, the RelatedObjectQuery type does not have a Condition property. Instead, the query criteria is broken down into individual elements, such as RelatedClass, RelatedQualifier, RelatedRole, and so o. The QueryString, QueryLanguage, and IsSchemaQuery properties are still available. This time, however, since ASSOCIATORS OF schema queries are unambiguously identified by the SchemaOnly keyword, the IsSchemaQuery property will be correctly set because the query string represents a schema query. Thus, the following code will print TRUE on the console:

RelatedObjectQuery query = new RelatedObjectQuery( "ASSOCIATORS OF {Win32_Service} WHERE SchemaOnly"); Console.WriteLine(query.IsSchemaQuery);

This also holds true for other query elements such as ClassDefsOnly. The following code sample will print TRUE on the console as well:

RelatedObjectQuery query = new RelatedObjectQuery( "ASSOCIATORS OF {Win32_Service='WinMgmt'} WHERE ClassDefsOnly"); Console.WriteLine(query.ClassDefinitionsOnly);

Conversely, the ASSOCIATORS OF query can be assembled out of individual components as follows:

RelatedObjectQuery query = new RelatedObjectQuery(); query.SourceObject = "Win32_Service='WinMgmt'"; query.RelatedClass = "Win32_ComputerSystem"; Console.WriteLine(query.QueryString);

This code will produce the following output, which represents perfectly a valid ASSOCIATORS OF query:

associators of {Win32_Service='WinMgmt'} where resultclass = Win32_ComputerSystem

There is more than one way to create an instance of RelatedObjectQuery type. For your convenience, this type offers a few overloaded constructors, which allow you to build a query from its individual components:

public RelatedObjectQuery ( string sourceObject , string relatedClass )

where the parameters are defined as follows:

public RelatedObjectQuery ( string sourceObject , string relatedClass , string relationshipClass , string relatedQualifier , string relationshipQualifier , string relatedRole , string thisRole , bool classDefinitionsOnly )

where

and

public RelatedObjectQuery ( bool isSchemaQuery , string sourceObject , string relatedClass , string relationshipClass , string relatedQualifier , string relationshipQualifier , string relatedRole , string thisRole )

where

Using the RelationshipQuery Type

The remaining query type. RelationshipQuery, which embodies REFERENCES OF statement, is very similar to RelatedObjectQuery. Its properties are a one-to-one reflection of the corresponding syntax elements of the REFERENCES OF statement, and its behavior is conceptually the same as the behavior of RelatedObjectQuery.

You can create a fully functional instance of the RelationshipQuery type by calling one of its constructors, which takes a string parameter that represents the REFERENCES OF query:

RelationshipQuery query = new RelationshipQuery( "REFERENCES OF {Win32_Process=100} WHERE ResultClass = CIM_ProcessExecutable"); ManagementObjectSearcher ms = new ManagementObjectSearcher(query); foreach(ManagementObject mo in ms.Get()) { Console.WriteLine(mo["__PATH"]); }

Once such an instance is created, the query text is parsed and the object properties are set appropriately to reflect respective syntax elements of the query. Thus, if you print out the SourceObject and RelationshipClass properties of the preceding RelationshipQuery object, you will see the following output:

SourceObject: Win32_Process=106 RelationshipClass: CIM_ProcessExecutable

You can assemble a valid query object by assigning its properties to the appropriate values. Thus, the following code example is equivalent of the previous one:

RelationshipQuery query = new RelationshipQuery(); query.SourceObject = "Win32_Process=100"; query.RelationshipClass = "CIM_ProcessExecutable"; ManagementObjectSearcher ms = new ManagementObjectSearcher(query); foreach(ManagementObject mo in ms.Get()) { Console.WriteLine(mo["__PATH"]); }

Finally, the RelationshipQuery type offers several convenience constructors that allow you to build a query from individual components:

public RelationshipQuery ( string sourceObject , string relationshipClass )

where the parameters are defined as follows:

public RelationshipQuery ( string sourceObject , string relationshipClass , string relationshipQualifier , string thisRole , bool classDefinitionsOnly )

where

and

public RelationshipQuery ( bool isSchemaQuery , string sourceObject , string relationshipClass , string relationshipQualifier , string thisRole )

where

Alternatives to Query Types

When it comes to parsing queries, RelatedObjectQuery and RelationshipQuery types are very useful. However, in my opinion, these types have little value for the purposes of retrieving the data. Although it is entirely possible to read and analyze just about any kind of relationship information using either RelatedObjectQuery or RelationshipQuery along with ManagementObjectSearcher, there are better ways of achieving the same goal. For instance, you may remember the GetRelated and GetRelationships methods of the ManagementObject type, as well as the GetRelatedClasses and GetRelationshipClasses methods of the ManagementClass type, described in the previous chapter. Consider the following example:

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

Functionally, this code is equivalent to the previous example involving RelationshipQuery and ManagementObjectSearcher types. However, the code here is much clearer and easier to read. Interestingly, all four methods—GetRelated, GetRelationships, GetRelatedClasses and GetRelationshipClasses—are implemented in terms of ASSOCIATORS OF and REFERENCES OF WQL queries, although these implementation details are hidden from a casual user. Basically, unless you need to parse an existing query, consider using the relationship methods of either the ManagementObject or ManagementClass types before you resort to queries. You will end up with much simpler code, or at least save yourself some typing.

Summary

WQL and the associated query support functionality of the System.Management namespace are extremely powerful and, on occasion, simply indispensable. Having read this chapter and worked though the code examples, you should be able to

You should now possess enough information to build sophisticated applications that are capable of retrieving and analyzing any kind of management data that is accessible through WMI.

Категории