Webority.Graph 0.24.0

There is a newer version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Webority.Graph" Version="0.24.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Webority.Graph" Version="0.24.0" />
                    
Directory.Packages.props
<PackageReference Include="Webority.Graph" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Webority.Graph --version 0.24.0
                    
#r "nuget: Webority.Graph, 0.24.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Webority.Graph@0.24.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Webority.Graph&version=0.24.0
                    
Install as a Cake Addin
#tool nuget:?package=Webority.Graph&version=0.24.0
                    
Install as a Cake Tool

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
Mail 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: GetFileAttachmentsAsync is removed; inbound attachments are listed and streamed. GetFileAttachmentInfosAsync(mailbox, messageId, ct) returns GraphMailAttachmentInfo (id, name, content type, size, inline flag) without any bytes, and OpenFileAttachmentAsync(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 of GetFileAttachmentsAsync switch to the two new methods; a class implementing IGraphMailService must replace the old member with both. GraphMailAttachment remains for outgoing mail only. Same Mail.Read permission.
  • 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 implementing IMicrosoftGraphCalendarService (a host's stand-in, for example) must add the member. It uses the same Calendars.ReadWrite application 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 with AddMicrosoftGraphDirectory(). Callers need nothing, and no existing interface changes. Reading the directory objects needs the Directory.Read.All application permission; reading the consent policies needs the Policy.Read.All application 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. GraphSubscriptionResult gains NotificationUrl, also set by CreateSubscriptionAsync. A class implementing IGraphMailService must add the method.

Upgrading to 0.15.0

  • Entra gains GetLicenseSubscriptionsAsync(). It returns one LicenseSubscriptionInfo per Microsoft 365 commerce subscription in the tenant (/directory/subscriptions): SKU, purchased seats, renewal date, trial flag and status. AssignedSeats comes 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 failed Result, not an exception. Callers need nothing; a class implementing IMicrosoftGraphEntraService (a host's stand-in, for example) must add the member. Reading subscriptions needs the LicenseAssignment.Read.All or Directory.Read.All application permission.

Upgrading to 0.14.0

  • Bookings gains GetStaffMembersOfAllBusinessesAsync() and RemoveStaffMemberFromAllBusinessesAsync(emailAddress). They work across every Bookings business the app can read, not only the configured BookingBusinessId. Callers need nothing; a class implementing IMicrosoftGraphBookingsService (a host's stand-in, for example) must add both members. Listing businesses and deleting staff needs the Bookings.ReadWrite.All application permission.

Upgrading to 0.13.0

  • GraphMailSendRequest gains ListUnsubscribeUrl. When it is set, the message carries List-Unsubscribe: <url> and List-Unsubscribe-Post: List-Unsubscribe=One-Click (RFC 8058), so Gmail and Yahoo show their one-click unsubscribe. Graph's JSON send accepts only x- 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.Graph 6.7.0 (was 6.2.0). A host that references Microsoft.Graph directly should reference 6.7.0 or later, so that one version loads. Microsoft.Graph.Core stays 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 (CancelEventAsync is for meetings and refuses them). An event that is already gone counts as deleted, so a retry is safe.
  • Both use the Calendars.ReadWrite application permission that creating an event already needs. Callers need nothing; a class implementing IMicrosoftGraphCalendarService (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 top now reads the whole history. Channel and chat message reads stop at the requested count when top is given, so pass top wherever 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 throws KeyNotFoundException, a rejected password (policy) throws InvalidOperationException, and missing permission throws UnauthorizedAccessException. Callers need nothing; a class implementing IMicrosoftGraphEntraService (a host's stand-in, for example) must add the member. The shared app needs the User-PasswordProfile.ReadWrite.All application permission.

Upgrading to 0.8.0

  • The search connector gains IGraphSearchItemService, registered by AddGraphSearchConnector() beside IGraphSearchConnectionManager. 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 needs Mail.Read (application), which reading messages already needs.
  • GraphMailMessage gains CcEmails and HasAttachments, filled by every message read.
  • Callers need nothing. A class implementing IGraphMailService, such as a host's stand-in (Webority OS's NoOpGraphMailService), 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 a Result. Bookings removes a cancelled appointment, so a repeat cancel fails with BookingAppointmentNotFoundError; treat that as done when retrying. Callers need nothing; a class implementing IMicrosoftGraphBookingsService (a host's stand-in, for example) must add the member. Check the app's Graph permission: Microsoft documents the cancel action as needing the Bookings.ReadWrite.All (or Bookings.Manage.All) application permission, while booking works with BookingsAppointment.ReadWrite.All alone. An app granted only the latter must be granted Bookings.ReadWrite.All before cancelling.

Upgrading to 0.5.0

  • Bookings gains ReassignStaffAsync(appointmentId, staffMemberIds) and GetStaffMembersAsync(). Reassigning moves an appointment to other staff in place, keeping its id and Teams link; it returns a Result that fails with BookingAppointmentNotFoundError or BookingSlotUnavailableError. Callers need nothing; a class implementing IMicrosoftGraphBookingsService (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 implementing IMicrosoftGraphCalendarService (a host's stand-in, for example) must add the member. It uses the same Calendars.ReadWrite application 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 a Result. Callers need nothing; a class implementing IMicrosoftGraphEntraService (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, GetEventAsync and CreateOnlineMeetingAsync only ever threw NotSupportedException. Delete any call and any implementation in a stand-in; read an event with GetUserEventByIdAsync(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/endTimeUtc and startDateTime/endDateTime become startDateTimeUtc/endDateTimeUtc.
    • IMicrosoftGraphRoomService: startUtc/endUtc become startDateTimeUtc/endDateTimeUtc.
    • IGraphMailService: since becomes sinceDateTimeUtc, expirationDateTime becomes expiresDateTimeUtc, newExpirationDateTime becomes newExpiresDateTimeUtc.
    • IMicrosoftGraphBookingsService.GetAvailableSlotsAsync: fromUtc/toUtc become fromDateTimeUtc/toDateTimeUtc.
  • Entra hire date is a calendar date. IMicrosoftGraphEntraService.UpdateUserPropertiesAsync takes DateOnly? employeeHireDate instead of DateTimeOffset?. Pass DateOnly.FromDateTime(...) of the date you hold; the package sends it to Graph as that day's UTC midnight.
  • Calendar windows are sent as UTC. GetUserEventsAsync and GetUserEventsDeltaAsync now 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 InvalidOperationException while MicrosoftGraph:Bookings:BusinessTimeZone differs from the zone the Bookings business is set to.
  • Mailbox: an unresolvable out-of-office window is flagged, not guessed. MailboxAutomaticReplies.ScheduledWindowUnresolved is true (and the scheduled times null) when Graph's zone or time cannot be converted; ScheduledTimeZone carries the zone Graph sent.
  • OData values are escaped. Filters and keys double a single quote in the value, and a null value now throws ArgumentNullException instead of searching for an empty string.
Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed