According to the official Microsoft documentation, one can assign custom security attribute values using the following code:
var requestBody = new User
{
CustomSecurityAttributes = new CustomSecurityAttributeValue
{
AdditionalData = new Dictionary<string, object>
{
{
"Engineering" , new
{
OdataType = "#Microsoft.DirectoryServices.CustomSecurityAttributeValue",
ProjectDate = "2022-10-01",
}
},
},
},
};
var result = await graphClient.Users["{user-id}"].PatchAsync(requestBody);
This is apparently the C# equivalent of the following HTTP PATCH request:
PATCH https://graph.microsoft.com/v1.0/users/{id}
Content-type: application/json
{
"customSecurityAttributes":
{
"Engineering":
{
"@odata.type":"#Microsoft.DirectoryServices.CustomSecurityAttributeValue",
"ProjectDate":"2022-10-01"
}
}
}
Whilst I have successfully utilised the Graph Explorer and run an HTTP PATCH request for custom security attributes without incident, my C# code does not work. Instead, the client throws the following error:
Microsoft.Graph.Beta.Models.ODataErrors.ODataError: Invalid property 'SalesforceAccountId'.
at Microsoft.Kiota.Http.HttpClientLibrary.HttpClientRequestAdapter.ThrowIfFailedResponse(HttpResponseMessage response, Dictionary`2 errorMapping, Activity activityForAttributes, CancellationToken cancellationToken)
at Microsoft.Kiota.Http.HttpClientLibrary.HttpClientRequestAdapter.SendAsync[ModelType](RequestInformation requestInfo, ParsableFactory`1 factory, Dictionary`2 errorMapping, CancellationToken cancellationToken)
at Microsoft.Kiota.Http.HttpClientLibrary.HttpClientRequestAdapter.SendAsync[ModelType](RequestInformation requestInfo, ParsableFactory`1 factory, Dictionary`2 errorMapping, CancellationToken cancellationToken)
at Microsoft.Graph.Beta.Users.Item.UserItemRequestBuilder.PatchAsync(User body, Action`1 requestConfiguration, CancellationToken cancellationToken)
at PwsPortal.App.Services.MsGraph.GraphService.AssignAccountIdToUserAsync(String userId, String accountId) in C:\Users\George\source\repos\PwsPortal2\PwsPortal\App\Services\MsGraph\GraphService.cs:line 90
at PwsPortal.Controllers.SalesforceController.AddSecurityAttributeToUserAsync() in C:\Users\George\source\repos\PwsPortal2\PwsPortal\Controllers\SalesforceController.cs:line 105
at lambda_method77(Closure, Object)
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfActionResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.ResponseCompression.ResponseCompressionMiddleware.InvokeCore(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
*** Headers omitted ***
However, as you can see, my code is near-enough identical to the Microsoft example - the method is the same, only the set, name, and value, are different:
var requestBody = new User
{
CustomSecurityAttributes = new CustomSecurityAttributeValue
{
AdditionalData = new Dictionary<string, object>
{
{
"SalesforceAccountId" , new
{
ODataType = "#Microsoft.DirectoryServices.CustomSecurityAttributeValue",
Id = accountId,
}
},
},
},
};
await _graphServiceClient.Users[userId].PatchAsync(requestBody);
My question is; what is going on and how can it be solved? I need to programmatically add custom security attribute values and apparently the graph service client doesn't behave like it is supposed to in this instance. What am I doing wrong?
This is a confirmed issue with the SDK snippet generator and the way it serialises/deserialises anonymous types.
There is a solution on GitHub as well, but it is messy, unintuitive and probably isn't very robust:
var requestBody = new User
{
CustomSecurityAttributes = new CustomSecurityAttributeValue
{
AdditionalData = new Dictionary<string, object>
{
{
"Engineering" , new CustomSecurityAttributeValue() // Specify the object rather than using an anonymous type
{
OdataType = "#Microsoft.DirectoryServices.CustomSecurityAttributeValue",
AdditionalData = new Dictionary<string, object> { // Put the custom attribute key and value into the AdditionalData property
{ "projectDate", "2022-10-01" }
},
}
},
},
},
};
Hope this helps if anyone searches for the same problem. This was current as of this date and Graph SDK 5.52.0.