简体   繁体   中英

C# Exchange Web Services Managed API Impersonation -> Microsoft Graph API

I have a c# application that queries our Microsoft Exchange servers (now Exchange Online). It was written using Microsoft.Exchange.WebServices .NET library. The application pool in IIS runs under an account with elevated permissions in Exchange. This allows it to query the calendars of all users so that the application can show if they are busy/out of office or working elsewhere. _service.ImpersonatedUserId = new ImpersonatedUserId(ConnectingIdType.SmtpAddress, emailAddress); sets the service tells the service that the app pool account will be impersonating the user (email address) to query the calendar.

All of that being said, the Microsoft Exchange Web Services Managed API will be depreciated by the end of this year. I want to re-write this process with Microsoft Graph. I have found a good amount of information on how to access the exchange data and query calendars by using this .

Does anyone have any good examples they have found of how to accomplish the function below using Microsoft Graph API? Is there a .NET wrapper class I can use or do I need to use the REST web service endpoints and create my own?

public FindItemsResults<Appointment> GetCalendarAppointments(string emailAddress, string calendarName, DateTime start, DateTime end)
{
        // start with on prem exchange
        _service.UseDefaultCredentials = true; // use app pool security context
        _service.Url = new Uri(ConfigurationManager.ConnectionStrings["ExchangeURL"].ConnectionString);

        _service.ImpersonatedUserId = new ImpersonatedUserId(ConnectingIdType.SmtpAddress, emailAddress);

        FolderView folderView = new FolderView(25);
        folderView.PropertySet = new PropertySet(BasePropertySet.IdOnly);
        folderView.PropertySet.Add(FolderSchema.DisplayName);
        SearchFilter searchFilter = new SearchFilter.IsEqualTo(FolderSchema.DisplayName, calendarName);
        folderView.Traversal = FolderTraversal.Deep;
        FindFoldersResults findFolderResults = _service.FindFolders(WellKnownFolderName.Root, searchFilter, folderView);

        if (findFolderResults.TotalCount == 0)
            return null;

        FindItemsResults<Appointment> appointments;
        CalendarFolder calendarFolder;
        CalendarView calendarView = new CalendarView(start, end, 30);

        calendarView.PropertySet = new PropertySet(AppointmentSchema.Id,
                                                    AppointmentSchema.Start,
                                                    AppointmentSchema.End,
                                                    AppointmentSchema.Subject,
                                                    AppointmentSchema.Location);

        calendarFolder = (CalendarFolder)findFolderResults.Folders[0];

        try
        {
            appointments = calendarFolder.FindAppointments(calendarView);
        }
        catch (Exception e)
        {
            if (e.Message == "The SMTP address has no mailbox associated with it.")
            {
                // try exchange online
                _service.Credentials = new WebCredentials(ConfigurationManager.ConnectionStrings["ExchangeOnlineServiceAccountUsername"].ConnectionString,
                                                          ConfigurationManager.ConnectionStrings["ExchangeOnlineServiceAccountPassword"].ConnectionString);

                _service.Url = new Uri(ConfigurationManager.ConnectionStrings["ExchangeOnlineUrl"].ConnectionString);

                try
                {
                    appointments = calendarFolder.FindAppointments(calendarView);
                }
                catch (Exception ex)
                {
                    throw new Exception("Error when trying to read exchange to get calendar " + calendarName + " from exchange online inbox " + emailAddress + ": " + ex.Message);
                }

            }
            else
            {
                throw new Exception("Error when trying to read exchange to get calendar " + calendarName + " from on prem exchange inbox " + emailAddress + ": " + e.Message);
            }
        }

        if (appointments == null || appointments.Items.Count < 1)
            return null;

        return appointments;
}

@Eric You can use the sdk's provided by Microsoft and achieve the above functionalities with the Graph API Endpoints. The overview of the sdk's for various platforms along with examples can be found here .

You can also try the Graph explorer and their postman collection to have an idea on the API endpoints.

Github link to MS-GRAPH-DOTNET-SDK .

I was able to accomplish this by setting up Microsoft Graph Application API permissions for my App Registration. For my scenario I needed Calendars.Read + Users.Read.All + Groups.Read.All + GroupMember.Read.All. These permissions had to be granted Admin Consent by an Azure Admin before I could use them. After creating a client secret in Azure, I referenced this example from GitHub to get started. In the end, I created an extension class when I obtain the token from Azure AD, append it to the request and retrieve current calendar appointments for users of a specific group. Reference it however you like and I hope it helps someone else in the future.

/// <summary>
/// Class will contain all MS graph API types of requests for now
/// </summary>
/// <see cref="https://github.com/microsoftgraph/msgraph-sdk-dotnet" />
public class MicrosoftGraphExtensions
{
    private GraphServiceClient GraphServiceClient;

    public MicrosoftGraphExtensions()
    {
        // Note: Per post at https://prcode.co.uk/2020/03/24/microsoft-graph-client-clientcredentialprovider-not-recognised/
        // the Microsoft.Graph.Auth nuget package (which is required to use the ClientCredentialProvider code below)
        // is not yet available except of pre-release.  
        // For now, we can use the following method and manually add the token to the authorization header of the API
        GraphServiceClient = new GraphServiceClient(new DelegateAuthenticationProvider(async (request) =>
        {
            string[] tokenScopes = ConfigurationManager.ConnectionStrings["Azure_TokenScopes"].ConnectionString.Split(new char[] { ',' });

            // build the confidential client application the same way as before
            var confidentailClient = ConfidentialClientApplicationBuilder
                .Create(ConfigurationManager.ConnectionStrings["CLIENTIDFROMAZURE"].ConnectionString)
                .WithTenantId(ConfigurationManager.ConnectionStrings["TENANTIDFROMAZURE"].ConnectionString)
                .WithClientSecret(ConfigurationManager.ConnectionStrings["CLIENTSECRETFROMAZURE"].ConnectionString)
                .Build();

            // Retrieve an access token for Microsoft Graph (gets a fresh token if needed).
            var authResult = await confidentailClient.AcquireTokenForClient(tokenScopes).ExecuteAsync().ConfigureAwait(false);

            // Add the access token in the Authorization header of the API
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);

        }));

        /* eventually we should be able to do the following when the nuget package is available

           IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
               .Create(ConfigurationManager.ConnectionStrings["Azure_ClientId"].ConnectionString)
               .WithTenantId(ConfigurationManager.ConnectionStrings["Azure_TenantId"].ConnectionString)
               .WithClientSecret(ConfigurationManager.ConnectionStrings["Azure_ClientSecret"].ConnectionString)
               .Build();

           // to reference different authProviders supported with graph, look https://docs.microsoft.com/en-us/graph/sdks/choose-authentication-providers?tabs=CS
           ClientCredentialProvider authProvider = new ClientCredentialProvider(confidentialClientApplication);

           ClientCredentialProvider authProvider = new ClientCredentialProvider(confidentialClientApplication);
           GraphServiceClient = new GraphServiceClient(authProvider);

       */
    }

    /// <summary>
    /// Get a list of the group's members. A group can have users, devices, organizational contacts, and other groups as members. 
    /// This operation is transitive and returns a flat list of all nested members.
    /// </summary>
    /// <param name="groupName">displayName of the group</param>
    /// <returns>List of NON GROUP objects with only id, displayName & mail properties</returns>
    public async Task<IEnumerable<User>> GetGroupMembersAsync(string groupName)
    {
        var groups =
            await GraphServiceClient.Groups
            .Request()

            // https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter
            .Filter("displayName+eq+'" + groupName + "'")

            // want to select minimal properties necessary
            .Select("id,displayName")

            // we are assumning that the group name is unique so only get top 1
            .Top(1)

            .GetAsync();

        if (groups.FirstOrDefault() == null)
            throw new Exception("Group with name of " + groupName + " not found");

        var members =
            await GraphServiceClient.Groups[groups.FirstOrDefault().Id].TransitiveMembers
            .Request()

            // currently api does not support filtering by odata.type to
            // get users or groups etc but all of our role groups do not have emails
            // so we can filter them out this way

            // atm it seems like checking for null or empty strings isn't even supported
            // we would have to do it client side after query is complete
            //.Filter("displayName+ne+'Intern, Human Resources' and not startswith(surname,'Scanner')")

            .Select("id,displayName,mail,givenName,surname")

            .GetAsync();

        List<User> allUsers = new List<User>();

        var pageIterator = PageIterator<DirectoryObject>
            .CreatePageIterator(GraphServiceClient, members, (m) =>
            {
                // this is where we are filtering and only adding users to collection
                // only add users with email property who are not first name "Intern" and who are not last name "Scanner"
                // Not a fan of having to do this here, BUT can't find very many things that the .Filter attribute 
                // actually supports, so we need to do it somewhere
                if(m is User user && !string.IsNullOrEmpty(user.Mail) && user.Surname != "Intern" && user.Surname != "Scanner")
                {
                    allUsers.Add(user);
                }

                return true;
            });

        await pageIterator.IterateAsync();

        return allUsers;
    }

    /// <summary>
    /// Returns the current event the user is in that isn't marked as private, free,
    /// tentative or unknown.  If none is found, null is returned
    /// </summary>
    /// <param name="id">id of the user from MS Graph</param>
    /// <returns>A single event</returns>
    public async Task<Event> GetUsersCurrentAppointmentAsync(string id)
    {
        // give me anything that "occurs" within the specified timeframe
        // we use 3 min here because we know that is the typical update time from the client
        var queryOptions = new List<QueryOption>()
        {
            new QueryOption("startDateTime", DateTime.UtcNow.ToString("o")),
            new QueryOption("endDateTime", DateTime.UtcNow.ToString("o"))
        };

        var events =

            await GraphServiceClient.Users[id].CalendarView
            .Request(queryOptions)

            // https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter
            .Filter(
                    "sensitivity+eq+'normal'" + // show apts that are marked normal sensitivity
                    " and showAs+ne+'free'" + // show apts that are not marked showAs = free
                    " and showAs+ne+'tentative'" + // show apts that are not marked showAs = tentative
                    " and showAs+ne+'Unknown'" + // show apts that are nto marked showAs = unknown
                    " and isCancelled+eq+false" // show apts that have not been cancelled
                    )

            // want to select minimal properties necessary
            .Select("showAs,location,start,end,sensitivity")

            .GetAsync();

        if (events.Count < 1)
            return null;

        // once its back client side, we will only return one appointment
        // out of office takes precedence
        // then working elsewere
        // then finally Busy
        List<Event> lstEvents = events.ToList();

        // oof takes precedence so start with that
        if (lstEvents.Where(e => e.ShowAs == FreeBusyStatus.Oof).ToList().Count > 0)
        {
            // we know there is at least one oof apt, is there more?
            if(lstEvents.Where(e => e.ShowAs == FreeBusyStatus.Oof).ToList().Count > 1)
            {
                // there is more than one, so we show the one ending LATEST
                return lstEvents.Where(e => e.ShowAs == FreeBusyStatus.Oof).OrderByDescending(e => e.End.DateTime).FirstOrDefault();
            }
            else
            {
                // we know there is only one, so return that
                return lstEvents.Where(e => e.ShowAs == FreeBusyStatus.Oof).FirstOrDefault();
            }
        }

        // now do workingElsewhere
        if (lstEvents.Where(e => e.ShowAs == FreeBusyStatus.WorkingElsewhere).ToList().Count > 0)
        {
            // we know there is at least one workingelsewhere apt, is there more?
            if (lstEvents.Where(e => e.ShowAs == FreeBusyStatus.WorkingElsewhere).ToList().Count > 1)
            {
                // there is more than one, so we show the one ending LATEST
                return lstEvents.Where(e => e.ShowAs == FreeBusyStatus.WorkingElsewhere).OrderByDescending(e => e.End.DateTime).FirstOrDefault();
            }
            else
            {
                // we know there is only one, so return that
                return lstEvents.Where(e => e.ShowAs == FreeBusyStatus.WorkingElsewhere).FirstOrDefault();
            }
        }

        // finally do busy
        if (lstEvents.Where(e => e.ShowAs == FreeBusyStatus.Busy).ToList().Count > 0)
        {
            // we know there is at least one workingelsewhere apt, is there more?
            if (lstEvents.Where(e => e.ShowAs == FreeBusyStatus.Busy).ToList().Count > 1)
            {
                // there is more than one, so we show the one ending LATEST
                return lstEvents.Where(e => e.ShowAs == FreeBusyStatus.Busy).OrderByDescending(e => e.End.DateTime).FirstOrDefault();
            }
            else
            {
                // we know there is only one, so return that
                return lstEvents.Where(e => e.ShowAs == FreeBusyStatus.Busy).FirstOrDefault();
            }
        }

        // technically it should never get here because we are initially only getting apts not marked as showAs free, tentative or unknown
        // the only remaining possible showAs are handled above with oof, workingElsewhere and busy
        return lstEvents.OrderByDescending(e => e.End).FirstOrDefault();
    }

    /// <summary>
    /// Returns the calendar view for the given user principal name
    /// </summary>
    /// <param name="userPrincipalName">UserPrincipalName</param>
    /// <param name="start">Start time must be in UTC</param>
    /// <param name="end">End time must be in UTC</param>
    /// <returns></returns>
    public async Task<List<Event>> GetUserCalendar(string userPrincipalName, string calendarName, DateTime start, DateTime end)
    {
        var users =
            await GraphServiceClient.Users
            .Request()

            .Filter("userPrincipalName+eq+'" + userPrincipalName + "'")

            .Select("id")

            .Top(1)

            .GetAsync();

        User user = users.FirstOrDefault();

        if (user == null)
            throw new Exception("Could not find user " + userPrincipalName + ".");

        // next we have to get the id for the calendar by name provided
        var calendars =
            await GraphServiceClient.Users[user.Id].Calendars
            .Request()

            .Filter("name+eq+'" + calendarName + "'")

            .Select("id")

            .GetAsync();

        Calendar calendar = calendars.FirstOrDefault();

        if (calendar == null)
            throw new Exception("Could not find calendar with name " + calendarName + " for user " + userPrincipalName);

        // give me anything that "occurs" within the specified timeframe
        // we use 3 min here because we know that is the typical update time from the client
        var queryOptions = new List<QueryOption>()
        {
            new QueryOption("startDateTime",start.ToString("o")),
            new QueryOption("endDateTime", end.ToString("o"))
        };

        var events =

            await GraphServiceClient.Users[user.Id].Calendars[calendar.Id].CalendarView
            .Request(queryOptions)

            // https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter

            // want to select minimal properties necessary
            .Select("id,subject,location,start,end")

            .GetAsync();

        return events.ToList();
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM