Search

Using the Network Functions in C# (Part I - User Functions)

0 views

Overview of Network Functions in C#

When you work with user accounts on Windows, you often find yourself choosing between a heavyweight solution like Active Directory and a lightweight, direct call to the Win32 API. Active Directory gives you powerful directory services but it requires a domain controller, extra licensing, and a learning curve that can be overkill for small workstations or home labs. For most local scenarios, the Win32 network management functions provide a straightforward path to create, delete, and modify user accounts without the overhead of a full directory service.

These native APIs are exposed in the netapi32.dll library and include calls such as NetUserAdd, NetUserDel, NetUserGetInfo, and NetUserSetInfo. To use them from C# you need to declare the function signatures with the correct parameters and data structures, then marshal the data back and forth between managed and unmanaged memory. The process is known as Platform Invoke, or P/Invoke, and it is a reliable bridge between the .NET world and the unmanaged world.

Because the functions follow a consistent pattern - pass a server name, a data structure, and receive an NTSTATUS code - once you understand one you can apply the same approach to the rest. This article walks through the most common user‑management tasks: adding and removing users, querying and updating user data, changing passwords, enumerating all local users, and checking group membership. The code samples use the USER_INFO_1 structure for simplicity, but you can switch to the more detailed USER_INFO_4 when you need to set additional properties such as description or home directory.

The following sections dive into each step, starting with how to set up your project for P/Invoke, then covering each function in turn. By the end of the guide you will have a small but complete toolkit for local user management that you can drop into any C# application.

Preparing the Project for Platform Invoke

Before calling any Win32 API functions, you must import the System.Runtime.InteropServices namespace. This namespace provides the DllImport attribute that tells the CLR where to find the unmanaged code and how to marshal parameters.

Open your project in Visual Studio, right‑click the project node, choose AddClass, name it NetApiFunctions.cs, and add the following using directive at the top of the file:

Prompt
using System;</p> <p>using System.Runtime.InteropServices;</p>

Next, create a static class that will host all of the P/Invoke declarations. Keep this class focused: declare only the functions you need, and use the simplest structure that satisfies the operation. For user management you’ll need the following:

Prompt
public static class NetApiFunctions</p> <p>{</p> <p> [DllImport("netapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]</p> <p> public static extern int NetUserAdd(</p> <p> string servername,</p> <p> uint level,</p> <p> ref USER_INFO_1 buf,</p> <p> out uint parm_err);</p> <p> public static extern int NetUserDel(</p> <p> string username);</p> <p> public static extern int NetUserGetInfo(</p> <p> string username,</p> <p> uint level,</p> <p> out IntPtr bufptr);</p> <p> public static extern int NetUserSetInfo(</p> <p> uint level,</p> <p> public static extern int NetUserChangePassword(</p> <p> string oldpassword,</p> <p> string newpassword);</p> <p> public static extern int NetUserEnum(</p> <p> uint level,</p> <p> uint filter,</p> <p> out IntPtr bufptr,</p> <p> uint prefmaxlen,</p> <p> out uint entriesread,</p> <p> out uint totalentries,</p> <p> out IntPtr resume_handle);</p> <p> public static extern int NetUserGetLocalGroups(</p> <p> uint level,</p> <p> uint flags,</p> <p> out uint totalentries);</p> <p> public static extern int NetApiBufferFree(IntPtr buffer);</p> <p>}</p>

Each function uses the CharSet.Unicode flag because the Win32 API expects Unicode strings. The SetLastError = true flag allows you to retrieve the native error code with Marshal.GetLastWin32Error() if an API call fails.

The USER_INFO_1 structure is defined next. This structure contains the username, password, and a few key attributes. For more advanced settings you can replace it with USER_INFO_4 later.

Prompt
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]</p> <p>public struct USER_INFO_1</p> <p>{</p> <p> public string usri1_name;</p> <p> public string usri1_password;</p> <p> public uint usri1_password_age;</p> <p> public uint usri1_priv;</p> <p> public string usri1_home_dir;</p> <p> public string usri1_comment;</p> <p> public uint usri1_flags;</p> <p> public string usri1_script_path;</p> <p>}</p>

With the declarations in place, you can now call the functions from anywhere in your project. The next sections explain how to use each of the functions for typical user‑management tasks.

Adding Users with NetUserAdd

Creating a new account is one of the most common operations when managing a workstation. The NetUserAdd function accepts a server name (or null for the local machine), a level indicating the structure type, and the user data structure itself.

Below is a complete example that adds a user called TestUser with a simple password and basic privileges. The account is marked as a local administrator by setting the UF_SCRIPT flag to 0x0001 and the UF_PASSWD_NOTREQD flag to 0x0020 if you wish to create a user without a password. For demonstration purposes we use a normal user privilege level (UF_NORMAL_ACCOUNT, 0x0200).

Prompt
USER_INFO_1 user = new USER_INFO_1</p> <p>{</p> <p> usri1_name = "TestUser",</p> <p> usri1_password = "P@ssw0rd!",</p> <p> usri1_password_age = 0,</p> <p> usri1_priv = 1, // USER_PRIV_USER</p> <p> usri1_home_dir = null,</p> <p> usri1_comment = "Sample account created by NetUserAdd",</p> <p> usri1_flags = 0x0200, // UF_NORMAL_ACCOUNT</p> <p> usri1_script_path = null</p> <p>};</p> <p>uint parm_err;</p> <p>int result = NetApiFunctions.NetUserAdd(null, 1, ref user, out parm_err);</p> <p>if (result == 0)</p> <p>{</p> <p> Console.WriteLine("User added successfully.");</p> <p>}</p> <p>else</p> <p>{</p> <p> Console.WriteLine($"NetUserAdd failed with code {result}. Parameter error: {parm_err}");</p> <p>}</p>

The function returns zero on success. Any non‑zero value indicates an error. The parm_err parameter shows which field in the structure caused the failure; for example, a value of 2 means the usri1_password field was invalid.

To add a user on a remote machine, replace the null server name with the UNC name of the target computer, such as \\RemotePC. You must run the application with administrative rights on that remote machine; otherwise, the call will fail with an access denied error.

When creating multiple accounts, you can iterate over a list of usernames and call NetUserAdd for each. Handle the returned status codes carefully, and consider wrapping the logic in a helper method that logs failures and continues with the next user. This pattern ensures that a single failure does not abort the entire batch operation.

Removing Users with NetUserDel

Deleting an account is straightforward. The NetUserDel function only needs the server name and the username. Unlike NetUserAdd, it does not require a structure or a level.

Prompt
string usernameToDelete = "TestUser";</p> <p>int deleteResult = NetApiFunctions.NetUserDel(null, usernameToDelete);</p> <p>if (deleteResult == 0)</p> <p>{</p> <p> Console.WriteLine($"{usernameToDelete} removed.");</p> <p>}</p> <p>else</p> <p>{</p> <p> Console.WriteLine($"Failed to delete {usernameToDelete}. Error code: {deleteResult}");</p> <p>}</p>

Again, passing null targets the local computer. For remote deletion, specify the computer’s UNC name. Be mindful that some accounts may be in use by services or scheduled tasks; attempting to delete such accounts can result in an ERROR_ACCESS_DENIED or ERROR_USER_EXISTS status code.

In many scripts you’ll find a check before deletion to confirm the account exists. Use NetUserGetInfo with level 0 or 1 to retrieve a minimal record; if the call succeeds, proceed with deletion. This avoids unnecessary error handling for non‑existent accounts.

Retrieving and Updating User Information

The NetUserGetInfo and NetUserSetInfo functions work in tandem. First you pull the current data into a structure, modify fields you need, then push the updated structure back. Because NetUserGetInfo returns a pointer to unmanaged memory, you must marshal it into a managed structure before making changes.

Below is a full routine that changes the comment and the flags of a user. It uses USER_INFO_1 to keep the example concise. If you need to adjust the password or other fields, switch to USER_INFO_4 and update the corresponding fields.

Prompt
string targetUser = "TestUser";</p> <p>uint bufferSize;</p> <p>IntPtr bufPtr;</p> <p>int getResult = NetApiFunctions.NetUserGetInfo(null, targetUser, 1, out bufPtr);</p> <p>if (getResult != 0)</p> <p>{</p> <p> Console.WriteLine($"GetInfo failed: {getResult}");</p> <p> return;</p> <p>}</p> <p>USER_INFO_1 current = Marshal.PtrToStructure<USER_INFO_1>(bufPtr);</p> <p>current.usri1_comment = "Updated by NetUserSetInfo";</p> <p>current.usri1_flags = 0x0200; // Ensure NORMAL_ACCOUNT flag</p> <p>uint setParmErr;</p> <p>int setResult = NetApiFunctions.NetUserSetInfo(null, targetUser, 1, ref current, out setParmErr);</p> <p>if (setResult == 0)</p> <p>{</p> <p> Console.WriteLine("User data updated.");</p> <p>}</p> <p>else</p> <p>{</p> <p> Console.WriteLine($"SetInfo failed: {setResult}, parameter error: {setParmErr}");</p> <p>}</p> <p>NetApiFunctions.NetApiBufferFree(bufPtr);</p>

Notice the call to NetApiBufferFree after processing the buffer. This is mandatory; failing to free the buffer will leak memory in your process.

When you need to change more sensitive data like the password, avoid using NetUserSetInfo. Instead, call NetUserChangePassword or NetUserSetInfo with a new password field. The password field in USER_INFO_1 must be provided as a plain string; the system handles encryption internally.

In a real application, wrap this logic in a try/catch block and provide user‑friendly error messages based on the error codes returned. You can also create a mapping from common NTSTATUS codes to descriptive messages for easier debugging.

Changing Passwords with NetUserChangePassword

When a user must update their own password, the NetUserChangePassword function offers a simple two‑step validation. The caller supplies the old password and the new desired password; the system verifies the old password before making the change.

Prompt
string userName = "TestUser";</p> <p>string oldPwd = "P@ssw0rd!";</p> <p>string newPwd = "NewP@ss1!";</p> <p>int pwdResult = NetApiFunctions.NetUserChangePassword(null, userName, oldPwd, newPwd);</p> <p>if (pwdResult == 0)</p> <p>{</p> <p> Console.WriteLine("Password changed successfully.");</p> <p>}</p> <p>else</p> <p>{</p> <p> Console.WriteLine($"Password change failed: {pwdResult}");</p> <p>}</p>

Because the function requires the old password, it is often used in user‑interactive scenarios such as a “Change Password” dialog. For administrative password resets where you do not know the current password, you must instead set the UF_PASSWD_NOTREQD flag and then use NetUserSetInfo to assign a new password. This bypasses the old‑password check but requires elevated privileges.

Always enforce password complexity rules in your environment; otherwise, the function may reject the new password with a ERROR_PASSWORD_RESTRICTION code. If you encounter this, review the local security policy settings or consult your domain’s password policy if you’re on a domain machine.

In scripts that automate password changes, remember to mask the new password in logs and avoid printing it in clear text. Store it temporarily only in memory, and clear the buffer afterward to reduce the risk of credential leakage.

Enumerating All Users with NetUserEnum

To produce a list of every user account on a machine, use NetUserEnum. This function returns a buffer containing an array of USER_INFO_0 structures when you request level 0, which contains only the username. It also accepts a filter to limit results to local accounts, domain accounts, or everyone.

Prompt
const uint LEVEL = 0;</p> <p>const uint FILTER_NORMAL_ACCOUNT = 0x0002;</p> <p>IntPtr bufferPtr;</p> <p>uint entriesRead;</p> <p>uint totalEntries;</p> <p>int enumResult = NetApiFunctions.NetUserEnum(</p> <p> null,</p> <p> LEVEL,</p> <p> FILTER_NORMAL_ACCOUNT,</p> <p> out bufferPtr,</p> <p> uint.MaxValue,</p> <p> out entriesRead,</p> <p> out totalEntries,</p> <p> out IntPtr resumeHandle);</p> <p>if (enumResult != 0)</p> <p>{</p> <p> Console.WriteLine($"NetUserEnum failed: {enumResult}");</p> <p> return;</p> <p>}</p> <p>int structSize = Marshal.SizeOf(typeof(USER_INFO_0));</p> <p>for (int i = 0; i <p>{</p> <p> IntPtr currentPtr = new IntPtr(bufferPtr.ToInt64() + i * structSize);</p> <p> USER_INFO_0 userInfo = Marshal.PtrToStructure<USER_INFO_0>(currentPtr);</p> <p> Console.WriteLine($"- {userInfo.usri0_name}");</p> <p>}</p> <p>NetApiFunctions.NetApiBufferFree(bufferPtr);</p>

The USER_INFO_0 structure is minimal:

public struct USER_INFO_0

{

public string usri0_name;

}

When you want to pull additional details such as the home directory or user comment, switch the level to 1 or 3. Keep in mind that higher levels produce larger buffers and increase memory usage.

After enumeration, always free the buffer with NetApiBufferFree. Neglecting to do so leads to memory leaks that accumulate over repeated enumeration calls.

For administrators managing many machines, you can pair NetUserEnum with a remote server name. Loop over a list of computer names and gather all user lists into a single report. This technique powers audit scripts that verify user provisioning across a lab environment.

Discovering Group Membership with NetUserGetLocalGroups

Knowing which groups a user belongs to is crucial for permission checks and audit trails. The NetUserGetLocalGroups function returns a list of group names for a specified user. It accepts a level parameter; level 0 returns only the group name.

IntPtr groupBuffer;

uint groupEntriesRead;

uint groupTotalEntries;

int grpResult = NetApiFunctions.NetUserGetLocalGroups(

null,

targetUser,

0,

0,

out groupBuffer,

uint.MaxValue,

out groupEntriesRead,

out groupTotalEntries);

if (grpResult != 0)

{

Console.WriteLine($"GetLocalGroups failed: {grpResult}");

return;

}

int structSize = Marshal.SizeOf(typeof(LOCALGROUP_USERS_INFO_0));

for (int i = 0; i

{

IntPtr currentPtr = new IntPtr(groupBuffer.ToInt64() + i * structSize);

LOCALGROUP_USERS_INFO_0 grpInfo = Marshal.PtrToStructure<LOCALGROUP_USERS_INFO_0>(currentPtr);

Console.WriteLine($"Group: {grpInfo.lgrui0_name}");

}

NetApiFunctions.NetApiBufferFree(groupBuffer);

The LOCALGROUP_USERS_INFO_0 structure is defined as follows:

public struct LOCALGROUP_USERS_INFO_0

{

public string lgrui0_name;

}

When you need more than just the name - such as the SID or group type - request a higher level. For most auditing purposes, the names suffice.

Remote group queries work the same way as other functions: supply the computer’s UNC name in the first parameter. The user running the application must have the necessary rights to read the local group memberships on that machine.

Combine group enumeration with the user list from the previous section to build a comprehensive view of who can do what on your network. This data can feed into role‑based access control tools or security dashboards.

Sample Project and Resources

All the code snippets above can be assembled into a single console application that demonstrates adding a user, updating their properties, listing all users, showing group membership, and finally deleting the account. The project includes error handling, resource cleanup, and comments to guide you through each step.

Download the full source code from the following link. The repository contains a Visual Studio 2022 solution, a NetApiFunctions.cs helper file, and a Program.cs that walks through the complete workflow.

  • Network User Management Sample on GitHub
  • Download the ZIP archive

    Feel free to clone the repository, open the solution in Visual Studio, and run the application. It prompts for input at each step, allowing you to see the results directly in the console. If you run into permission errors, ensure you launch Visual Studio or the command prompt with administrative rights.

    With these tools in hand, you can quickly create scripts that automate user provisioning, perform audits, or build management utilities for small workgroups or home labs. The Win32 network functions are reliable, fast, and free of the extra infrastructure that Active Directory demands. They give you fine‑grained control over local accounts without leaving the comfort of the .NET ecosystem.

Suggest a Correction

Found an error or have a suggestion? Let us know and we'll review it.

Share this article

Comments (0)

Please sign in to leave a comment.

No comments yet. Be the first to comment!

Related Articles