Webority.Graph
0.30.0
See the version list below for details.
dotnet add package Webority.Graph --version 0.30.0
NuGet\Install-Package Webority.Graph -Version 0.30.0
<PackageReference Include="Webority.Graph" Version="0.30.0" />
<PackageVersion Include="Webority.Graph" Version="0.30.0" />
<PackageReference Include="Webority.Graph" />
paket add Webority.Graph --version 0.30.0
#r "nuget: Webority.Graph, 0.30.0"
#:package Webority.Graph@0.30.0
#addin nuget:?package=Webority.Graph&version=0.30.0
#tool nuget:?package=Webority.Graph&version=0.30.0
webority-graph
Microsoft Graph for Webority products, published to nuget.org as Webority.Graph. One app-only
Graph client (client credentials) and a typed service per Microsoft 365 area.
Setup
"MicrosoftGraph": {
"Shared": {
"ClientId": "<app registration id>",
"CredentialKind": "ClientSecret",
"ClientSecret": "<secret>",
"Authority": "https://login.microsoftonline.com/<tenant id>"
}
}
Signing in without a secret
"MicrosoftGraph": {
"Shared": {
"ClientId": "<app registration id>",
"CredentialKind": "ManagedIdentityFederation",
"ManagedIdentityClientId": "<identity client id>",
"Authority": "https://login.microsoftonline.com/<tenant id>"
}
}
With ManagedIdentityFederation the app registration gets a federated credential that trusts the
user-assigned managed identity: issuer https://login.microsoftonline.com/<tenant-id>/v2.0, subject
set to the identity's principal (object) id, audience api://AzureADTokenExchange. The identity must
also be assigned to the host app. The same settings apply to MicrosoftGraph:MailSend.
Call the registration for each area you use. Each one also registers the shared client and validates its settings at startup, so a missing value stops the app from booting.
| Area | Registration | Service | Own settings |
|---|---|---|---|
| Bookings | AddMicrosoftGraphBookings() |
IMicrosoftGraphBookingsService |
MicrosoftGraph:Bookings:BookingBusinessId, MicrosoftGraph:Bookings:BusinessTimeZone |
| Calendar | AddMicrosoftGraphCalendar() |
IMicrosoftGraphCalendarService |
|
| Calendar subscriptions | AddMicrosoftGraphCalendarSubscriptions() |
IMicrosoftGraphCalendarSubscriptionService |
MicrosoftGraph:CalendarSubscriptions |
AddMicrosoftGraphMail() |
IGraphMailService |
||
| Mailbox settings | AddMicrosoftGraphMailbox() |
IMicrosoftGraphMailboxService |
|
| Teams | AddMicrosoftGraphTeams() |
IMicrosoftGraphTeamsService |
MicrosoftGraph:Teams |
| Bot installation | AddMicrosoftGraphBotInstaller() |
IMicrosoftGraphBotInstaller |
|
| Entra users and licences | AddMicrosoftGraphEntra() |
IMicrosoftGraphEntraService |
|
| Mailbox addresses (Exchange Online) | AddMicrosoftGraphExchange() |
IExchangeMailboxAddressService |
|
| App credentials | AddEntraAppCredentials() |
IEntraAppCredentialService |
|
| Directory principals and consent policies | AddMicrosoftGraphDirectory() |
IDirectoryPrincipalReader |
|
| People, presence, org chart | AddPeopleAndPresenceGraph() |
IPeopleDirectoryService, IPresenceService, IOrgChartService |
|
| Rooms | AddMicrosoftGraphRooms() |
IMicrosoftGraphRoomService |
|
| To Do | AddMicrosoftGraphTodo() |
IMicrosoftGraphTodoService |
|
| Search connector | AddGraphSearchConnector() |
IGraphSearchConnectionManager, IGraphSearchItemService |
MicrosoftGraph:SearchConnector |
IGraphLivenessProbe (GET /organization) comes with every registration. It gives up after 10 seconds
and reports unhealthy, so a health check never waits on a slow Graph.
The shared Graph client allows each request 3 minutes rather than the default 100 seconds. That is deliberate: a calendar delta page over a long window can take that long to come back.
Send-only mail
AddMicrosoftGraphMailSend() registers a send-only IGraphMailService under the key mail-send
(MicrosoftGraphMailSend.ServiceKey), resolved with GetRequiredKeyedService<IGraphMailService>("mail-send").
It signs in with its own app registration from the MicrosoftGraph:MailSend section (ClientId,
CredentialKind, ClientSecret or ManagedIdentityClientId, Authority), separate from the shared client. The app needs only Exchange's Application
Mail.Send role, scoped to its mailboxes. Callers set ResolveSentCopy to false, because the app holds
only the right to send and cannot read the mailbox's Sent Items.
Errors
A failed Graph call throws InvalidOperationException with the Graph ODataError as its inner
exception and the Graph error code in its message. The package does not log it: the caller that
handles the exception logs it, so each failure is recorded once.
IGraphMailService.GetMessageByIdAsync returns null, rather than throwing, when the message or the
mailbox is gone (ErrorItemNotFound, ResourceNotFound, Request_ResourceNotFound,
ErrorInvalidUser, MailboxNotEnabledForRESTAPI). GetFileAttachmentInfosAsync returns an empty list
for the same reasons; OpenFileAttachmentAsync throws.
IMicrosoftGraphBotInstaller reads the Teams app definition's permissions once per catalog id per
hour, so a republished app definition is used within the hour without a restart.
The install request body carries no @odata.type key. Versions 0.4.0 to 0.10.0 sent it as an
explicit null, which made Graph refuse every personal install with expected a string for ODataType value; move to 0.10.1 or later.
Bookings
GetAvailableSlotsAsync returns bookable start times, not the free ranges Graph reports. It cuts
them to the service's length on the scheduling interval, applies the minimum lead time and
maximum advance (the service's own policy, else the business's), and counts only the staff assigned
to the service. Start times sit on a grid counted from midnight in BusinessTimeZone, so a 60-minute
service in India starts at 10:00, not 10:30. The grid restarts at every local midnight, which keeps it
aligned after a daylight-saving change.
"MicrosoftGraph": {
"Bookings": {
"BookingBusinessId": "Sales@contoso.com",
"BusinessTimeZone": "Asia/Kolkata"
}
}
BusinessTimeZone is an IANA id and must match the zone the Bookings business itself uses. The app
refuses to start when it is missing or names a zone the host does not know. On first use the service
also reads the zone the Bookings business reports (its booking page settings) and refuses to offer or
book times while the two differ, with an error naming both. When Graph reports no zone it can resolve,
the service logs a warning and carries on, since there is nothing to compare.
BookAsync checks the time is still free, books one of the free staff members so their calendar is
blocked, and fails with BookingSlotUnavailableError when the time has gone.
ReassignStaffAsync replaces the staff on an existing appointment and sends Graph only the new staff
list, so the appointment keeps its id, time, customer and Teams link. Staff already on the appointment
need no check. Anyone being added must be free for the appointment's whole length, read the same way
BookAsync reads it, because Graph accepts a staff member who is busy. A busy one fails with
BookingSlotUnavailableError and nothing changes; an appointment that is gone fails with
BookingAppointmentNotFoundError. GetStaffMembersAsync lists the business's staff (id, name, email,
role) to choose from.
CancelAppointmentAsync cancels an appointment through Bookings' cancel action, so the customer and staff
receive Microsoft's cancellation notice carrying your message. Bookings removes a cancelled appointment,
so cancelling one that is already cancelled fails with BookingAppointmentNotFoundError, the same as one
that never existed; a caller retrying a cancel can treat that error as done.
GetStaffMembersOfAllBusinessesAsync lists the staff of every Bookings business the app can read, not only
the configured one, once per email address (case-insensitive, first occurrence wins; staff with no email
are left out).
RemoveStaffMemberFromAllBusinessesAsync deletes the staff member with an email address from every
Bookings business and returns the names of the businesses they were removed from (empty when they were on
none). Deleting a staff member removes them from every service of that business; existing appointments
keep their history. A failure on one business does not stop the rest; the result then fails with one
message per business that could not be changed.
Search connector items
IGraphSearchItemService writes items into a Graph search connection (the connection and its schema
come from IGraphSearchConnectionManager).
UpsertItemAsync(connectionId, item)creates or replaces one item (PUT/external/connections/{id}/items/{itemId}).UpsertManyAsync(connectionId, items)upserts them one after another and stops at the first failure. Upserting is idempotent, so the whole batch can be sent again.DeleteItemAsync(connectionId, itemId)removes an item; one that is already gone counts as deleted.
When Graph answers 429 or 503, the service waits the Retry-After it sends (5 seconds when absent,
never more than 5 minutes) and tries again, up to 5 attempts, before the failure is thrown.
A GraphSearchItem carries an Id, Properties (name to value, matching the connection schema),
Content with its ContentType (Text or Html) and an Acl.
await items.UpsertItemAsync(connectionId, new GraphSearchItem(
Id: "doc-42",
Properties: new Dictionary<string, object> { ["title"] = "Quarterly plan" },
Content: "Full text to index",
ContentType: GraphSearchContentType.Text,
Acl: new[]
{
new GraphSearchAclEntry(GraphSearchAclType.Group, salesGroupObjectId, GraphSearchAccessType.Grant),
}));
ACLs decide who sees an item in search results, and Graph requires at least one entry, so the
service refuses an item with none. Type is User, Group or Everyone; Value is the Entra object
id of the user or group, or the tenant id for Everyone. AccessType is Grant or Deny, and a deny
wins over a grant.
Permission: the app needs ExternalItem.ReadWrite.OwnedBy (application), which lets it write items
only into connections it created. Each customer tenant must grant admin consent to the app for that
permission before items can be written there.
Converting Graph times
Graph sends a time as a wall clock plus a zone name, and the zone may be a Windows id
(India Standard Time) or an IANA id (Asia/Kolkata). Use GraphTimeZones rather than
TimeZoneInfo.FindSystemTimeZoneById or appending Z, so a product reads Graph times exactly as the
package does.
if (GraphTimeZones.TryResolve(options.BusinessTimeZone, out var businessTimeZone))
{
// businessTimeZone is a TimeZoneInfo, whichever family the id was named in.
}
if (!GraphTimeZones.TryConvertToUtc(graphEvent.Start, out var startDateTimeUtc))
{
// Graph sent a time that cannot be converted: unknown zone, unreadable time, or a time inside a
// daylight-saving gap. startDateTimeUtc is null; do not guess.
}
TryConvertToUtc returns true with a null time when Graph sent no time at all, so "absent" and
"unconvertible" stay distinguishable. It never treats the wall clock as UTC.
No environment switch
The package always registers the real service. A host that must not reach Microsoft 365 outside Production registers its own stand-in implementation of the interface and decides when.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.0
- Azure.Identity (>= 1.21.0)
- FluentResults (>= 4.0.0)
- Microsoft.Extensions.Caching.Memory (>= 10.0.13)
- Microsoft.Extensions.Http.Resilience (>= 10.10.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.13)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.13)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.13)
- MimeKitLite (>= 4.18.1)
- Webority.Domain.Abstractions (>= 0.4.7)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Webority.Graph:
| Package | Downloads |
|---|---|
|
Webority.Email.Graph
Microsoft 365 for Webority.Email over Webority.Graph: inbound mail (messages, attachments, sent items, subscriptions, change notifications) shared by outreach and support, and the backup sender for Critical mail through a shared mailbox. |
|
|
Webority.Email.Outreach.Graph
Microsoft 365 adapter for Webority.Email.Outreach over Webority.Graph: sends from a mailbox, reads replies and sent items, creates and renews mail subscriptions, parses the change notifications, and checks a send-as address against the mailbox's aliases. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.32.0 | 0 | 11/3/2026 |
| 0.31.1 | 0 | 11/3/2026 |
| 0.31.0 | 0 | 11/3/2026 |
| 0.30.0 | 114 | 11/2/2026 |
| 0.29.1 | 591 | 11/1/2026 |
| 0.29.0 | 132 | 11/1/2026 |
| 0.28.4 | 54 | 11/1/2026 |
| 0.28.3 | 54 | 11/1/2026 |
| 0.28.2 | 89 | 11/1/2026 |
| 0.28.1 | 229 | 10/30/2026 |
| 0.28.0 | 70 | 10/29/2026 |
| 0.27.2 | 56 | 10/29/2026 |
| 0.27.1 | 56 | 10/29/2026 |
| 0.27.0 | 53 | 10/29/2026 |
| 0.26.0 | 57 | 10/29/2026 |
| 0.25.2 | 71 | 10/28/2026 |
| 0.25.1 | 84 | 10/28/2026 |
| 0.25.0 | 94 | 10/28/2026 |
| 0.24.0 | 238 | 10/25/2026 |
| 0.23.0 | 223 | 10/20/2026 |