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 Add → Class, name it NetApiFunctions.cs, and add the following using directive at the top of the file:
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:
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.
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).
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.
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.
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.
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.
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.





No comments yet. Be the first to comment!