WizardM365API.Client
15.3.0
dotnet add package WizardM365API.Client --version 15.3.0
NuGet\Install-Package WizardM365API.Client -Version 15.3.0
<PackageReference Include="WizardM365API.Client" Version="15.3.0" />
<PackageVersion Include="WizardM365API.Client" Version="15.3.0" />
<PackageReference Include="WizardM365API.Client" />
paket add WizardM365API.Client --version 15.3.0
#r "nuget: WizardM365API.Client, 15.3.0"
#:package WizardM365API.Client@15.3.0
#addin nuget:?package=WizardM365API.Client&version=15.3.0
#tool nuget:?package=WizardM365API.Client&version=15.3.0
WizardM365API.Client
A comprehensive .NET client SDK for interacting with the Wizard M365 API. This package provides a type-safe, easy-to-use interface for all Microsoft 365 services, Azure DevOps, and security operations.
Table of Contents
- Features
- Installation
- Quick Start
- Configuration
- API Reference
- Persistence Feature
- Error Handling
- Best Practices
- Troubleshooting
- Support
Features
Microsoft 365 Services
- 🔷 Teams: Post, reply, edit, and retrieve Teams channel messages, get user activity, presence, and team memberships
- 📝 OneNote: Create and manage notebooks, sections, and pages with full CRUD operations
- 📁 SharePoint: Create folders, get site information, and manage SharePoint objects
- 💬 Chat: Harvest chat messages and retrieve chat information across conversations
- 📞 Communications: Access call records, online meetings, and meeting transcripts
- 👤 Users: Manage user events, calendar operations, directory audits, and activity logs
Security & Compliance
- 🛡️ Defender: Retrieve machines, alerts, vulnerabilities, software, recommendations, and security insights
- 📋 Audits: Start and manage audit jobs, retrieve audit logs and compliance reports
Development & Operations
- 🔧 Azure DevOps: Access pull requests, work items, commits, and comprehensive developer analytics
- 🔐 Applications: Create and manage Azure AD app registrations programmatically
Core Features
- ✅ Type-Safe API: Strongly-typed request and response models with IntelliSense support
- 🔄 Automatic Retries: Built-in retry logic for transient failures
- 🔒 OAuth2 Authentication: Automatic token management with client credentials flow
- 💾 Persistence Layer: Optional automatic database persistence for API operations
- 📊 Logging: Comprehensive structured logging for debugging and monitoring
- ⚡ Async/Await: Fully asynchronous API with cancellation token support
- 🏥 Health Checks: Monitor API health and status
Installation
dotnet add package WizardM365API.Client
Quick Start
1. Prerequisites
- .NET 6.0 or higher
- Azure AD tenant with appropriate permissions
- Wizard M365 API subscription key
2. Configuration
Add the M365 API settings to your appsettings.json:
{
"M365Api": {
"BaseUrl": "https://your-m365-api.com",
"SubscriptionKey": "your-subscription-key",
"SystemId": "your-system-id",
"DefaultUserEmail": "user@example.com",
"TimeoutSeconds": 30,
"RetryAttempts": 3,
"ThrowOnApiError": false,
"OAuth2": {
"TenantId": "your-azure-ad-tenant-id",
"ClientId": "your-azure-ad-application-client-id",
"ClientSecret": "your-azure-ad-application-client-secret",
"Scope": "https://graph.microsoft.com/.default",
"TokenCacheDurationMinutes": 55
},
"Persistence": {
"Enabled": false,
"FailSilently": true,
"PersistTeamsOperations": false,
"PersistOneNoteOperations": false,
"PersistSharePointOperations": false,
"PersistChatOperations": false,
"PersistCommunicationsOperations": false,
"TimeoutSeconds": 30
}
}
}
⚠️ Security Note: Never commit sensitive credentials to source control. Use environment variables, Azure Key Vault, or user secrets for production deployments.
3. Dependency Injection Setup
Register the M365 API client in your Program.cs or Startup.cs:
using WizardM365API.Client.Extensions;
// Basic setup - reads configuration from appsettings.json
builder.Services.AddM365ApiClient(builder.Configuration);
// OR configure manually with code
builder.Services.AddM365ApiClient(options =>
{
options.BaseUrl = "https://your-m365-api.com";
options.SubscriptionKey = "your-subscription-key";
options.SystemId = "your-system-id";
options.DefaultUserEmail = "user@example.com";
options.TimeoutSeconds = 60;
options.RetryAttempts = 3;
});
// For applications requiring persistence (see Persistence Feature section)
builder.Services.AddM365ApiClientWithPersistence(builder.Configuration);
4. Using the Client
using WizardM365API.Client;
using WizardM365API.Client.Models.Teams;
public class TeamsService
{
private readonly IM365ApiClient _m365Client;
public TeamsService(IM365ApiClient m365Client)
{
_m365Client = m365Client;
}
public async Task PostMessageAsync()
{
var request = new PostChannelMessageRequest
{
TeamId = "team-id",
ChannelId = "channel-id",
Subject = "Hello from Client SDK",
Message = "This message was sent using the M365 API Client SDK!",
Importance = "Important", // Supported: Standard, Important
UserEmail = "user@example.com"
};
var response = await _m365Client.Teams.PostChannelMessageAsync(request);
if (response.IsSuccess)
{
Console.WriteLine($"Message posted successfully: {response.Data?.Id}");
}
else
{
Console.WriteLine($"Error: {response.ErrorMessage}");
}
}
}
Configuration
Configuration Options
The M365 API Client supports comprehensive configuration through the M365ApiClientOptions class:
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
BaseUrl |
string | Yes | - | Base URL of the Wizard M365 API endpoint |
SubscriptionKey |
string | Yes | - | Your API subscription key for authentication |
SystemId |
string | Yes | - | Unique identifier for your system/application |
DefaultUserEmail |
string | No | null | Default user email for operations requiring user context |
TimeoutSeconds |
int | No | 30 | HTTP request timeout in seconds |
RetryAttempts |
int | No | 3 | Number of retry attempts for failed requests |
ThrowOnApiError |
bool | No | false | Whether to throw exceptions on API errors |
OAuth2 Configuration
Configure Azure AD authentication for Microsoft Graph API operations:
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
OAuth2.TenantId |
string | Yes | - | Your Azure AD tenant ID |
OAuth2.ClientId |
string | Yes | - | Azure AD application (client) ID |
OAuth2.ClientSecret |
string | Yes | - | Azure AD application client secret |
OAuth2.Scope |
string | No | "" | OAuth2 scope (e.g., "https://graph.microsoft.com/.default") |
OAuth2.TokenCacheDurationMinutes |
int | No | 55 | Duration to cache access tokens (in minutes) |
Persistence Configuration
Enable automatic database persistence for API operations (optional):
| Option | Type | Default | Description |
|---|---|---|---|
Persistence.Enabled |
bool | false | Master switch for persistence functionality |
Persistence.FailSilently |
bool | true | Continue operation if persistence fails |
Persistence.PersistTeamsOperations |
bool | false | Enable persistence for Teams operations |
Persistence.PersistOneNoteOperations |
bool | false | Enable persistence for OneNote operations |
Persistence.PersistSharePointOperations |
bool | false | Enable persistence for SharePoint operations |
Persistence.PersistChatOperations |
bool | false | Enable persistence for Chat operations |
Persistence.PersistCommunicationsOperations |
bool | false | Enable persistence for Communications operations |
Persistence.TimeoutSeconds |
int | 30 | Timeout for persistence operations |
Environment-Specific Configuration
For different environments (Development, Staging, Production), use environment-specific configuration files:
appsettings.Development.json
{
"M365Api": {
"BaseUrl": "https://dev-api.example.com",
"SubscriptionKey": "dev-subscription-key",
"SystemId": "dev-system",
"TimeoutSeconds": 60,
"ThrowOnApiError": true
}
}
appsettings.Production.json
{
"M365Api": {
"BaseUrl": "https://api.example.com",
"SubscriptionKey": "#{ProductionSubscriptionKey}#",
"SystemId": "production-system",
"TimeoutSeconds": 30,
"RetryAttempts": 5,
"ThrowOnApiError": false
}
}
Using Azure Key Vault
For secure credential storage in production:
// Program.cs
builder.Configuration.AddAzureKeyVault(
new Uri($"https://{keyVaultName}.vault.azure.net/"),
new DefaultAzureCredential());
builder.Services.AddM365ApiClient(options =>
{
options.BaseUrl = builder.Configuration["M365Api:BaseUrl"];
options.SubscriptionKey = builder.Configuration["M365Api:SubscriptionKey"];
options.OAuth2.ClientSecret = builder.Configuration["M365Api:OAuth2:ClientSecret"];
});
API Reference
Teams Client
// Importance values for Teams channel messages/replies/edits:
// "Standard" (default) or "Important"
// Post a message to a Teams channel
var postRequest = new PostChannelMessageRequest
{
TeamId = "team-id",
ChannelId = "channel-id",
Subject = "Message Subject",
Message = "Message content",
Importance = "Important",
EmailsToMention = new List<string> { "user@example.com" }
};
var response = await client.Teams.PostChannelMessageAsync(postRequest);
// Get a channel message using the generic M365 object endpoint
var getMessageRequest = new GetM365ObjectRequest
{
ObjectTypeId = 1, // Required: Object type for channel message
ObjectId = "1259337", // Required: Your object ID
UserEmail = "mcjoseph.agbanlog@wizard-ai.com" // Required: User email
};
var channelMessageResponse = await client.Teams.GetChannelMessageObjectAsync(getMessageRequest);
// Reply to a message
var replyRequest = new ReplyChannelMessageRequest
{
TeamId = "team-id",
ChannelId = "channel-id",
MessageId = "message-id",
Message = "Reply content",
Importance = "Important"
};
var replyResponse = await client.Teams.ReplyChannelMessageAsync(replyRequest);
// Edit a message
var editRequest = new EditChannelMessageRequest
{
TeamId = "team-id",
ChannelId = "channel-id",
MessageId = "message-id",
Message = "Updated content",
Importance = "Important"
};
var editResponse = await client.Teams.EditChannelMessageAsync(editRequest);
// Get messages
var getRequest = new GetChannelMessagesRequest
{
TeamId = "team-id",
ChannelId = "channel-id",
Top = 50
};
var messagesResponse = await client.Teams.GetChannelMessagesAsync(getRequest);
OneNote Client
// Create a notebook
var notebookRequest = new CreateNotebookRequest
{
DisplayName = "My Notebook",
SiteId = "optional-site-id"
};
var notebookResponse = await client.OneNote.CreateNotebookAsync(notebookRequest);
// Create a section
var sectionRequest = new CreateSectionRequest
{
DisplayName = "My Section",
NotebookId = "notebook-id"
};
var sectionResponse = await client.OneNote.CreateSectionAsync(sectionRequest);
// Create a page
var pageRequest = new CreatePageRequest
{
Title = "My Page",
Content = "<html><body><h1>Hello World</h1></body></html>",
SectionId = "section-id"
};
var pageResponse = await client.OneNote.CreatePageAsync(pageRequest);
SharePoint Client
// Get site ID
var siteIdRequest = new GetSiteIdRequest
{
HostName = "contoso.sharepoint.com",
SiteRelativePath = "sites/teamsite"
};
var siteIdResponse = await client.SharePoint.GetSiteIdAsync(siteIdRequest);
// Create a folder
var folderRequest = new CreateFolderRequest
{
SiteId = "site-id",
FolderName = "New Folder",
ParentPath = "/Shared Documents"
};
var folderResponse = await client.SharePoint.CreateFolderAsync(folderRequest);
Chat Client
Retrieve and harvest chat messages from Microsoft Teams chats.
// Harvest chat messages
var harvestRequest = new HarvestMessagesRequest
{
UserId = "user-id",
StartDateTime = DateTime.UtcNow.AddDays(-7),
EndDateTime = DateTime.UtcNow,
Top = 100,
UserEmail = "user@example.com"
};
var harvestResponse = await client.Chat.HarvestMessagesAsync(harvestRequest);
if (harvestResponse.IsSuccess)
{
Console.WriteLine($"Harvested {harvestResponse.Data?.Count} chat messages");
}
Communications Client
Access call records, online meetings, and meeting transcripts from Microsoft Teams.
// Get call records for a user
var callRecordsRequest = new GetCallRecordsRequest
{
UserId = "user-id-or-upn",
StartDateTime = DateTime.UtcNow.AddDays(-30),
EndDateTime = DateTime.UtcNow,
UserEmail = "user@example.com"
};
var callRecordsResponse = await client.Communications.GetCallRecordsAsync(callRecordsRequest);
if (callRecordsResponse.IsSuccess)
{
foreach (var callRecord in callRecordsResponse.Data)
{
Console.WriteLine($"Call: {callRecord.Id} - Duration: {callRecord.Duration}");
}
}
// Get a specific call record
var callRecordRequest = new GetCallRecordRequest
{
ObjectId = "call-record-id",
ObjectTypeId = 123, // Object type ID for call records
UserEmail = "user@example.com"
};
var callRecordResponse = await client.Communications.GetCallRecordAsync(callRecordRequest);
if (callRecordResponse.IsSuccess)
{
var callRecord = callRecordResponse.Data;
Console.WriteLine($"Call started: {callRecord.StartDateTime}");
Console.WriteLine($"Participants: {callRecord.Sessions?.Count}");
}
Available Operations:
GetCallRecordsAsync()- Retrieve call records for a user within a date rangeGetCallRecordAsync()- Get detailed information about a specific call record
Users Client
Manage user operations including calendar events, online meetings, directory audits, and activity logs.
// Create a calendar event for a user
var createEventRequest = new CreateUserEventRequest
{
UserIdOrUpn = "user@example.com",
Subject = "Team Meeting",
Start = new DateTimeTimeZone
{
DateTime = DateTime.UtcNow.AddDays(1).ToString("yyyy-MM-ddTHH:mm:ss"),
TimeZone = "UTC"
},
End = new DateTimeTimeZone
{
DateTime = DateTime.UtcNow.AddDays(1).AddHours(1).ToString("yyyy-MM-ddTHH:mm:ss"),
TimeZone = "UTC"
},
UserEmail = "user@example.com"
};
var eventResponse = await client.Users.CreateUserEventAsync(createEventRequest);
// Get user calendar events
var eventsRequest = new GetUserEventsRequest
{
UserIdOrUpn = "user@example.com",
StartDateTime = DateTime.UtcNow,
EndDateTime = DateTime.UtcNow.AddDays(7),
UserEmail = "user@example.com"
};
var eventsResponse = await client.Users.GetUserEventsAsync(eventsRequest);
// Get online meeting by join URL
var meetingRequest = new GetOnlineMeetingByJoinUrlRequest
{
JoinWebUrl = "https://teams.microsoft.com/l/meetup-join/...",
UserEmail = "user@example.com"
};
var meetingResponse = await client.Users.GetOnlineMeetingByJoinUrlAsync("user-id", meetingRequest);
// Get meeting transcripts
var transcriptsRequest = new GetMeetingTranscriptsRequest
{
UserEmail = "user@example.com"
};
var transcriptsResponse = await client.Users.GetMeetingTranscriptsAsync("user-id", "meeting-id", transcriptsRequest);
// Get transcript content
var transcriptContentRequest = new GetTranscriptContentRequest
{
UserEmail = "user@example.com"
};
var contentResponse = await client.Users.GetTranscriptContentAsync("user-id", "meeting-id", "transcript-id", transcriptContentRequest);
// Get user information
var userRequest = new GetUserRequest
{
UserEmail = "user@example.com"
};
var userResponse = await client.Users.GetUserAsync("user-id-or-upn", userRequest);
// Get directory audit logs
var auditRequest = new GetDirectoryAuditsRequest
{
StartDateTime = DateTime.UtcNow.AddDays(-30),
EndDateTime = DateTime.UtcNow,
UserEmail = "user@example.com"
};
var auditsResponse = await client.Users.GetDirectoryAuditsAsync("user-id", auditRequest);
// Get user activity logs
var activityRequest = new GetUserActivityLogsRequest
{
StartDateTime = DateTime.UtcNow.AddDays(-7),
EndDateTime = DateTime.UtcNow,
UserEmail = "user@example.com"
};
var activityResponse = await client.Users.GetUserActivityLogsAsync("user-id", activityRequest);
Available Operations:
CreateUserEventAsync()- Create a calendar event for a userGetUserEventsAsync()- Retrieve user calendar events within a date rangeGetOnlineMeetingByJoinUrlAsync()- Get online meeting details by join URLGetMeetingTranscriptsAsync()- Get transcripts for an online meetingGetTranscriptContentAsync()- Get the content of a specific transcriptGetUserAsync()- Get user profile informationGetDirectoryAuditsAsync()- Get directory audit logs for usersGetUserActivityLogsAsync()- Get user activity logs (document access, OneNote, Teams, etc.)
Applications Client
Create and manage Azure AD app registrations programmatically.
// Create an app registration
var createAppRequest = new CreateAppRegistrationRequest
{
TenantId = "tenant-id",
ClientId = "your-admin-client-id",
ClientSecret = "your-admin-client-secret",
DisplayName = "My Application",
SignInAudience = "AzureADMyOrg",
RedirectUris = new List<string> { "https://myapp.com/callback" },
RequiredResourceAccess = new List<ApiPermissionRequest>
{
new ApiPermissionRequest
{
ResourceAppId = "00000003-0000-0000-c000-000000000000", // Microsoft Graph
ResourceAccess = new List<ResourceAccessRequest>
{
new ResourceAccessRequest
{
Id = "e1fe6dd8-ba31-4d61-89e7-88639da4683d", // User.Read
Type = "Scope"
}
}
}
},
UserEmail = "admin@example.com"
};
var createResponse = await client.Applications.CreateAppRegistrationAsync(createAppRequest);
if (createResponse.IsSuccess)
{
Console.WriteLine($"App created: {createResponse.Data.DisplayName}");
Console.WriteLine($"App ID: {createResponse.Data.AppId}");
Console.WriteLine($"Client Secret: {createResponse.Data.ClientSecret}");
Console.WriteLine($"Secret expires: {createResponse.Data.ClientSecretExpirationDate}");
}
// Get app registrations
var getAppsRequest = new GetAppRegistrationsRequest
{
TenantId = "tenant-id",
ClientId = "your-admin-client-id",
ClientSecret = "your-admin-client-secret",
Top = 50,
Skip = 0,
UserEmail = "admin@example.com"
};
var appsResponse = await client.Applications.GetAppRegistrationsAsync(getAppsRequest);
if (appsResponse.IsSuccess)
{
Console.WriteLine($"Total apps: {appsResponse.Data.TotalCount}");
foreach (var app in appsResponse.Data.AppRegistrations)
{
Console.WriteLine($"App: {app.DisplayName} ({app.AppId})");
}
}
Available Operations:
CreateAppRegistrationAsync()- Create a new Azure AD app registrationGetAppRegistrationsAsync()- Retrieve app registrations from a tenant
Response Models:
CreateAppRegistrationResponse- Contains app ID, client secret, permissions, and configuration detailsGetAppRegistrationsResponse- Contains list of app registrations with pagination info
Azure DevOps Client
Access Azure DevOps data including pull requests, work items, commits, and developer analytics.
// Get pull requests
var prRequest = new GetPullRequestsRequest
{
Organization = "your-organization",
Project = "your-project",
Repository = "your-repository",
PersonalAccessToken = "your-pat",
StartDate = DateTime.UtcNow.AddDays(-30),
EndDate = DateTime.UtcNow,
Status = "completed",
UserEmail = "user@example.com"
};
var prResponse = await client.AzureDevOps.GetPullRequestsAsync(prRequest);
if (prResponse.IsSuccess)
{
Console.WriteLine($"Found {prResponse.Data.Count} pull requests");
foreach (var pr in prResponse.Data.PullRequests)
{
Console.WriteLine($"PR #{pr.PullRequestId}: {pr.Title} - {pr.Status}");
}
}
// Get work items
var workItemsRequest = new GetWorkItemsRequest
{
Organization = "your-organization",
Project = "your-project",
PersonalAccessToken = "your-pat",
WorkItemIds = new List<int> { 123, 456, 789 },
UserEmail = "user@example.com"
};
var workItemsResponse = await client.AzureDevOps.GetWorkItemsAsync(workItemsRequest);
// Get commits
var commitsRequest = new GetCommitsRequest
{
Organization = "your-organization",
Project = "your-project",
Repository = "your-repository",
PersonalAccessToken = "your-pat",
FromDate = DateTime.UtcNow.AddMonths(-1),
ToDate = DateTime.UtcNow,
UserEmail = "user@example.com"
};
var commitsResponse = await client.AzureDevOps.GetCommitsAsync(commitsRequest);
// Get comprehensive developer analytics
var analyticsRequest = new DeveloperAnalyticsRequest
{
Organization = "your-organization",
Project = "your-project",
Repository = "your-repository",
PersonalAccessToken = "your-pat",
SearchCriteria = new DeveloperSearchCriteria
{
DeveloperEmail = "developer@example.com",
StartDate = DateTime.UtcNow.AddMonths(-3),
EndDate = DateTime.UtcNow
},
UserEmail = "admin@example.com"
};
var analyticsResponse = await client.AzureDevOps.GetDeveloperAnalyticsAsync(analyticsRequest);
if (analyticsResponse.IsSuccess)
{
var analytics = analyticsResponse.Data;
Console.WriteLine($"Developer: {analytics.Developer}");
Console.WriteLine($"Total PRs: {analytics.TotalPullRequests}");
Console.WriteLine($"Total Commits: {analytics.TotalCommits}");
Console.WriteLine($"Total Work Items: {analytics.TotalWorkItems}");
Console.WriteLine($"Lines Added: {analytics.TotalLinesAdded}");
Console.WriteLine($"Lines Deleted: {analytics.TotalLinesDeleted}");
}
Available Operations:
GetPullRequestsAsync()- Get pull requests with developer analyticsGetWorkItemsAsync()- Get work items for a projectGetCommitsAsync()- Get commits for a repositoryGetDeveloperAnalyticsAsync()- Get comprehensive developer analytics (PRs, commits, work items)
Response Models:
GetPullRequestsResponse- Pull requests with metrics (lines changed, review comments, etc.)GetWorkItemsResponse- Work items with full details and historyGetCommitsResponse- Commits with file changes and statisticsDeveloperAnalyticsResponse- Aggregated developer productivity metrics
Defender Client
// Get machines
var machinesRequest = new GetMachinesRequest
{
TenantId = "your-tenant-id",
ClientId = "your-client-id",
ClientSecret = "your-client-secret",
UserEmail = "user@example.com",
Top = 50,
HealthStatus = "Active",
OsPlatform = "Windows10"
};
var machinesResponse = await client.Defender.GetMachinesAsync(machinesRequest);
if (machinesResponse.IsSuccess)
{
// machinesResponse.Data is List<DefenderMachine>
foreach (var machine in machinesResponse.Data)
{
Console.WriteLine($"Machine: {machine.ComputerDnsName} - Status: {machine.HealthStatus}");
}
}
// Get machine alerts
var alertsRequest = new GetMachineAlertsRequest
{
TenantId = "your-tenant-id",
ClientId = "your-client-id",
ClientSecret = "your-client-secret",
MachineId = "machine-id",
UserEmail = "user@example.com"
};
var alertsResponse = await client.Defender.GetMachineAlertsAsync(alertsRequest);
if (alertsResponse.IsSuccess)
{
// alertsResponse.Data is List<DefenderMachineAlert>
foreach (var alert in alertsResponse.Data)
{
Console.WriteLine($"Alert: {alert.Title} - Severity: {alert.Severity}");
}
}
// Get machine vulnerabilities
var vulnerabilitiesRequest = new GetMachineVulnerabilitiesRequest
{
TenantId = "your-tenant-id",
ClientId = "your-client-id",
ClientSecret = "your-client-secret",
MachineId = "machine-id",
UserEmail = "user@example.com"
};
var vulnerabilitiesResponse = await client.Defender.GetMachineVulnerabilitiesAsync(vulnerabilitiesRequest);
// Get machine software
var softwareRequest = new GetMachineSoftwareRequest
{
TenantId = "your-tenant-id",
ClientId = "your-client-id",
ClientSecret = "your-client-secret",
MachineId = "machine-id",
UserEmail = "user@example.com"
};
var softwareResponse = await client.Defender.GetMachineSoftwareAsync(softwareRequest);
// Get machine logon users
var logsRequest = new GetMachineLogsRequest
{
TenantId = "your-tenant-id",
ClientId = "your-client-id",
ClientSecret = "your-client-secret",
MachineId = "machine-id",
UserEmail = "user@example.com"
};
var logsResponse = await client.Defender.GetMachineLogsAsync(logsRequest);
// Get security recommendations
var recommendationsRequest = new GetRecommendationsRequest
{
TenantId = "your-tenant-id",
ClientId = "your-client-id",
ClientSecret = "your-client-secret",
UserEmail = "user@example.com",
Top = 100,
RecommendationCategory = "Security"
};
var recommendationsResponse = await client.Defender.GetRecommendationsAsync(recommendationsRequest);
if (recommendationsResponse.IsSuccess)
{
// recommendationsResponse.Data is List<DefenderRecommendation>
foreach (var recommendation in recommendationsResponse.Data)
{
Console.WriteLine($"Recommendation: {recommendation.RecommendationName} - Severity: {recommendation.SeverityScore}");
}
}
Available Operations:
GetMachinesAsync()- Get machines with filtering options (health status, OS platform, etc.)GetMachineAlertsAsync()- Get alerts for a specific machineGetMachineLogsAsync()- Get logon users for a machineGetMachineVulnerabilitiesAsync()- Get vulnerabilities for a machineGetMachineSoftwareAsync()- Get software installed on a machineGetRecommendationsAsync()- Get security recommendations
Response Models:
List<DefenderMachine>- Machine information with health status, OS details, risk scoreList<DefenderMachineAlert>- Security alerts with severity, status, and remediation infoList<DefenderMachineLog>- Logon user informationList<DefenderMachineVulnerability>- CVE information and severity scoresList<DefenderMachineSoftware>- Installed software inventoryList<DefenderRecommendation>- Security recommendations with exposure scores
🔒 Security Note: The Defender API endpoints use POST requests (not GET) to prevent credential exposure in URLs and server logs. Client secrets are transmitted securely in the request body over HTTPS. See CHANGELOG-DEFENDER-SECURITY.md for more details.
Audits Client
Start and manage audit jobs for Office 365 compliance and security auditing.
// Start an audit job
var startJobRequest = new StartAuditJobRequest
{
StartDateTime = DateTime.UtcNow.AddDays(-30),
EndDateTime = DateTime.UtcNow,
RecordType = "AzureActiveDirectory", // Optional: filter by record type
Operations = new List<string> { "UserLoggedIn", "FileAccessed" }, // Optional: filter by operations
UserEmail = "admin@example.com"
};
var jobResponse = await client.Audits.StartAuditJobAsync(startJobRequest);
if (jobResponse.IsSuccess)
{
string queryId = jobResponse.Data.QueryId;
Console.WriteLine($"Audit job started: {queryId}");
// Poll for job status
bool isComplete = false;
while (!isComplete)
{
await Task.Delay(5000); // Wait 5 seconds between polls
var statusRequest = new GetAuditJobStatusRequest
{
QueryId = queryId,
UserEmail = "admin@example.com"
};
var statusResponse = await client.Audits.GetAuditJobStatusAsync(statusRequest);
if (statusResponse.IsSuccess)
{
Console.WriteLine($"Job status: {statusResponse.Data.Status}");
isComplete = statusResponse.Data.Status == "Succeeded";
if (isComplete)
{
// Get audit results
var resultsRequest = new GetAuditJobResultsRequest
{
QueryId = queryId,
PageSize = 1000,
PageToken = null, // For pagination
UserEmail = "admin@example.com"
};
var resultsResponse = await client.Audits.GetAuditJobResultsAsync(resultsRequest);
if (resultsResponse.IsSuccess)
{
Console.WriteLine($"Retrieved {resultsResponse.Data.Records.Count} audit records");
foreach (var record in resultsResponse.Data.Records)
{
Console.WriteLine($"Operation: {record.Operation}");
Console.WriteLine($"User: {record.UserId}");
Console.WriteLine($"Time: {record.CreationTime}");
Console.WriteLine($"Workload: {record.Workload}");
Console.WriteLine("---");
}
// Handle pagination
if (!string.IsNullOrEmpty(resultsResponse.Data.NextPageToken))
{
resultsRequest.PageToken = resultsResponse.Data.NextPageToken;
// Fetch next page...
}
}
}
}
}
}
Available Operations:
StartAuditJobAsync()- Start an audit job for a specified date rangeGetAuditJobStatusAsync()- Check the status of an audit jobGetAuditJobResultsAsync()- Retrieve audit records from a completed job
Response Models:
StartAuditJobResponse- Contains query ID for tracking the audit jobGetAuditJobStatusResponse- Job status (Pending, Processing, Succeeded, Failed)GetAuditJobResultsResponse- Audit records with pagination support
Audit Record Types:
- Exchange (email operations)
- SharePoint (document operations)
- OneDrive (file operations)
- AzureActiveDirectory (sign-ins, user management)
- Teams (meetings, messages, calls)
- and more...
Health Client
Monitor the health and status of the Wizard M365 API.
// Basic health check
var healthResponse = await client.Health.GetHealthAsync();
if (healthResponse.IsSuccess)
{
Console.WriteLine($"API Status: {healthResponse.Data.Status}");
Console.WriteLine($"Version: {healthResponse.Data.Version}");
}
// Detailed health check (includes component status)
var detailedHealthResponse = await client.Health.GetDetailedHealthAsync();
if (detailedHealthResponse.IsSuccess)
{
Console.WriteLine($"Overall Status: {detailedHealthResponse.Data.Status}");
foreach (var component in detailedHealthResponse.Data.Components)
{
Console.WriteLine($" {component.Name}: {component.Status}");
}
}
Available Operations:
GetHealthAsync()- Get basic API health statusGetDetailedHealthAsync()- Get detailed health status including component health
Error Handling
Response Structure
All API operations return an ApiResponse<T> object with the following structure:
public class ApiResponse<T>
{
public bool IsSuccess { get; set; } // True if operation succeeded
public T? Data { get; set; } // Response data (if successful)
public string? ErrorType { get; set; } // Error category
public int? ErrorCode { get; set; } // Error code
public string? ErrorMessage { get; set; } // Human-readable error message
public int StatusCode { get; set; } // HTTP status code
}
Basic Error Handling
Always check IsSuccess before accessing Data:
var response = await client.Teams.PostChannelMessageAsync(request);
if (response.IsSuccess)
{
// Success - safely access response.Data
var message = response.Data;
Console.WriteLine($"Message posted: {message.Id}");
}
else
{
// Error - handle appropriately
Console.WriteLine($"Error {response.StatusCode}: {response.ErrorMessage}");
Console.WriteLine($"Error Type: {response.ErrorType}");
}
Advanced Error Handling
Handle different error types with pattern matching:
var response = await client.Defender.GetMachinesAsync(request);
if (!response.IsSuccess)
{
switch (response.ErrorType)
{
case "ValidationError":
// Invalid request parameters
Console.WriteLine($"Validation failed: {response.ErrorMessage}");
break;
case "AuthenticationError":
// Authentication/authorization failed
Console.WriteLine($"Auth failed: {response.ErrorMessage}");
// Perhaps refresh tokens or re-authenticate
break;
case "RateLimitError":
// Too many requests
Console.WriteLine($"Rate limited. Retry after: {response.ErrorMessage}");
// Implement exponential backoff
break;
case "NotFoundError":
// Resource not found
Console.WriteLine($"Resource not found: {response.ErrorMessage}");
break;
case "ServerError":
// API server error
Console.WriteLine($"Server error: {response.ErrorMessage}");
// Log and alert for investigation
break;
default:
Console.WriteLine($"Unexpected error: {response.ErrorMessage}");
break;
}
}
Exception Handling
By default, the client does not throw exceptions for API errors. Enable exceptions if preferred:
builder.Services.AddM365ApiClient(options =>
{
options.ThrowOnApiError = true; // Throw exceptions on API errors
});
// Now you can use try-catch
try
{
var response = await client.Teams.PostChannelMessageAsync(request);
var message = response.Data;
}
catch (M365ApiException ex)
{
Console.WriteLine($"API Error: {ex.Message}");
Console.WriteLine($"Status Code: {ex.StatusCode}");
}
Retry Logic
The client includes built-in retry logic for transient failures:
builder.Services.AddM365ApiClient(options =>
{
options.RetryAttempts = 5; // Retry up to 5 times
options.TimeoutSeconds = 60; // 60 second timeout per request
});
Retry logic applies to:
- Network timeouts
- HTTP 429 (Too Many Requests)
- HTTP 500-599 (Server errors)
Logging
The client uses structured logging for debugging and monitoring:
// Configure logging in Program.cs
builder.Logging.AddConsole();
builder.Logging.SetMinimumLevel(LogLevel.Information);
// The client will log:
// - Request attempts
// - Retry attempts
// - Errors and exceptions
// - Performance metrics
Example log output:
[Information] Attempting to post message to Teams channel. TeamId: abc123, ChannelId: xyz789
[Information] Successfully posted message to Teams channel. MessageId: msg456
[Error] Failed to post message. Error: ValidationError - TeamId is required
Persistence Feature
The M365 API Client now supports automatic persistence of API operation results to your database. This feature allows you to automatically store information about Teams messages, OneNote pages, SharePoint folders, and other M365 objects in your database entities after successful API operations.
Enabling Persistence
1. Basic Setup with Configuration
{
"M365Api": {
"BaseUrl": "https://your-m365-api.com",
"ApiKey": "your-api-key",
"SystemId": "your-system-id",
"Persistence": {
"Enabled": true,
"FailSilently": true,
"PersistTeamsOperations": false,
"PersistOneNoteOperations": false,
"PersistSharePointOperations": false,
"PersistChatOperations": false,
"TimeoutSeconds": 10,
"SetAuditFields": true
}
}
}
// Enable persistence using configuration
builder.Services.AddM365ApiClientWithPersistence(builder.Configuration);
// Or configure persistence manually
builder.Services.AddM365ApiClientWithPersistence(options =>
{
options.BaseUrl = "https://your-m365-api.com";
options.ApiKey = "your-api-key";
options.SystemId = "your-system-id";
options.Persistence.Enabled = true;
options.Persistence.PersistTeamsOperations = true; // Enable Teams persistence if needed
});
2. Entity Setup
Your entities must implement the required interfaces:
using WizardM365API.Client.Persistence.Entities;
public class MsTeamsTeam : IMsTeamsTeam
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public string? Link { get; set; }
public string ExternalId { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public string? UpdatedBy { get; set; }
// Your additional properties
public bool IsActive { get; set; } = true;
public virtual ICollection<MsTeamsChannel> Channels { get; set; } = new List<MsTeamsChannel>();
}
public class MsTeamsChannel : IMsTeamsChannel
{
public Guid Id { get; set; }
public Guid TeamId { get; set; }
public string Name { get; set; } = string.Empty;
public string? Type { get; set; }
public string? Link { get; set; }
public string ExternalId { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public string? UpdatedBy { get; set; }
// Your additional properties and navigation properties
public virtual MsTeamsTeam Team { get; set; } = null!;
public virtual ICollection<MsTeamsConversation> Conversations { get; set; } = new List<MsTeamsConversation>();
}
public class MsTeamsConversation : IMsTeamsConversation
{
public Guid Id { get; set; }
public Guid ChannelId { get; set; }
public string? Link { get; set; }
public string ExternalId { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public string? CreatedBy { get; set; }
public DateTime? UpdatedAt { get; set; }
public string? UpdatedBy { get; set; }
public bool IsQueued { get; set; }
public bool Processed { get; set; }
public int? ObjectId { get; set; }
public int? ObjectTypeId { get; set; }
// Your additional properties
public virtual MsTeamsChannel Channel { get; set; } = null!;
}
3. DbContext Setup
Ensure your DbContext includes DbSets for entities that implement the required interfaces:
public class MyDbContext : DbContext
{
public DbSet<MsTeamsTeam> MsTeamsTeams { get; set; }
public DbSet<MsTeamsChannel> MsTeamsChannels { get; set; }
public DbSet<MsTeamsConversation> MsTeamsConversations { get; set; }
public DbSet<MsOneNoteNotebook> MsOneNoteNotebooks { get; set; }
public DbSet<MsOneNoteSection> MsOneNoteSections { get; set; }
public DbSet<MsOneNotePage> MsOneNotePages { get; set; }
// ... other DbSets
}
Using Persistence-Enabled Methods
Use the new overloads that accept a DbContext parameter:
public class TeamsService
{
private readonly IM365ApiClient _m365Client;
private readonly MyDbContext _dbContext;
public TeamsService(IM365ApiClient m365Client, MyDbContext dbContext)
{
_m365Client = m365Client;
_dbContext = dbContext;
}
public async Task PostMessageWithPersistenceAsync()
{
var request = new PostChannelMessageRequest
{
TeamId = "team-id",
ChannelId = "channel-id",
Subject = "Hello from Client SDK",
Message = "This message will be automatically saved to database!",
Importance = "Important",
UserEmail = "user@example.com"
};
// This will post the message AND automatically save it to your database
var response = await _m365Client.Teams.PostChannelMessageAsync(request, _dbContext);
if (response.IsSuccess)
{
// Message posted and persisted successfully
Console.WriteLine($"Message posted and saved: {response.Data?.Id}");
}
}
public async Task GetChannelWithPersistenceAsync()
{
var request = new GetM365ObjectRequest
{
ObjectId = "channel-id",
UserEmail = "user@example.com"
};
// This will get the channel AND automatically save it to your database
var response = await _m365Client.Teams.GetChannelAsync(request, _dbContext);
if (response.IsSuccess)
{
// Channel retrieved and persisted successfully
Console.WriteLine($"Channel retrieved and saved: {response.Data?.DisplayName}");
}
}
}
Available Persistence Interfaces
- Teams:
IMsTeamsTeam,IMsTeamsChannel,IMsTeamsConversation - OneNote:
IMsOneNoteNotebook,IMsOneNoteSection,IMsOneNotePage - SharePoint:
IMsSharePointSite,IMsSharePointList,IMsSharePointFolder,IMsSharePointFile - Chat:
IMsChatConversation,IMsChatMessage
Configuration Options
| Option | Description | Default |
|---|---|---|
Enabled |
Whether persistence is enabled | false |
FailSilently |
Whether to fail silently if persistence fails | true |
PersistTeamsOperations |
Whether to persist Teams operations | false |
PersistOneNoteOperations |
Whether to persist OneNote operations | false |
PersistSharePointOperations |
Whether to persist SharePoint operations | false |
PersistChatOperations |
Whether to persist Chat operations | false |
TimeoutSeconds |
Timeout for persistence operations | 10 |
SetAuditFields |
Whether to automatically set audit fields | true |
How It Works
- Automatic Detection: The persistence service uses reflection to find entity types in your DbContext that implement the required interfaces
- Mapping: API responses are automatically mapped to your entity properties
- Upsert Logic: Existing entities are updated, new ones are created
- Error Handling: Persistence failures don't break API operations (configurable)
- Audit Fields: Automatically sets CreatedAt, UpdatedAt, UpdatedBy fields
- Object IDs: Uses ObjectId and ObjectTypeId from the request DTOs
Benefits
- Zero Code Changes: Existing API calls work unchanged
- Opt-in Persistence: Use persistence-enabled methods only when needed
- Flexible Entity Design: Your entities can have additional properties beyond the required interface
- Robust Error Handling: Persistence failures don't break your application flow
- Automatic Relationships: Handles parent-child relationships (Team → Channel → Conversation)
Best Practices
1. Security
Never Hardcode Credentials
// ❌ BAD - Credentials in code
options.SubscriptionKey = "abc123-secret-key";
options.OAuth2.ClientSecret = "my-client-secret";
// ✅ GOOD - Use configuration
options.SubscriptionKey = configuration["M365Api:SubscriptionKey"];
options.OAuth2.ClientSecret = configuration["M365Api:OAuth2:ClientSecret"];
Use Azure Key Vault for Production
builder.Configuration.AddAzureKeyVault(
new Uri($"https://{keyVaultName}.vault.azure.net/"),
new DefaultAzureCredential());
Secure Credential Storage
- Use User Secrets for local development
- Use Azure Key Vault for production
- Use Environment Variables for CI/CD pipelines
- Never commit
.configorappsettings.jsonwith secrets to source control
2. Performance
Use Cancellation Tokens
// Allow operations to be cancelled
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var response = await client.Teams.GetChannelMessagesAsync(request, cts.Token);
Implement Pagination
// For large datasets, use pagination
var request = new GetChannelMessagesRequest
{
TeamId = "team-id",
ChannelId = "channel-id",
Top = 50 // Fetch in batches
};
Configure Appropriate Timeouts
builder.Services.AddM365ApiClient(options =>
{
options.TimeoutSeconds = 60; // Increase for operations that may take longer
options.RetryAttempts = 3; // Balance between resilience and latency
});
3. Error Handling
Always Check IsSuccess
// ❌ BAD - Accessing Data without checking
var message = response.Data.Id; // NullReferenceException if failed!
// ✅ GOOD - Check first
if (response.IsSuccess && response.Data != null)
{
var message = response.Data.Id;
}
Log Errors for Debugging
if (!response.IsSuccess)
{
_logger.LogError(
"Failed to post Teams message. Error: {ErrorType} - {ErrorMessage}. RequestId: {RequestId}",
response.ErrorType, response.ErrorMessage, request.RequestId);
}
Implement Retry with Exponential Backoff for Rate Limits
async Task<ApiResponse<T>> RetryWithBackoff<T>(Func<Task<ApiResponse<T>>> operation, int maxAttempts = 3)
{
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
var response = await operation();
if (response.IsSuccess || response.ErrorType != "RateLimitError")
return response;
if (attempt < maxAttempts)
{
var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt)); // Exponential backoff
await Task.Delay(delay);
}
}
return await operation(); // Final attempt
}
4. Resource Management
Use Dependency Injection
// ✅ GOOD - Let DI manage lifecycle
public class MyService
{
private readonly IM365ApiClient _client;
public MyService(IM365ApiClient client)
{
_client = client;
}
}
Avoid Creating Multiple Client Instances
// ❌ BAD - Creating clients manually
var client1 = new M365ApiClient(...);
var client2 = new M365ApiClient(...);
// ✅ GOOD - Use singleton from DI
builder.Services.AddM365ApiClient(builder.Configuration);
5. Testing
Use Interfaces for Testability
// Service depends on interface, making it easy to mock
public class TeamsService
{
private readonly IM365ApiClient _client;
public TeamsService(IM365ApiClient client)
{
_client = client;
}
}
// In tests, mock the interface
var mockClient = new Mock<IM365ApiClient>();
mockClient.Setup(x => x.Teams.PostChannelMessageAsync(It.IsAny<PostChannelMessageRequest>(), default))
.ReturnsAsync(ApiResponse<ChatMessage>.Success(new ChatMessage()));
Integration Testing
// Use a test configuration with a sandbox environment
public class IntegrationTestsFixture : IClassFixture<WebApplicationFactory<Program>>
{
private readonly IM365ApiClient _client;
public IntegrationTestsFixture(WebApplicationFactory<Program> factory)
{
var services = factory.Services;
_client = services.GetRequiredService<IM365ApiClient>();
}
}
6. Monitoring
Health Check Integration
// Add health checks for M365 API
builder.Services.AddHealthChecks()
.AddCheck("m365-api", async () =>
{
var client = serviceProvider.GetRequiredService<IM365ApiClient>();
var response = await client.Health.GetHealthAsync();
return response.IsSuccess
? HealthCheckResult.Healthy("M365 API is healthy")
: HealthCheckResult.Unhealthy($"M365 API is down: {response.ErrorMessage}");
});
Application Insights Integration
builder.Services.AddApplicationInsightsTelemetry();
// The client's built-in logging will automatically flow to App Insights
Troubleshooting
Common Issues
1. Authentication Errors
Problem: AuthenticationError: Unauthorized (401)
Solutions:
- Verify your
SubscriptionKeyis correct and active - Check OAuth2 credentials (
TenantId,ClientId,ClientSecret) - Ensure the Azure AD app has the required permissions
- Verify tokens haven't expired
- Check if Azure AD app permissions have been admin-consented
// Test authentication
var response = await client.Health.GetHealthAsync();
if (!response.IsSuccess && response.ErrorType == "AuthenticationError")
{
// Check your credentials
}
2. Rate Limiting
Problem: RateLimitError: Too Many Requests (429)
Solutions:
- Implement exponential backoff retry logic
- Reduce request frequency
- Use pagination to fetch data in smaller batches
- Cache responses when possible
- Consider upgrading your API subscription tier
// Check rate limit headers in responses
if (response.ErrorType == "RateLimitError")
{
// Wait and retry
await Task.Delay(TimeSpan.FromSeconds(60));
}
3. Timeout Errors
Problem: TimeoutException: Request timed out
Solutions:
- Increase timeout configuration
- Use pagination for large datasets
- Check network connectivity
- Verify API endpoint health
builder.Services.AddM365ApiClient(options =>
{
options.TimeoutSeconds = 120; // Increase timeout
});
4. Validation Errors
Problem: ValidationError: Required field missing
Solutions:
- Check all required fields are populated
- Verify data types and formats
- Review API documentation for parameter requirements
- Enable detailed logging to see request details
// Ensure all required fields are set
var request = new PostChannelMessageRequest
{
TeamId = "...", // Required
ChannelId = "...", // Required
Message = "...", // Required
UserEmail = "..." // Required
};
5. Persistence Errors
Problem: Persistence fails but API operation succeeds
Solutions:
- Check
FailSilentlysetting in configuration - Verify DbContext is registered correctly
- Ensure entity interfaces are implemented
- Check database connection string
- Review persistence timeout settings
// Enable persistence error logging
builder.Services.AddM365ApiClientWithPersistence(options =>
{
options.Persistence.Enabled = true;
options.Persistence.FailSilently = false; // Throw exceptions for debugging
});
6. SSL/TLS Errors
Problem: SSL connection could not be established
Solutions:
- Ensure your application can access HTTPS endpoints
- Check firewall and proxy settings
- Verify SSL certificates are trusted
- Update root certificates on the server
Debug Mode
Enable detailed logging for troubleshooting:
builder.Logging.AddConsole();
builder.Logging.AddDebug();
builder.Logging.SetMinimumLevel(LogLevel.Debug);
// Or for even more detail
builder.Logging.SetMinimumLevel(LogLevel.Trace);
Getting Help
If you encounter issues not covered here:
- Check the logs - Enable detailed logging to see what's happening
- Review API documentation - Ensure you're using the API correctly
- Check API health - Use the Health Client to verify API status
- Contact support - Reach out with:
- Error messages and stack traces
- Request/response details (sanitized)
- Configuration (without credentials)
- Steps to reproduce
License
MIT License
Support
For issues and questions:
- Documentation: Refer to the main Wizard M365 API documentation
- Issues: Create an issue in the repository
- Security: Report security issues privately to the security team
Made with ❤️ by the Wizard AI Team
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Microsoft.EntityFrameworkCore (>= 8.0.8)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Http (>= 8.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Options (>= 8.0.0)
- Microsoft.Graph (>= 5.77.0)
- System.Text.Json (>= 9.0.5)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 15.3.0 | 97 | 3/3/2026 |
| 15.2.3 | 580 | 10/21/2025 |
| 15.2.2 | 199 | 10/21/2025 |
| 15.2.1 | 203 | 10/21/2025 |
| 15.2.0 | 202 | 10/21/2025 |
| 15.1.1 | 208 | 10/16/2025 |
| 15.1.0 | 274 | 10/9/2025 |
| 15.0.0 | 214 | 10/8/2025 |
| 14.1.0 | 197 | 10/7/2025 |
| 14.0.0 | 206 | 10/7/2025 |
| 13.3.0 | 247 | 9/24/2025 |
| 13.2.0 | 199 | 9/24/2025 |
| 13.1.0 | 197 | 9/24/2025 |
| 13.0.0 | 209 | 9/24/2025 |
| 12.3.0 | 334 | 9/18/2025 |
| 12.2.0 | 348 | 9/17/2025 |
| 12.1.0 | 348 | 9/17/2025 |
| 12.0.0 | 330 | 9/17/2025 |
| 11.1.0 | 343 | 9/16/2025 |
| 11.0.0 | 326 | 9/16/2025 |