Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Enumerating Network Resources

0.00/5 (No votes)
27 Feb 2004 1  
Using the WNetEnumResource API from C#

Introduction

I'm very new to C# as a programming language and to .NET as a framework. Coming as I did from a C++ and MFC background I researched the alternatives and hinted to my family that a copy of Tom Archers 'Inside C#' (2nd edition) would be a much appreciated Christmas present. The hints were taken and it arrived in my Christmas stocking. A good read and very well worth the money that I don't offically know about :). Having read the book from cover to cover it was time to write some real C# code. But I'm of the school of thought that says that one cannot truly learn a new programming language by using examples targetted at just one or another feature of the new language. I need a real project. Otherwise, if I attempt something and it doesn't work the way I imagine it should it's all too easy to write it off. A real project presents challenges that must be solved before the project can be said to be complete.

The Project

I chose a project that's pretty much going to be out of date in maybe a years time, probably rather less. It's a SETI monitor that automatically discovers computers on my local network and monitors the progress of instances of SETI running on each computer. This article doesn't present that project (though a future article might).

Instead, this article focusses on a class I wrote as part of the project to enumerate network resources. In Win32 you do this by using the WNetOpenEnum() API followed by (probably) multiple calls to the WNetEnumResource() API.

A not so quick search of the .NET documentation didn't reveal any classes implementing this functionality so it was time to write my own. Bear in mind that this code was written by a veteran C++ programmer trying to move to C# :)

Moving a C++ function to C#

I have written code using the WNetOpenEnum() and WNetEnumResource() API's many times in the past, always in C++. So I was starting from the standpoint of having written working code but without necessarily understanding all the nuances of the API. After all, if it works following the MSDN examples tailored to my needs one moves on to other stuff right?

So I started with working C++ code. I won't present the code here. But let's discuss the API's. The first API is WNetOpenEnum(). The C/C++ prototype looks like this.

DWORD WNetOpenEnum(
    DWORD dwScope,                // scope of enumeration

    DWORD dwType,                 // resource types to list

    DWORD dwUsage,                // resource usage to list

    LPNETRESOURCE lpNetResource,  // resource structure

    LPHANDLE lphEnum              // enumeration handle buffer

);
I won't go into all the details of the various parameters but I'll pass lightly over them. The scope parameter specifies whether you want to enumerate global resources, remembered resources and suchlike. The type parameter lets you restrict enumerated resources to disk, print or all. Usage lets you specify such things as containers. (See the MSDN documentation for full details).

The lphEnum is a pointer to a handle to an enumeration context which is used in subsequent calls to WNetEnumResource().

That leaves the lpNetResource parameter.

All the parameters apart from the lpNetResource parameter map directly onto C# datatypes. The lpNetResource parameter doesn't. So let's look at the C/C++ definition of that type.

The NETRESOURCE structure

typedef struct _NETRESOURCE { 
    DWORD  dwScope; 
    DWORD  dwType; 
    DWORD  dwDisplayType; 
    DWORD  dwUsage; 
    LPTSTR lpLocalName; 
    LPTSTR lpRemoteName; 
    LPTSTR lpComment; 
    LPTSTR lpProvider; 
} NETRESOURCE; 

All of the members map onto a C# datatype. DWORD maps on the C# uint type and LPTSTR maps onto the string type. So far so good.

So let's see what a C# definition of a NETRESOURCE structure might look like.

[StructLayout(LayoutKind.Sequential)]
private class NETRESOURCE 
{
    public ResourceScope       dwScope = 0;
    public ResourceType        dwType = 0;
    public ResourceDisplayType dwDisplayType = 0;
    public ResourceUsage       dwUsage = 0;
    public string              lpLocalName = null;
    public string              lpRemoteName = null;
    public string              lpComment = null;
    public string              lpProvider = null;
};
This is a literal copy of the NETRESOURCE structure from the C/C++ header file with the syntax massaged to make it digestible to the C# compiler. Well not quite. Each DWORD member has been changed to an enum type, where the enum values are defined elsewhere within the class. The enum values are in turn literal copies of the relevant C/C++ values massaged to make them digestible to the C# compiler. I did it this way for two reasons. The first is that I believe even the original C++ structure should have been defined in this way. Changing the DWORD's into enum's buys some compile time parameter checking for free. The second reason is that (at least in Visual Studio), Intellisense will kick in and help me remember the constants.

The WNetOpenEnum() function

The definition of the WNetOpenEnum() API, for C#, looks like this,
[DllImport("Mpr.dll", EntryPoint="WNetOpenEnumA", 
           CallingConvention=CallingConvention.Winapi)]
private static extern ErrorCodes WNetOpenEnum(ResourceScope dwScope, 
                                              ResourceType dwType, 
                                              ResourceUsage dwUsage, 
                                              NETRESOURCE p,
                                              out IntPtr lphEnum);
That's pretty straightforward. Simply set up a call using the various enum values for the type of enumeration you want, pass it an instance of the NETRESOURCE structure and a reference to a place to store the returned enumeration handle. Calling code might look like this:
IntPtr     handle = IntPtr.Zero;
ErrorCodes result;

result = WNetOpenEnum(ResourceScope.RESOURCE_GLOBALNET, ResourceType.RESOURCETYPE_ANY, 
                      ResourceUsage.RESOURCEUSAGE_ALL, pRsrc, out handle);

The WNetEnumResource() function

Now we've got a handle to an open enumerator we use that handle to request information from the OS.
[DllImport("Mpr.dll", EntryPoint="WNetEnumResourceA", 
           CallingConvention=CallingConvention.Winapi)]
private static extern ErrorCodes WNetEnumResource(IntPtr hEnum, 
                                                  ref uint lpcCount,
                                                  IntPtr buffer, 
                                                  ref uint lpBufferSize);
This is where things get interesting. If you have another look at the NETRESOURCE structure you'll see that it contains 4 pointers to strings. An obvious questions is where do those pointers point? In other words, where is the memory allocated? A few moments thought reveals that they can't be pointing at strings inside the OS because in all likelihood those strings are in someone elses memory space. The API has to copy the strings into memory visible to the calling process. Therefore, the memory into which the strings are copied must be allocated by the calling process. However the documentation says nothing about how large each string buffer should be or even if we should point each pointer at a buffer allocated in our memory space. A close reading of the MSDN documentation for the WNetEnumResource() reveals this in the remarks.

An application cannot set the lpBuffer parameter to NULL and retrieve the required buffer size from the lpBufferSize parameter. Instead, the application should allocate a buffer of a reasonable size (16 kilobytes is typical) and use the value of lpBufferSize for error detection.

Aha! Obviously the WNetEnumResource API reserves the first few bytes of the memory buffer for an instance of the NETRESOURCE structure and copies the strings into the same buffer after that structure. So we're expected to allocate a buffer large enough to hold the NETRESOURCE structure and all 4 strings. In C++ this is trivial. And doubtless, as I become more proficient with C# and the .NET Framework, it'll become trivial there too :)

Incidentally, this also reveals how old these API's are. I'm sure the structure would have used BSTR's to solve the memory allocation problem had they existed at the time this structure was defined.

A hidden gotcha

Another close reading of the documentation for WNetEnumResource() says that if you set the lpCount to X the function will return X results if they will fit into the buffer. If not it will return as many results as will fit in the buffer and set the value pointed at by lpCount to the number of returned results. But in practice it doesn't seem to work that way. All my tests in C++ return just 1 result per call regardless of how large the buffer is and what I specify as the desired count. *shrug*

The WNetCloseEnum() function

As you'd expect, having opened a handle and used it, you're required to close it when you've done with it. That's done with the WNetCloseEnum() API.
[DllImport("Mpr.dll", EntryPoint="WNetCloseEnum", 
           CallingConvention=CallingConvention.Winapi)]
private static extern ErrorCodes WNetCloseEnum(IntPtr hEnum);

Putting it all together

So we've had a look at the 3 API's and the structure we need to enumerate servers on our local network. Here's the enumeration function itself:
private void EnumerateServers(
                NETRESOURCE pRsrc, 
                ResourceScope scope, 
                ResourceType type, 
                ResourceUsage usage, 
                ResourceDisplayType displayType)
{
    uint        bufferSize = 16384;
    IntPtr      buffer  = Marshal.AllocHGlobal((int) bufferSize);
    IntPtr      handle = IntPtr.Zero;
    ErrorCodes  result;
    uint        cEntries = 1;

    result = WNetOpenEnum(scope, type, usage, pRsrc, out handle);

    if (result == ErrorCodes.NO_ERROR)
    {
        do
        {
            result = WNetEnumResource(handle, ref cEntries, buffer, ref bufferSize);

            if (result == ErrorCodes.NO_ERROR)
            {
                Marshal.PtrToStructure(buffer, pRsrc);

                if (pRsrc.dwDisplayType == displayType)
                    aData.Add(pRsrc.lpRemoteName);

                //  If this is a container resource recursively call ourselves to 

                //  enumerate the contents...

                if ((pRsrc.dwUsage & ResourceUsage.RESOURCEUSAGE_CONTAINER) == 
                                     ResourceUsage.RESOURCEUSAGE_CONTAINER)
                    EnumerateServers(pRsrc, scope, type, usage, displayType);
            }
            else if (result != ErrorCodes.ERROR_NO_MORE_ITEMS)
                break;
        } while (result != ErrorCodes.ERROR_NO_MORE_ITEMS);

        WNetCloseEnum(handle);
    }

    Marshal.FreeHGlobal((IntPtr) buffer);
}
This is almost trivial. We pass a NETRESOURCE structure and a bunch of constants to the function. We then open the enumeration and if that succeeds we go into a loop calling WNetEnumResource() for each network resource. For each resource whose type matches the type passed as a parameter we add the remote name member of the structure to an array. For each resource which has the ResourceUsage.RESOURCEUSAGE_CONTAINER bit set we recursively call the function.

But wait a minute! What's with the Marshal.AllocHGlobal() call? That's to allocate a block of memory to serve as the buffer which the WNetEnumResource() API will treat as a NETRESOURCE structure followed by buffer space for the strings.

When we get the buffer back after the call we use Marshal.PtrToStructure() to copy the NETRESOURCE portion of the buffer into the structure instance passed to the function. When the copy is done the string members of the structure still point at the strings allocated within the buffer. I struggled mightily with this (doubtless because of my C++ background). It took seeming ages to understand that even with the unsafe keyword bracketing code the compiler wasn't about to let me simply cast the buffer into a NETRESOURCE structure without some .NET Framework intervention. My thanks go to Heath Stewart for pointing me in the right direction.

Other things about the class

Because the class is used to enumerate a list of servers it makes sense for it to implement the IEnumerable interface to allow it to be used inside a foreach statement. I used a trick Tom Archer mentions in his book. Because the class uses an ArrayList to store the list of enumerated servers and that class already implements IEnumerable my GetEnumerator() implementation simply returns the enumerator for the ArrayList.
public IEnumerator GetEnumerator()
{
    return aData.GetEnumerator();
}
What could be simpler than that?

The enumerator returns strings, so one might use it like this.

ServerEnum servers = new ServerEnum(ResourceScope.RESOURCE_GLOBALNET,
                                    ResourceType.RESOURCETYPE_DISK, 
                                    ResourceUsage.RESOURCEUSAGE_ALL, 
                                    ResourceDisplayType.RESOURCEDISPLAYTYPE_SHARE);

foreach (string s1 in servers)
    Console.WriteLine(s1);
Which example would enumerate all shares on the network. If the second parameter were changed to ResourceType.RESOURCETYPE_PRINT a list of all printer shares would be returned. The sample project download is compiled to show a list of servers on the network. So the relevant code looks like this.
ServerEnum servers = new ServerEnum(ResourceScope.RESOURCE_GLOBALNET,
                                    ResourceType.RESOURCETYPE_DISK, 
                                    ResourceUsage.RESOURCEUSAGE_ALL, 
                                    ResourceDisplayType.RESOURCEDISPLAYTYPE_SERVER);

foreach (string s1 in servers)
    Console.WriteLine(s1);

Control of enumerated results

The bunch of constants that are passed to the EnumerateServers function are used for fine control of the output. The following tables are from the MSDN Library April 2000.

The ResourceScope constants and their meanings are:

Value Meaning
RESOURCE_CONNECTED Enumerate currently connected resources. The dwUsage member cannot be specified.
RESOURCE_GLOBALNET Enumerate all resources on the network. The dwUsage member is specified.
RESOURCE_REMEMBERED Enumerate remembered (persistent) connections. The dwUsage member cannot be specified.

The ResourceType constants are:

Value Meaning
RESOURCETYPE_ANY All resources.
RESOURCETYPE_DISK Disk resources.
RESOURCETYPE_PRINT Print resources.

The ResourceUsage constants are:

Value Meaning
RESOURCEUSAGE_CONNECTABLE The resource is a connectable resource; the name pointed to by the lpRemoteName member can be passed to the WNetAddConnection() function to make a network connection.
RESOURCEUSAGE_CONTAINER The resource is a container resource; the name pointed to by the lpRemoteName member can be passed to the WNetOpenEnum() function to enumerate the resources in the container.

And, finally, the ResourceDisplayType constants are:

Value Meaning
RESOURCEDISPLAYTYPE_DOMAIN The object should be displayed as a domain.
RESOURCEDISPLAYTYPE_SERVER The object should be displayed as a server.
RESOURCEDISPLAYTYPE_SHARE The object should be displayed as a share.
RESOURCEDISPLAYTYPE_GENERIC The method used to display the object does not matter.

The last set of constants, ResourceDisplayType are confusing. The documentation seems to say that it's a hint as to how to display the data. But it actually seems to behave as a filter.

You apply various combinations of these constants to filter the results returned by the network enumerator. For example, if all you're interested in is a list of servers on the network you'd use ResourceScope.RESOURCE_GLOBALNET, ResourceType.RESOURCETYPE_ANY, ResourceUsage.RESOURCEUSAGE_CONTAINER and ResourceDisplayType.RESOURCEDISPLAYTYPE_SERVER.

Note that the class presented in the download includes some constants that aren't documented in MSDN but that are present in the C++ header files. In particular there are many more constants defined for the ResourceDisplayType than MSDN documents. On my system most of them result in nothing being returned at all, but then this is my system, I have a workgroup, not a domain or an Active Directory. I included them for the sake of completeness.

Acknowledgements

I'd like to thank Marc Clifton for reviewing this article for me. As it was my first C# article I was somewhat nervous about posting it so I chose to ask an MVP for review and advice. Marc was very helpful and patient.

History

28 February 2004 - Initial version.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here