Запрос GET Microsoft Graph Api не может десериализовать объект JSON при вызове приложений

Я делаю запрос GET, чтобы вернуть все приложения с их идентификатором и датой истечения срока действия пароля. Я работал с Graph Explore, который предлагает Microsoft, и мне это очень помогло. Код, который у меня есть, возвращает HTTP-запрос, а затем при попытке десериализации он терпит неудачу с приведенной ниже ошибкой. Я потратил много времени на SOF, просматривая аналогичный пост, но, похоже, ничего не работает. Я чувствую, что, возможно, здесь что-то упущено или я делаю это неправильно.

Я также использую более старую версию Graph Nugget pkg v4.41.0, а Core — v2.0.13. Я пошел их обновить, и в приложении было много критических изменений, и у меня пока нет времени все переписывать.

Я получаю следующую ошибку:

"Message": "An error has occurred.", 
"ExceptionMessage": "Cannot deserialize the current JSON object (e.g. {\"name\":\"value\"}) into type 'System.Collections.Generic.List`1[Microsoft.Graph.Application]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly.\r\nTo fix this error either change the JSON to a JSON array (e.g. [1,2,3]) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List<T>) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object.\r\nPath '['@odata.context']', line 1, position 18.",
"ExceptionType": "Newtonsoft.Json.JsonSerializationException",
"StackTrace": "   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)\r\n   at Newtonsoft.Json.Serialization...
        public async Task<List<Microsoft.Graph.Application>> GetAppExpList()
        {
           List<Microsoft.Graph.Application> apps = null;                       

           // query string for url call
           var url = this.GetGraphUrl($"{Consts.GraphUrl}?$select=id,passwordCredentials&$format=json");
           
           // tried to do this with the built in Graph methods
           List<QueryOption> options = new List<QueryOption>
           {
            new QueryOption("$select", "id,DisplayName,passwordCredentials"), 
            new QueryOption("$format", "json")
           };

           // Here I wanted to see what was brought back. No PasswordCreds for some reason 
           var test = await _graphClient.Applications.Request(options).GetAsync();


          // GET request to the Applications Graph Api
          var response = await this.HttpHandler.SendGraphGetRequest(url);            
        
          // Returns JSON data 
          if (response.IsSuccessStatusCode)
          {
            // Shows the correct data in JSON Format
            var rawData = await response.Content.ReadAsStringAsync();

            // Throws error above.    
            apps = JsonConvert.DeserializeObject<List<Microsoft.Graph.Application>>(rawData);
                            
          }

         return apps; 
    }

Вот JSON, который возвращается для var rawData = await response.Content.ReadAsStringAsync();

{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#applications(id,passwordCredentials)"
 ,"value":[
 {
    "id":"00000000-0000-0000-0000-000000000000",
    "displayName":"App 1 name Here",
    "passwordCredentials":[]
 },
 {
    "id":"00000000-0000-0000-0000-000000000001",
    "displayName":" App 2 name here",
    "passwordCredentials":[]
 },
 {
    "id":"00000000-0000-0000-0000-000000000002",
    "displayName":"App 3 name here",
    "passwordCredentials":[
       {
        "customKeyIdentifier":null,
        "displayName":"secret",
        "endDateTime":"2025-01-30T14:46:40.985Z",
        "hint":"oHI",
        "keyId":"00000000-0000-0000-0000-0000000000",
        "secretText":null,
        "startDateTime":"2023-01-31T14:46:40.985Z"
        }
     ]
  }
]

}

value — это список в данных JSON. Весь json сам по себе не является списком.
jaabh 03.04.2024 22:18

извините, я обрезал, потому что есть куча приложений. Я отформатирую его правильно

whisk 03.04.2024 22:19

Для «взаимодействия» вы можете попробовать Collection<> вместо List<>.

Gerry Schmitz 08.04.2024 19:04

Я попробовал код _graphClient.Applications.Request(options).GetAsync() с той же версией SDK, и он работает нормально, паролиCredentials заполняются. Вносили ли вы какие-либо изменения в JsonSeriliazer?

user2250152 09.04.2024 09:06

Посмотрите ответ Оливера ниже — он на 100% правильный. Не анализируйте ничего вручную — SDK уже все проанализировал. А восстановить секрет вы не сможете, это вообще невозможно по очевидным соображениям безопасности.

Nikolay 11.04.2024 01:35
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
5
632
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Десериализация List<Microsoft.Graph.Application> не будет работать, поскольку ответом является объект JSON, а не массив JSON. Попробуйте десериализовать ответ как GraphServiceApplicationsCollectionPage

if (response.IsSuccessStatusCode)
{
    // Shows the correct data in JSON Format
    var rawData = await response.Content.ReadAsStringAsync();

    var response = JsonConvert.DeserializeObject<GraphServiceApplicationsCollectionPage>(rawData);
    
    apps = response.CurrentPage.ToList();
                            
}

Итак, когда я это делаю, он все равно взрывается и дает тот же результат. Он никогда не доходит до вызова DeserializeObject. Я не знал о классе GraphServiceApplicationsCollectionPage, так что было приятно узнать о нем.

whisk 05.04.2024 15:27

попробуйте код ниже, чтобы преобразовать результат строки.

var rawData = await response.Content.ReadAsStringAsync();
dynamic obj = JsonConvert.DeserializeObject<dynamic>(rawData);
var appList = obj.value;
List<Application> apps = JsonConvert.DeserializeObject<List<Application>>(appList.ToString());

Кстати, для получения результата я все же рекомендую использовать Graph sdk. Похоже, вы пытаетесь использовать V5 Graph SDK. Попробуйте мои коды ниже. Коды и конфигурации в Program.cs и appsettings.json одинаковы, необходимые пакеты nuget и коды GraphClient имеют некоторые различия.

<ItemGroup>
  <PackageReference Include = "Microsoft.AspNetCore.Authentication.JwtBearer" Version = "8.0.0" NoWarn = "NU1605" />
  <PackageReference Include = "Microsoft.AspNetCore.Authentication.OpenIdConnect" Version = "8.0.0" NoWarn = "NU1605" />
  <PackageReference Include = "Microsoft.Identity.Web" Version = "2.15.2" />
  <PackageReference Include = "Microsoft.Identity.Web.UI" Version = "2.15.2" />
  <PackageReference Include = "Microsoft.Identity.Web.DownstreamApi" Version = "2.15.2" />
    <PackageReference Include = "Microsoft.Identity.Web.GraphServiceClient" Version = "2.15.2" />
</ItemGroup>

//UserCollectionResponse
var users = await _graphServiceClient.Users.GetAsync();
//ApplicationCollectionResponse
var apps = await _graphServiceClient.Applications
                .GetAsync(requestionConfiguration =>
                {
                    requestionConfiguration.QueryParameters.Select = new string[] { "Id", "DisplayName", "passwordCredentials" };
                }
                );

Если вы используете поток учетных данных клиента, который использует разрешение Application Api, вы можете попробовать коды ниже.

 var scopes = new[] { "https://graph.microsoft.com/.default" };
 var tenantId = "tenantId";
 var clientId = "clientId";
 var clientSecret = "clientSecret";
 var clientSecretCredential = new ClientSecretCredential(
                 tenantId, clientId, clientSecret);
 var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
 //UserCollectionResponse
 var users = await graphClient.Users.GetAsync();
 //ApplicationCollectionResponse
 var apps = await graphClient.Applications
                 .GetAsync(requestionConfiguration =>
                 {
                     requestionConfiguration.QueryParameters.Select = new string[] { "Id", "DisplayName", "passwordCredentials" };
                 }
                 );

Я думаю, что @whisk нужно решение для SDK v4.

user2250152 09.04.2024 08:55
Ответ принят как подходящий

После того, как я попробовал то же самое и изучил ваш код, кажется, что вы слишком много пытаетесь. SDK уже выполняет за вас десериализацию, и вы можете просто работать со свойствами объектов. Этот пример кода вернет все приложения:

using Azure.Core;
using Microsoft.Graph;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Extensibility;

namespace ConsoleApp
{
    public static class Program
    {
        private static readonly string ClientId = "{your client id}";
        private static readonly string[] Scopes = new[] { "https://graph.microsoft.com/.default" };

        // Replace with your tenant id, if it is a single tenant app.
        private static readonly string TenantId = "common";

        private static AccessToken _cachedToken;

        public static async Task Main()
        {
            var credential = DelegatedTokenCredential.Create((_, _)
                => throw new NotSupportedException(),
                GetOrRequestAccessToken);

            var graphClient = new GraphServiceClient(credential, Scopes);

            var options = new[]
            {
                new QueryOption("$select", "id,displayName,passwordCredentials"),
            };

            var appsFound = await graphClient.Applications.Request(options).GetAsync();

            foreach (var app in appsFound)
            {
                Console.WriteLine($"Id: {app.Id}");
                Console.WriteLine($"Display Name: {app.DisplayName}");

                foreach (var password in app.PasswordCredentials)
                {
                    Console.WriteLine($"   Key Id: {password.KeyId}");
                    Console.WriteLine($"   Display Name: {password.DisplayName}");
                    Console.WriteLine($"   Start: {password.StartDateTime}");
                    Console.WriteLine($"   End: {password.EndDateTime}");
                    Console.WriteLine($"   Hint: {password.Hint}");
                }

                Console.WriteLine();
            }

        }

        private static async ValueTask<AccessToken> GetOrRequestAccessToken(TokenRequestContext _, CancellationToken __)
        {
            if (_cachedToken.ExpiresOn <= DateTimeOffset.UtcNow)
            {
                var app = PublicClientApplicationBuilder.Create(ClientId)
                    .WithExperimentalFeatures()
                    .WithAuthority(AzureCloudInstance.AzurePublic, TenantId)
                    .WithRedirectUri("http://localhost")
                    .Build();

                var result = await app.AcquireTokenInteractive(Scopes)
                    .OnBeforeTokenRequest(request =>
                    {
                        request.Headers.Add("Origin", "http://localhost");
                        return Task.CompletedTask;
                    })
                    .ExecuteAsync();

                _cachedToken = new AccessToken(result.AccessToken, result.ExpiresOn);
            }

            return _cachedToken;
        }
    }
}

Если вы хотите получить значение SecretText из Graph SDK, вы должны знать, что это невозможно согласно документации :

secretText: содержит надежные пароли, созданные Microsoft Entra ID, длиной от 16 до 64 символов. Сгенерированное значение пароля возвращается только во время первоначального запроса POST к addPassword. В будущем восстановить этот пароль невозможно.

Другие вопросы по теме