Distribution lists are still common in Microsoft 365 environments. They are used when an organization needs one email address to deliver messages to multiple people, such as a support team, regional office, project group, or internal announcement list.
For developers, the confusing part is that a distribution list looks like a normal group in Microsoft Graph, but it does not have the same management capabilities as a Microsoft 365 group or a security group.
Microsoft Graph can identify distribution groups through the group resource. A traditional distribution group has mailEnabled set to true, securityEnabled set to false, and an empty groupTypes collection. However, Microsoft currently documents traditional distribution groups as read-only through the Microsoft Graph groups API.
This distinction is important before building an application around Graph.
If you need to read and report on distribution lists, Microsoft Graph can help. If you require to create, modify, add members to, or remove members from traditional Exchange distribution groups, you generally need to use Exchange administration capabilities rather than assuming the normal Graph group endpoints support those operations.
There is also a newer distributionList resource in Microsoft Graph beta, but it is a different API surface and currently exposes distribution lists associated with a user's mailbox. Microsoft explicitly states that beta APIs are subject to change and should not be used as the basis for production applications.
Understanding these differences prevents a common implementation mistake: treating every email-enabled group as if it were a Microsoft 365 group.
Distribution Lists vs Microsoft 365 Groups
Before writing any C# code, identify what type of group you are managing.
Microsoft 365 groups are designed for collaboration and can provide shared conversations, calendars, files, and other Microsoft 365 resources.
Traditional distribution groups are primarily used to distribute email messages.
Microsoft's current Exchange documentation distinguishes Microsoft 365 groups, distribution groups, dynamic distribution groups, and mail-enabled security groups as separate group types.
A simplified comparison looks like this:
Group type | Email enabled | Microsoft Graph management |
|---|
Microsoft 365 group | Yes | Supported |
Security group | No | Supported |
Mail-enabled security group | Yes | Read-only through Graph groups API |
Traditional distribution group | Yes | Read-only through Graph groups API |
Dynamic distribution group | Yes | Not supported by Graph groups API |
The last two rows are particularly important.
A traditional distribution group is not equivalent to a Microsoft 365 group just because both have email addresses.
What a .NET Application Can Do With Graph
A .NET application can use Microsoft Graph to discover and inspect distribution groups.
For example, you can build an internal administration tool that:
Finds distribution groups
Displays group names
Displays email addresses
Determines group type
Reports group ownership information where available
Produces an inventory of groups
Finds groups based on naming conventions
Checks whether a group is mail-enabled
Combines group information with other directory information
Microsoft Graph supports filtering groups based on mailEnabled, securityEnabled, and groupTypes. Microsoft's current documentation provides a filter for identifying traditional distribution groups using mailEnabled true, securityEnabled false, and a non-Unified group type.
This is a useful starting point for reporting and administrative dashboards.
Create a .NET Project
Start with a normal .NET application.
For example:
dotnet new console -n DistributionGroupInspector
cd DistributionGroupInspector
Install the Microsoft Graph SDK and Azure Identity package:
dotnet add package Microsoft.Graph
dotnet add package Azure.Identity
The Graph SDK gives the application strongly typed access to Microsoft Graph resources.
Azure.Identity provides authentication mechanisms such as interactive browser authentication and client credentials.
The authentication method you choose depends on how the application will run.
Choose the Authentication Model
There are two common application patterns.
The first is a user-operated administration tool.
Administrator
|
v
.NET application
|
v
Microsoft Entra sign-in
|
v
Microsoft Graph
The second is an unattended service.
Scheduled service
|
v
Application identity
|
v
Microsoft Graph
These models have different security requirements.
A desktop administration tool may use delegated permissions and require an administrator to sign in.
A background service may use application permissions and authenticate as an application.
Microsoft notes that group-related operations require administrator consent, and the exact permission requirements depend on the API operation being performed.
Do not give a background service broad directory permissions simply because they make development easier.
Start with the smallest permission set that supports the operation.
Authenticate From .NET
For a local administration application, interactive authentication can be convenient.
A simplified example is:
using Azure.Identity;
using Microsoft.Graph;
var scopes = new[]
{
"Group.Read.All"
};
var credential = new InteractiveBrowserCredential(
new InteractiveBrowserCredentialOptions
{
TenantId = "YOUR_TENANT_ID",
ClientId = "YOUR_CLIENT_ID"
});
var graphClient = new GraphServiceClient(
credential,
scopes);
The application registration must be configured in Microsoft Entra ID, and the required delegated permission must be granted.
Do not place a client secret directly in source code.
For production applications, credentials should be stored and managed using an appropriate secret-management mechanism.
Find Distribution Groups
Once authenticated, query the groups collection.
A useful filter for traditional distribution groups is:
mailEnabled eq true
and securityEnabled eq false
and groupTypes does not contain Unified
The corresponding Graph request can be represented as:
GET /groups?$filter=NOT groupTypes/any(c:c eq 'Unified') and mailEnabled eq true and securityEnabled eq false
Microsoft documents this filtering pattern for distribution groups and notes that advanced query capabilities are required for this filter.
In C#, the query can be expressed through the Graph SDK:
var groups = await graphClient.Groups.GetAsync(config =>
{
config.QueryParameters.Filter =
"NOT groupTypes/any(c:c eq 'Unified') and " +
"mailEnabled eq true and " +
"securityEnabled eq false";
config.QueryParameters.Select =
new[]
{
"id",
"displayName",
"mail",
"mailNickname",
"mailEnabled",
"securityEnabled",
"groupTypes"
};
});
The exact SDK request-builder syntax can change between SDK generations, so keep the Microsoft Graph SDK version aligned with the documentation for the version used by your project.
The important concept is the query itself: identify the group type before attempting management operations.
Display the Results
A simple console application can turn the Graph response into an administrative report.
if (groups?.Value != null)
{
foreach (var group in groups.Value)
{
Console.WriteLine(
$"{group.DisplayName} - {group.Mail}");
}
}
For an internal administration application, you would normally return a DTO rather than expose Graph SDK objects directly.
For example:
public sealed record DistributionGroupInfo(
string Id,
string DisplayName,
string? EmailAddress,
string? Alias);
Then map the Graph response to your application's own model.
This creates a useful boundary between Microsoft Graph and the rest of your application.
Add Pagination
Do not assume that one Graph request returns every group.
Large tenants can contain many groups, and Graph APIs use paging to return collections.
The SDK handles page traversal differently depending on the SDK version, so your application should explicitly design for pagination.
Conceptually:
Request page 1
↓
Read groups
↓
Check next page
↓
Request page 2
↓
Read groups
↓
Continue until no next page exists
A reporting application that works correctly in a small development tenant can silently become incomplete in a larger production tenant if it reads only the first page.
This is a common directory-integration mistake.
Add Search and Filtering
Do not retrieve every group and then filter everything in C# if Microsoft Graph can perform the filtering server-side.
For example, if the application needs distribution groups with a particular naming convention, combine the supported query capabilities with appropriate filtering.
A simplified example might be:
Distribution groups
|
+-- Region
+-- Department
+-- Environment
+-- Project
Instead of loading every group into memory:
var allGroups = await GetAllGroupsAsync();
var supportGroups = allGroups
.Where(g => g.DisplayName.Contains("Support"));
prefer a server-side query when the Graph API supports the required filter.
This reduces network traffic and application memory usage.
However, do not assume every OData expression works on every Graph property. Check the supported query capabilities for the specific endpoint.
Understand the Read-Only Boundary
This is the most important part of the implementation.
Suppose you have found a distribution group:
Display name: Support Team
Email: [email protected]
Type: Distribution group
It may be tempting to call:
PATCH /groups/{id}
and modify it.
That is where the architecture needs to stop and reconsider.
Microsoft's current Graph documentation identifies traditional distribution groups as read-only through the Graph groups API.
The normal Graph group update API supports properties for supported group types, but that does not mean every property or group type is writable.
Therefore, do not build a distribution-list management workflow around PATCH /groups simply because the endpoint exists.
What About Adding Members?
This is another common point of confusion.
For Microsoft 365 groups and supported security groups, Microsoft Graph provides membership APIs.
Developers sometimes assume that the same approach works for a traditional Exchange distribution group.
It does not follow automatically from the group being visible through Graph.
If the business requirement is:
Create distribution group
Add members
Remove members
Change delivery restrictions
Change ownership
Change moderation settings
you need to use the Exchange management surface that supports those operations.
Exchange provides distribution-group management through the Exchange admin center and Exchange Management Shell. Microsoft documents cmdlets such as New-DistributionGroup, Set-DistributionGroup, and Remove-DistributionGroup for Exchange distribution groups.
This distinction should be part of your application architecture.
Separate Graph From Exchange Management
A clean design might look like this:
.NET Application
|
+---------+---------+
| |
v v
Microsoft Graph Exchange management
| |
v v
Directory reporting Distribution changes
Graph can provide directory information and supported group operations.
Exchange management can handle distribution-group administration that Graph does not expose.
This separation prevents the application from pretending that one API supports everything.
Build a Service Layer
Do not put Microsoft Graph calls directly into button click handlers or controller actions.
Instead, create a service layer.
public interface IDistributionGroupService
{
Task<IReadOnlyList<DistributionGroupInfo>>
GetDistributionGroupsAsync(
CancellationToken cancellationToken);
}
Then implement it:
public sealed class DistributionGroupService
: IDistributionGroupService
{
private readonly GraphServiceClient graphClient;
public DistributionGroupService(
GraphServiceClient graphClient)
{
this.graphClient = graphClient;
}
public async Task<IReadOnlyList<DistributionGroupInfo>>
GetDistributionGroupsAsync(
CancellationToken cancellationToken)
{
var response =
await graphClient.Groups.GetAsync(
config =>
{
config.QueryParameters.Filter =
"NOT groupTypes/any(c:c eq 'Unified') and " +
"mailEnabled eq true and " +
"securityEnabled eq false";
config.QueryParameters.Select =
new[]
{
"id",
"displayName",
"mail",
"mailNickname"
};
},
cancellationToken);
return response?.Value?
.Select(group =>
new DistributionGroupInfo(
group.Id ?? string.Empty,
group.DisplayName ?? string.Empty,
group.Mail,
group.MailNickname))
.ToList()
?? new List<DistributionGroupInfo>();
}
}
This structure makes the application easier to test and change.
If Microsoft changes the API surface later, the Graph-specific changes remain mostly inside the service layer.
Handle Permissions Carefully
Directory APIs are security-sensitive.
A reporting application should not automatically receive write permissions.
For example, if the application only needs to display distribution groups, use read permissions appropriate to that requirement.
If the application also manages supported group types, request the corresponding write permissions.
Microsoft recommends least-privilege permissions and documents different permission requirements for delegated and application scenarios.
Also consider who is allowed to use the application.
A useful design is:
Normal user
|
+-- View allowed groups
Group administrator
|
+-- View groups
+-- Run supported management operations
Application administrator
|
+-- Configure tenant/application settings
Do not expose directory administration operations simply because the Graph token technically permits them.
Handle Graph Errors
Graph requests can fail for several reasons.
For example:
Your application should distinguish these cases.
A generic catch block is not enough:
try
{
await LoadGroupsAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Instead, classify errors at the service boundary and return meaningful application-level errors.
For example:
Authentication failure
Permission failure
Invalid request
Unsupported group operation
Throttled
Transient network failure
Unexpected Graph failure
This makes troubleshooting much easier.
Handle Throttling
Directory applications can run into throttling when they make too many requests.
Avoid designs that repeatedly query all groups.
For example, this pattern is inefficient:
Every page load
↓
Request every distribution group
↓
Request members for every group
↓
Request group properties again
A better design is:
Initial load
↓
Retrieve required data
↓
Cache appropriate read-only information
↓
Refresh when necessary
For administrative dashboards, short-lived caching can significantly reduce unnecessary Graph traffic.
Do not cache information longer than the application's business requirements allow, particularly when membership or ownership changes must be visible quickly.
Use the Correct Group API
Another important distinction is the newer distributionList resource.
Microsoft currently documents a beta endpoint that can list distribution lists associated with a user's mailbox:
GET /me/distributionLists
or:
GET /users/{id}/distributionLists
However, this API is currently under the Microsoft Graph beta surface, and Microsoft explicitly states that beta APIs are subject to change and are not supported for production applications.
Therefore, do not automatically replace the groups API with this endpoint.
First identify what your application actually needs.
If you need directory-wide inventory of traditional distribution groups, the groups API and its documented filtering capabilities may be relevant.
If you need mailbox-associated distributionList objects, the beta resource may be relevant for experimentation.
These are different use cases.
Common Mistakes
Treating every email-enabled group as a Microsoft 365 group
Email capability does not define the group type.
Check mailEnabled, securityEnabled, and groupTypes.
Using Graph PATCH for traditional distribution groups
Traditional distribution groups are read-only through the Graph groups API.
Do not assume the update endpoint supports them.
Using beta APIs in production without evaluating the risk
Beta Graph APIs can change.
Microsoft explicitly recommends against production use of beta APIs.
Requesting excessive permissions
Do not request directory-wide write permissions when the application only needs reporting.
Ignoring pagination
A tenant with hundreds or thousands of groups can expose incomplete results if the application reads only the first page.
Mixing Graph and Exchange responsibilities
If a requirement is not supported by Graph, do not force the architecture to use Graph.
Use the management surface that actually owns the operation.
Testing the Integration
Create test cases around both successful and failed scenarios.
At minimum, test:
1. Application authentication
2. Reading distribution groups
3. Filtering distribution groups
4. Empty result sets
5. Large group collections
6. Pagination
7. Missing permissions
8. Expired authentication
9. Throttling
10. Unsupported write operation
11. Network interruption
12. Tenant with mixed group types
Do not test only with a tenant containing one distribution list.
A directory application should be tested against realistic group diversity.
A Practical Architecture
For a production .NET administration tool, a reasonable structure is:
.NET UI / API
|
v
DistributionGroupService
|
+----------------------+
| |
v v
Microsoft Graph Exchange management
| |
v v
Read/reporting Supported distribution
group administration
Add supporting components around this:
Authentication
|
v
Authorization
|
v
Service layer
|
+-- Graph client
+-- Exchange management
|
v
Caching / logging / error handling
|
v
UI or API
This structure keeps identity, directory access, business logic, and presentation separate.
Advantages and Disadvantages
Advantages:
.NET integrates well with Microsoft Graph.
Strongly typed SDKs reduce manual HTTP handling.
Distribution groups can be discovered and reported through Graph.
Group filtering can reduce unnecessary data retrieval.
Authentication can be integrated with Microsoft Entra ID.
Graph provides a consistent API model for supported directory operations.
Graph and Exchange management can be combined when the business requirement needs both.
Disadvantages:
Traditional distribution groups have important Graph management limitations.
Group types are easy to confuse.
Permission configuration can become complex.
Pagination and throttling need to be handled correctly.
Some distribution-list operations require Exchange management capabilities.
Beta APIs introduce additional compatibility risk.
The biggest architectural lesson is that Microsoft Graph is not a universal replacement for every Microsoft 365 administration API.
Production Checklist
Before deploying a .NET application that works with distribution lists, verify:
The application identifies group types correctly.
Traditional distribution groups are treated as read-only through Graph.
The required Microsoft Graph permissions are documented.
Administrator consent is configured where required.
Delegated and application authentication are clearly separated.
Pagination is implemented.
Graph throttling is handled.
Transient failures use appropriate retry behavior.
Sensitive directory data is logged carefully.
Graph calls are isolated behind a service layer.
Exchange management is used for operations that Graph does not support.
Beta Graph APIs are not used for production-critical functionality without an explicit risk decision.
Test tenants contain realistic combinations of group types.
Unsupported operations return clear application errors.
Summary
Microsoft Graph is useful for .NET applications that need to discover, inspect, and report on Microsoft 365 groups, including traditional distribution groups.
The important detail is that visibility does not mean full management support.
Microsoft currently documents traditional distribution groups as read-only through the Microsoft Graph groups API. Mail-enabled security groups also have read-only limitations, while Microsoft 365 groups and security groups have broader Graph management capabilities.
For a .NET application, the safest approach is to identify the group type first, use Microsoft Graph for operations it actually supports, and use Exchange management capabilities for distribution-list administration that falls outside Graph's supported write surface.
A good implementation therefore looks like this:
Identify group type
↓
Authenticate with least privilege
↓
Use Microsoft Graph for supported operations
↓
Handle pagination and throttling
↓
Use Exchange management where required
↓
Keep API-specific code behind service boundaries
↓
Test against realistic tenant data
Once the group model and API boundaries are understood, building a reliable distribution-list administration tool becomes much simpler. The main challenge is not calling Microsoft Graph from C#. It is choosing the correct Microsoft management surface for the operation you actually need.
Join the conversation! Your thoughts help the community grow.