Webority.Graph
0.24.0
See the version list below for details.
dotnet add package Webority.Graph --version 0.24.0
NuGet\Install-Package Webority.Graph -Version 0.24.0
<PackageReference Include="Webority.Graph" Version="0.24.0" />
<PackageVersion Include="Webority.Graph" Version="0.24.0" />
<PackageReference Include="Webority.Graph" />
paket add Webority.Graph --version 0.24.0
#r "nuget: Webority.Graph, 0.24.0"
#:package Webority.Graph@0.24.0
#addin nuget:?package=Webority.Graph&version=0.24.0
#tool nuget:?package=Webority.Graph&version=0.24.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>",
"ClientSecret": "<secret>",
"Authority": "https://login.microsoftonline.com/<tenant id>"
}
}
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 |
|
| 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.
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.
Upgrading to the next release
- Breaking:
GetFileAttachmentsAsyncis removed; inbound attachments are listed and streamed.GetFileAttachmentInfosAsync(mailbox, messageId, ct)returnsGraphMailAttachmentInfo(id, name, content type, size, inline flag) without any bytes, andOpenFileAttachmentAsync(mailbox, messageId, attachmentId, ct)returns the content as a stream; disposing the stream releases the response. Check size and count on the list, then open only the files you keep. Callers ofGetFileAttachmentsAsyncswitch to the two new methods; a class implementingIGraphMailServicemust replace the old member with both.GraphMailAttachmentremains for outgoing mail only. SameMail.Readpermission. - Calendar gains
UpdateEventAsync(organizerId, eventId, ...). It changes an event in place and returns false when the event no longer exists. Callers need nothing; a class implementingIMicrosoftGraphCalendarService(a host's stand-in, for example) must add the member. It uses the sameCalendars.ReadWriteapplication permission that creating an event already needs.
Upgrading to 0.18.0
- Directory gains
IDirectoryPrincipalReader.GetByIdsAsync(objectIds)reads users, groups and service principals by object id, sending at most 1,000 ids per Graph call, and returns only the objects Graph sends back.GetUserConsentPoliciesAsync()returns the permission grant policies users may consent under; an empty list means user consent to apps is disabled. Register it withAddMicrosoftGraphDirectory(). Callers need nothing, and no existing interface changes. Reading the directory objects needs theDirectory.Read.Allapplication permission; reading the consent policies needs thePolicy.Read.Allapplication permission.
Upgrading to 0.17.0
- Mail gains
ListSubscriptionsAsync(). It returns every change-notification subscription the app owns, with its id, resource, notification address and expiry, so a host can keep one live subscription per mailbox: renew one and delete the rest.GraphSubscriptionResultgainsNotificationUrl, also set byCreateSubscriptionAsync. A class implementingIGraphMailServicemust add the method.
Upgrading to 0.15.0
- Entra gains
GetLicenseSubscriptionsAsync(). It returns oneLicenseSubscriptionInfoper Microsoft 365 commerce subscription in the tenant (/directory/subscriptions): SKU, purchased seats, renewal date, trial flag and status.AssignedSeatscomes from/subscribedSkus, where Microsoft counts seats in use per SKU, so two subscriptions of one SKU carry the same count. A renewal date of 9999-12-31 or none is null. A failed Graph call is a failedResult, not an exception. Callers need nothing; a class implementingIMicrosoftGraphEntraService(a host's stand-in, for example) must add the member. Reading subscriptions needs theLicenseAssignment.Read.AllorDirectory.Read.Allapplication permission.
Upgrading to 0.14.0
- Bookings gains
GetStaffMembersOfAllBusinessesAsync()andRemoveStaffMemberFromAllBusinessesAsync(emailAddress). They work across every Bookings business the app can read, not only the configuredBookingBusinessId. Callers need nothing; a class implementingIMicrosoftGraphBookingsService(a host's stand-in, for example) must add both members. Listing businesses and deleting staff needs theBookings.ReadWrite.Allapplication permission.
Upgrading to 0.13.0
GraphMailSendRequestgainsListUnsubscribeUrl. When it is set, the message carriesList-Unsubscribe: <url>andList-Unsubscribe-Post: List-Unsubscribe=One-Click(RFC 8058), so Gmail and Yahoo show their one-click unsubscribe. Graph's JSON send accepts onlyx-headers, so a request with the URL always goes through the MIME send, with or without a plain-text body. Callers that leave it null need nothing. (TASK-15584)
Upgrading to 0.12.1
- The package builds on
Microsoft.Graph6.7.0 (was 6.2.0). A host that referencesMicrosoft.Graphdirectly should reference 6.7.0 or later, so that one version loads.Microsoft.Graph.Corestays 4.0.1. No public member changed, so callers and implementers need nothing. (TASK-15500)
Upgrading to 0.12.0
- Calendar gains
CreateOutOfOfficeBlockAsync(userId, subject, body, startDateTimeUtc, endDateTimeUtc). It creates a timed event in the user's own calendar that shows them as out of office, with no reminder, no attendees and no response request. Microsoft Bookings counts the user busy for the whole span, so it blocks Bookings slots (an office holiday, for example). Bookings ignores all-day events, so block a whole day as local midnight to the next local midnight. (TASK-15490) - Calendar gains
DeleteEventAsync(userId, eventId). It deletes an event without notifying anyone, for events that have no attendees (CancelEventAsyncis for meetings and refuses them). An event that is already gone counts as deleted, so a retry is safe. - Both use the
Calendars.ReadWriteapplication permission that creating an event already needs. Callers need nothing; a class implementingIMicrosoftGraphCalendarService(a host's stand-in, for example) must add both members.
Upgrading to 0.11.0
- List methods now read every page, not only the first. Teams, channels, chats, members and messages; calendar events for a window (was Graph's first ten); To Do lists; Bookings services and staff; direct reports in the org chart; and the messages of a mail conversation (was the first fifty) all return the full result. A caller that relied on the short list gets more rows and may make more Graph requests. (BUG-12838, BUG-12840, BUG-12843, BUG-12844, BUG-12845, BUG-12846)
- A Teams message read with no
topnow reads the whole history. Channel and chat message reads stop at the requested count whentopis given, so passtopwherever the history can be long. (BUG-12843) - Mail sends record their own sent copy. A MIME send or a reply finds its sent copy by message id, so two sends with the same subject no longer swap ids. The lookup starts after half a second and backs off, instead of waiting five seconds first. (BUG-12836, BUG-12837)
- Presence for more than 650 people is requested in groups of 650, which Graph accepts. (BUG-12842)
- Concurrency. People search fetches photos up to eight at a time, alongside presence (BUG-12835). The org chart reads the profile, manager and each level's reports and photos concurrently (TASK-13640). Free-slot lookups read the Bookings settings and the service at the same time (TASK-13639). Bulk search item upload sends up to four items at a time (TASK-13648). A host's Graph throttling budget sees these requests in parallel.
- Callers and implementers of the Graph interfaces need to change nothing.
Upgrading to 0.9.0
- Entra gains
SetPasswordAsync(userIdOrUpn, password, forceChangeNextSignIn). It sets a user's password by patching the user's password profile. A missing user throwsKeyNotFoundException, a rejected password (policy) throwsInvalidOperationException, and missing permission throwsUnauthorizedAccessException. Callers need nothing; a class implementingIMicrosoftGraphEntraService(a host's stand-in, for example) must add the member. The shared app needs theUser-PasswordProfile.ReadWrite.Allapplication permission.
Upgrading to 0.8.0
- The search connector gains
IGraphSearchItemService, registered byAddGraphSearchConnector()besideIGraphSearchConnectionManager. It upserts and deletes the items in a connection, each with the ACL that decides who sees it in search results (see Search connector items). It is a new interface, so no existing member changed and a class implementing another Graph interface needs nothing. - Check the app's Graph permission before writing items: it needs
ExternalItem.ReadWrite.OwnedBy(application), and each customer tenant must grant admin consent for it. Without it every write fails with a Graph authorisation error. - Callers that do not write items need nothing.
Upgrading to 0.7.0
- Mail gains
GetFileAttachmentsAsync(mailboxUpn, graphMessageId). It returns each file attached to a message with its bytes, name, content type and whether it is shown inline. Attached Outlook items and cloud links are not files and are left out. The app needsMail.Read(application), which reading messages already needs. GraphMailMessagegainsCcEmailsandHasAttachments, filled by every message read.- Callers need nothing. A class implementing
IGraphMailService, such as a host's stand-in (Webority OS'sNoOpGraphMailService), must add the member.
Upgrading to 0.6.0
- Bookings gains
CancelAppointmentAsync(appointmentId, cancellationMessage). It cancels an appointment through Bookings' cancel action, so the customer and staff receive Microsoft's cancellation notice with the message, and returns aResult. Bookings removes a cancelled appointment, so a repeat cancel fails withBookingAppointmentNotFoundError; treat that as done when retrying. Callers need nothing; a class implementingIMicrosoftGraphBookingsService(a host's stand-in, for example) must add the member. Check the app's Graph permission: Microsoft documents the cancel action as needing theBookings.ReadWrite.All(orBookings.Manage.All) application permission, while booking works withBookingsAppointment.ReadWrite.Allalone. An app granted only the latter must be grantedBookings.ReadWrite.Allbefore cancelling.
Upgrading to 0.5.0
- Bookings gains
ReassignStaffAsync(appointmentId, staffMemberIds)andGetStaffMembersAsync(). Reassigning moves an appointment to other staff in place, keeping its id and Teams link; it returns aResultthat fails withBookingAppointmentNotFoundErrororBookingSlotUnavailableError. Callers need nothing; a class implementingIMicrosoftGraphBookingsService(a host's stand-in, for example) must add both members. No new Graph permission: reading staff is already needed for available slots, and changing an appointment uses the same Bookings write access as booking one.
Upgrading to 0.4.1
- Calendar gains
CancelEventAsync(organizerId, eventId, comment). It cancels an event in the organizer's calendar through Graph's cancel action, so every attendee receives Microsoft's cancellation notice with the comment. An event that is already gone counts as cancelled, so a retry is safe. Callers need nothing; a class implementingIMicrosoftGraphCalendarService(a host's stand-in, for example) must add the member. It uses the sameCalendars.ReadWriteapplication permission that creating an event already needs.
Upgrading to 0.3.4
- Entra gains
GetUserMailAddressesAsync(userIdOrPrincipalName). It returns the mailbox's primary address and every SMTP alias, lower-cased, as aResult. Callers need nothing; a class implementingIMicrosoftGraphEntraService(a host's stand-in, for example) must add the member. The shared app already reads users, so no new Graph permission is required.
Upgrading to 0.3.0 from 0.2.x
0.3.0 breaks the public API. What a consumer changes:
- Calendar: four members are gone.
UpdateEventAsync,DeleteEventAsync,GetEventAsyncandCreateOnlineMeetingAsynconly ever threwNotSupportedException. Delete any call and any implementation in a stand-in; read an event withGetUserEventByIdAsync(userId, eventId). - Date parameters are renamed to say they are UTC. Positional calls compile unchanged; named
arguments and every class implementing these interfaces (stand-ins included) must use the new names:
IMicrosoftGraphCalendarService:startTimeUtc/endTimeUtcandstartDateTime/endDateTimebecomestartDateTimeUtc/endDateTimeUtc.IMicrosoftGraphRoomService:startUtc/endUtcbecomestartDateTimeUtc/endDateTimeUtc.IGraphMailService:sincebecomessinceDateTimeUtc,expirationDateTimebecomesexpiresDateTimeUtc,newExpirationDateTimebecomesnewExpiresDateTimeUtc.IMicrosoftGraphBookingsService.GetAvailableSlotsAsync:fromUtc/toUtcbecomefromDateTimeUtc/toDateTimeUtc.
- Entra hire date is a calendar date.
IMicrosoftGraphEntraService.UpdateUserPropertiesAsynctakesDateOnly? employeeHireDateinstead ofDateTimeOffset?. PassDateOnly.FromDateTime(...)of the date you hold; the package sends it to Graph as that day's UTC midnight. - Calendar windows are sent as UTC.
GetUserEventsAsyncandGetUserEventsDeltaAsyncnow convert the window to UTC before calling Graph. A caller that passed a non-UTC offset used to get a window shifted by that offset. - Bookings checks its zone. Slot and booking calls fail with an
InvalidOperationExceptionwhileMicrosoftGraph:Bookings:BusinessTimeZonediffers from the zone the Bookings business is set to. - Mailbox: an unresolvable out-of-office window is flagged, not guessed.
MailboxAutomaticReplies.ScheduledWindowUnresolvedis true (and the scheduled times null) when Graph's zone or time cannot be converted;ScheduledTimeZonecarries the zone Graph sent. - OData values are escaped. Filters and keys double a single quote in the value, and a null value
now throws
ArgumentNullExceptioninstead of searching for an empty string.
| 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.3)
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 |