You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义RestClient调用Graph API创建变更通知订阅遇BadRequest错误

问题:自定义RestClient调用Microsoft Graph创建订阅返回BadRequest,官方SDK却成功

我尝试用自定义RestClient向Microsoft Graph API发送订阅变更通知的请求,收到BadRequest错误,提示无法处理订阅创建负载,需要检查属性命名是否为驼峰格式、DateTimeOffset属性格式是否合法。但使用官方SDK的GraphServiceClient传入相同参数(包括一致的NotificationUrl)却能成功创建订阅,两种实现代码如下:

自定义RestClient实现代码

public class GraphManager : RestClient, IGraphManager
{
    #region Private members

    private readonly IConfiguration _configuration;
    private static Dictionary<string, Subscription> Subscriptions = new Dictionary<string, Subscription>();
    private static Timer? subscriptionTimer = null;

    #endregion

    #region Constructors

    public GraphManager(IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
        : base(GetBaseUri(), GetAuthenticator(configuration, httpContextAccessor))
    {
        _configuration = configuration;
    }

    #endregion

    #region Private methods

    private static Uri GetBaseUri()
    {
        return new Uri("https://graph.microsoft.com");
    }

    private static Dictionary<string, string> GetRequestHeaders()
    {
        return new Dictionary<string, string>
            {
                { "ConsistencyLevel", "eventual" }
            };
    }

    private static IAuthenticator GetAuthenticator(IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
    {
        HttpRequest request = httpContextAccessor.HttpContext.Request;

        return new ConfidentialClientAuthenticator(configuration["Apps:Api:ClientId"],
            configuration["Apps:Api:ClientSecret"], configuration["Auth:Authority"],
            $"{request.Scheme}://{request.Host}", new[] { "https://graph.microsoft.com/.default" });
    }

    #endregion

    #region Public methods

    public async Task<string> GetSubscription()
    {
        var sub = new Microsoft.Graph.Subscription();
        sub.ChangeType = "updated";
        sub.NotificationUrl = "https://ngrok.url/api/notifications";
        sub.Resource = "/users";
        sub.ExpirationDateTime = DateTime.UtcNow.AddMinutes(15);
        sub.ClientState = "SecretClientState";

        string jsonString = System.Text.Json.JsonSerializer.Serialize(sub);

        string url = "v1.0/subscriptions";
        var response = await ProcessRequestAsync(HttpMethod.Post, url, jsonString);

        if (response.IsSuccessStatusCode)
        {
            var subscriptionResponse = await response.Content.ReadAsStringAsync();

            return subscriptionResponse;
        }
        else
        {
            throw new Exception($"Failed to create subscription: {response.StatusCode} - {response.ReasonPhrase}");
        }


    }
    #endregion
}

public class RestClient : IDisposable
{
    #region Private members

    private readonly HttpClient _client = new HttpClient();
    private readonly Uri _baseUri;
    private readonly IAuthenticator _authenticator;

    #endregion

    #region Constructors

    public RestClient(Uri baseUri, IAuthenticator authenticator, int timeout = 0)
    {
        _baseUri = baseUri;
        _authenticator = authenticator;

        if (timeout > 0)
        {
            _client.Timeout = TimeSpan.FromSeconds(timeout);
        }
    }

    public RestClient(IAuthenticator authenticator)
        : this(null, authenticator)
    {
    }

    #endregion

    #region IDisposable implementation

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    #endregion

    #region Private methods

    private Uri GetRequestUri(string requestUrl)
    {
        if (string.IsNullOrEmpty(requestUrl))
        {
            if (_baseUri == null)
                throw new InvalidOperationException("Invalid (empty) URL");

            return _baseUri;
        }
        else if (requestUrl.StartsWith("http://") || requestUrl.StartsWith("https://"))
        {
            return new Uri(requestUrl);
        }
        else
        {
            if (_baseUri == null)
                throw new InvalidOperationException("Invalid (relative) URL");

            return _baseUri.Append(requestUrl);
        }
    }


    #endregion

    #region Protected methods

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _client?.Dispose();
        }
    }

    protected string GetErrorFromResponse(HttpResponseMessage response)
    {
        return $"{response.StatusCode} (Details: '{response.Content.AsString()}')";
    }

    protected async Task<HttpResponseMessage> ProcessRequestAsync(HttpMethod httpMethod, string url,
                HttpContent content = null, Dictionary<string, string> headers = null, Dictionary<string, string> parameters = null)
    {
        using (HttpRequestMessage request = new HttpRequestMessage
        {
            Method = httpMethod,
            RequestUri = GetRequestUri(url),
            Content = content
        })
        {
            UriBuilder uriBuilder = new UriBuilder(GetRequestUri(url));

            if (parameters != null)
            {
                var query = HttpUtility.ParseQueryString(uriBuilder.Query);
                foreach (KeyValuePair<string, string> parameter in parameters)
                {
                    query[parameter.Key] = parameter.Value;
                }

                uriBuilder.Query = query.ToString();
            }

            request.RequestUri = uriBuilder.Uri;

            if (_authenticator != null)
            {
                IDictionary<string, string> authenticationHeaders = _authenticator.GetAuthenticationHeaders(request);

                if (authenticationHeaders != null)
                {
                    foreach (var header in authenticationHeaders)
                    {
                        request.Headers.Add(header.Key, header.Value);
                    }
                }
            }

            if (headers != null)
            {
                foreach (var header in headers)
                {
                    request.Headers.Add(header.Key, header.Value);
                }
            }

            return await _client.SendAsync(request);
        }
    }

    protected async Task<HttpResponseMessage> ProcessRequestAsync(HttpMethod httpMethod, string url, string contentJson, Dictionary<string, string> headers = null)
    {
        HttpContent httpContent = null;
        if (!string.IsNullOrEmpty(contentJson))
            httpContent = new StringContent(contentJson, Encoding.UTF8, "application/json");

        return await ProcessRequestAsync(httpMethod, url, httpContent, headers);
    }

    #endregion
}

public static class UriExtensions
{
    public static Uri Append(this Uri uri, params string[] paths)
    {
        return new Uri(paths.Aggregate(uri.AbsoluteUri, (current, path) => $"{current.TrimEnd('/')}/{path.TrimStart('/')}"));
    }
}

官方SDK实现代码

var graphServiceClient = GetGraphClient();

var sub = new Microsoft.Graph.Subscription();
sub.ChangeType = "updated";
sub.NotificationUrl = "https://76d2-2405-201-5c1d-405f-6845-eec-7804-46a1.ngrok-free.app/api/notifications";
sub.Resource = "/users";
sub.ExpirationDateTime = DateTime.UtcNow.AddMinutes(15);
sub.ClientState = "SecretClientState";

var newSubscription = await graphServiceClient
  .Subscriptions
  .Request()
  .AddAsync(sub);

Subscriptions[newSubscription.Id] = newSubscription;

问题原因

  1. JSON序列化库差异:
    • 官方Graph SDK使用Newtonsoft.Json(Json.NET),而自定义代码用的是System.Text.Json。
    • Microsoft.Graph.Subscription类通过JsonProperty特性指定了驼峰命名的JSON字段,但System.Text.Json默认不识别这些特性,导致序列化后的属性名是PascalCase(如ChangeType),不符合Graph API要求的驼峰格式(changeType)。
  2. 日期格式处理差异:
    • System.Text.Json对DateTime的序列化格式和Graph API要求的ISO 8601 DateTimeOffset格式不匹配,而官方SDK会自动将日期转换为符合要求的格式。

解决办法

方法一:改用Newtonsoft.Json序列化

安装Newtonsoft.Json NuGet包后,替换序列化代码:

string jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(sub);

该方式会自动遵循Subscription类的JsonProperty特性,生成符合要求的驼峰JSON,同时正确处理日期格式。

方法二:配置System.Text.Json适配要求

如果坚持使用System.Text.Json,需配置序列化选项并修正日期类型:

// 配置序列化选项
var options = new System.Text.Json.JsonSerializerOptions
{
    PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
    WriteIndented = false,
    Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() }
};
// 序列化
string jsonString = System.Text.Json.JsonSerializer.Serialize(sub, options);

// 同时将日期赋值改为DateTimeOffset类型
sub.ExpirationDateTime = DateTimeOffset.UtcNow.AddMinutes(15);

额外检查

确认请求的Content-Type为application/json(你的代码已设置),若仍有问题可补充官方SDK自动添加的SdkVersion头,但这不是导致BadRequest的直接原因。


内容的提问来源于stack exchange,提问作者Sonal Khatri

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 10:19:55