Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / C#
Article

Enumerating Network Resources

Rate me:
Please Sign up or sign in to vote.
4.80/5 (35 votes)
27 Feb 2004CPOL10 min read 207.1K   8.3K   59   32
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.

C++
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

C++
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.

C#
[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,
C#
[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:
C#
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.
C#
[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.
C#
[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:
C#
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.
C#
public IEnumerator GetEnumerator()
{
    return aData.GetEnumerator();
}
What could be simpler than that?

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

C#
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.
C#
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:

ValueMeaning
RESOURCE_CONNECTEDEnumerate currently connected resources. The dwUsage member cannot be specified.
RESOURCE_GLOBALNETEnumerate all resources on the network. The dwUsage member is specified.
RESOURCE_REMEMBEREDEnumerate remembered (persistent) connections. The dwUsage member cannot be specified.

The ResourceType constants are:

ValueMeaning
RESOURCETYPE_ANYAll resources.
RESOURCETYPE_DISKDisk resources.
RESOURCETYPE_PRINTPrint resources.

The ResourceUsage constants are:

ValueMeaning
RESOURCEUSAGE_CONNECTABLEThe 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_CONTAINERThe 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:

ValueMeaning
RESOURCEDISPLAYTYPE_DOMAINThe object should be displayed as a domain.
RESOURCEDISPLAYTYPE_SERVERThe object should be displayed as a server.
RESOURCEDISPLAYTYPE_SHAREThe object should be displayed as a share.
RESOURCEDISPLAYTYPE_GENERICThe 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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
I've been programming for 35 years - started in machine language on the National Semiconductor SC/MP chip, moved via the 8080 to the Z80 - graduated through HP Rocky Mountain Basic and HPL - then to C and C++ and now C#.

I used (30 or so years ago when I worked for Hewlett Packard) to repair HP Oscilloscopes and Spectrum Analysers - for a while there I was the one repairing DC to daylight SpecAns in the Asia Pacific area.

Afterward I was the fourth team member added to the Australia Post EPOS project at Unisys Australia. We grew to become an A$400 million project. I wrote a few device drivers for the project under Microsoft OS/2 v 1.3 - did hardware qualification and was part of the rollout team dealing directly with the customer.

Born and bred in Melbourne Australia, now living in Scottsdale Arizona USA, became a US Citizen on September 29th, 2006.

I work for a medical insurance broker, learning how to create ASP.NET websites in VB.Net and C#. It's all good.

Oh, I'm also a Kentucky Colonel. http://www.kycolonels.org

Comments and Discussions

 
QuestionFAT vs NTFS Pin
baruchl5-Dec-12 15:57
baruchl5-Dec-12 15:57 
QuestionHow to pop-up connect to dialog box in C# Pin
jdkulkarni17-Mar-08 23:14
jdkulkarni17-Mar-08 23:14 
Generalspeed issues Pin
gargamehl26-Mar-07 13:34
gargamehl26-Mar-07 13:34 
GeneralRe: speed issues Pin
thany.nl13-Feb-08 2:14
thany.nl13-Feb-08 2:14 
QuestionIs there a way to detect Network Printers' IP addresses? Pin
Jan Palmer25-Dec-06 21:52
Jan Palmer25-Dec-06 21:52 
General.NET 2.0 has 5000+ classes [modified] Pin
giddy_guitarist25-Aug-06 21:41
giddy_guitarist25-Aug-06 21:41 
GeneralGreat and helpful Article Pin
Raj Lal27-Jun-06 11:01
professionalRaj Lal27-Jun-06 11:01 
NewsHere is the C++ eqivelent in code project i found it Pin
Haitham Khedre29-May-06 8:00
Haitham Khedre29-May-06 8:00 
GeneralRe: Here is the C++ eqivelent in code project i found it Pin
Andreas Saurwein18-Jul-08 4:59
Andreas Saurwein18-Jul-08 4:59 
GeneralAll Read This Article , Simple and Do the same job from June 8, 2001 Pin
Haitham Khedre28-Apr-06 14:20
Haitham Khedre28-Apr-06 14:20 
Generaldosn't retrive all IP'S in LAN Like NetCut Useless Pin
Haitham Khedre28-Apr-06 13:24
Haitham Khedre28-Apr-06 13:24 
GeneralRe: dosn't retrive all IP'S in LAN Like NetCut Useless Pin
Rob Manderson28-Apr-06 13:30
protectorRob Manderson28-Apr-06 13:30 
GeneralRe: dosn't retrive all IP'S in LAN Like NetCut Useless Pin
Haitham Khedre28-Apr-06 14:25
Haitham Khedre28-Apr-06 14:25 
GeneralDose The Code retrive all IP's Like NetCut plz answer Pin
Haitham Khedre27-Apr-06 11:26
Haitham Khedre27-Apr-06 11:26 
Generalevery system in a lan..... Pin
sendmadhavan7-Mar-06 1:27
sendmadhavan7-Mar-06 1:27 
GeneralRe: every system in a lan..... Pin
Haitham Khedre2-May-06 9:46
Haitham Khedre2-May-06 9:46 
GeneralRe: every system in a lan..... Pin
gargamehl26-Mar-07 13:33
gargamehl26-Mar-07 13:33 
Generalfind domains in a lan Pin
sendmadhavan12-Feb-06 17:35
sendmadhavan12-Feb-06 17:35 
GeneralRe: find domains in a lan Pin
Haitham Khedre2-May-06 9:50
Haitham Khedre2-May-06 9:50 
GeneralPerfect codes for me! Pin
RiverwayRun12-May-05 9:12
RiverwayRun12-May-05 9:12 
GeneralRe: Perfect codes for me! Pin
Rob Manderson13-May-05 13:57
protectorRob Manderson13-May-05 13:57 
GeneralHi Pin
The illiterate3-Nov-04 0:57
The illiterate3-Nov-04 0:57 
GeneralConnecting to Network Drive Pin
Sameers Javed2-Nov-04 13:25
Sameers Javed2-Nov-04 13:25 
GeneralRe: Connecting to Network Drive Pin
The illiterate3-Nov-04 1:02
The illiterate3-Nov-04 1:02 
GeneralRe: Connecting to Network Drive Pin
Sameers Javed3-Nov-04 6:31
Sameers Javed3-Nov-04 6:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.