簡體   English   中英

C# 交換 Web 服務托管 API 模擬 -> Microsoft Graph ZDB974238714CA8DE634A7CE108A14F

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

我有一個 c# 應用程序可以查詢我們的 Microsoft Exchange 服務器(現在是 Exchange Online)。 它是使用 Microsoft.Exchange.WebServices .NET 庫編寫的。 IIS 中的應用程序池在 Exchange 中具有提升權限的帳戶下運行。 這允許它查詢所有用戶的日歷,以便應用程序可以顯示他們是忙/不在辦公室還是在其他地方工作。 _service.ImpersonatedUserId = new ImpersonatedUserId(ConnectingIdType.SmtpAddress, emailAddress); 設置服務告訴服務應用程序池帳戶將模擬用戶(電子郵件地址)來查詢日歷。

綜上所述,Microsoft Exchange Web Services Managed API 將在今年年底前折舊。 我想用 Microsoft Graph 重寫這個過程。 我找到了大量關於如何使用這個來訪問交換數據和查詢日歷的信息。

有沒有人有任何他們發現如何使用 Microsoft Graph API 完成下面的 function 的好例子? 是否有我可以使用的 .NET 包裝器 class 或者我需要使用 REST Z2567A5EC9705EB7AC2DZ98E 服務端點並創建我自己的服務端點43?

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您可以使用Microsoft提供的sdk並通過Graph API Endpoints實現上述功能。 可以在此處找到各種平台的 sdk 概述以及示例。

您還可以嘗試使用圖形資源管理器及其 postman 集合來了解 API 端點。

Github 鏈接到MS-GRAPH-DOTNET-SDK

我能夠通過為我的應用注冊設置 Microsoft Graph 應用程序 API 權限來完成此操作。 對於我的場景,我需要 Calendars.Read + Users.Read.All + Groups.Read.All + GroupMember.Read.All。 這些權限必須由 Azure 管理員授予管理員同意,然后我才能使用它們。 在 Azure 中創建客戶端密碼后,我從 GitHub 中引用了此示例以開始使用。 最后,當我從 Azure AD、append 獲取令牌時,我創建了一個擴展 class 以請求並檢索特定組用戶的當前日歷約會。 隨心所欲地引用它,我希望它在未來對其他人有所幫助。

/// <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();
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM