自定义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;
问题原因
- JSON序列化库差异:
- 官方Graph SDK使用Newtonsoft.Json(Json.NET),而自定义代码用的是
System.Text.Json。 Microsoft.Graph.Subscription类通过JsonProperty特性指定了驼峰命名的JSON字段,但System.Text.Json默认不识别这些特性,导致序列化后的属性名是PascalCase(如ChangeType),不符合Graph API要求的驼峰格式(changeType)。
- 官方Graph SDK使用Newtonsoft.Json(Json.NET),而自定义代码用的是
- 日期格式处理差异:
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
相关产品推荐
相关产品推荐

