Jadenkun
Jadenkun

Reputation: 327

Error stringifying an object when calling Custom Action in OData D365 Online

I have created a custom action that takes in an input parameter called paramsInput (string).

I want to send to said action a stringified JSON object so that it may be deserialized inside the action.

Snippet of action code:

public void Execute(IServiceProvider serviceProvider)
{
    try
    {
        _context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
        IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
        _crmService = factory.CreateOrganizationService(_context.UserId);
    
        string paramsInput = _context.InputParameters.Contains("paramsInput") ? _context.InputParameters["paramsInput"].ToString() : string.Empty;
    
        if (paramsInput != string.Empty)
        {
            // Deserialize to concrete class
            InputParams inputParameters = JsonConvert.DeserializeObject<InputParams>(paramsInput);
    
            // logic here...       
        }
    }
    catch (Exception ex)
    {
        // handle exception...
    }
}

I have also created a generic function that receives the action's name and the parameters to send and calls the action via OData:

public bool CallMessage(string action, JObject parameters, out string reason)
{
    RegenerateAccess(); // Microsoft Online
    string urlAPI = "/api/data/v9.1/" + action;
    using (HttpClient client = new HttpClient())
    {
        client.BaseAddress = new Uri(_serviceUrl);
        client.Timeout = new TimeSpan(0, 2, 0);  //2 minutes  
        client.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
        client.DefaultRequestHeaders.Add("OData-Version", "4.0");
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    
        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, urlAPI);
    
        /*StringBuilder sb = new StringBuilder();
        using (StringWriter sw = new StringWriter(sb))
        using (JsonTextWriter writer = new JsonTextWriter(sw))
        {
            writer.QuoteChar = '\'';
    
            JsonSerializer ser = new JsonSerializer();
            ser.Serialize(writer, parameters);
        }*/
    
    
        var jsonObj = JsonConvert.SerializeObject(parameters);
        //request.Content = new StringContent(sb.ToString(), Encoding.UTF8, "application/json");
        request.Content = new StringContent(jsonObj, Encoding.UTF8, "application/json");
        request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
    
        //Set the access token
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _authResult.AccessToken);
        HttpResponseMessage response = client.SendAsync(request).Result;
        reason = response.Content.ReadAsStringAsync().Result;
                
        return response.IsSuccessStatusCode;
    }
}

My issue is that I'm getting a Bad Request 400 back, and the response reads - Microsoft.OData.ODataException: An unexpected 'StartObject' node was found for property named 'paramsInput' when reading from the JSON reader. A 'PrimitiveValue' node was expected.

My parameters JObject is structured as follows:

{
    "paramsInput" : {
            "id" : "xxxx-xxxx...", // some guid
            "code" : 1 // int
    }
}

or via code:

Guid guid = Guid.NewGuid();
JObject parameters = new JObject();
parameters.Add("id", guid);
parameters.Add("code", 1);
JObject wrapper = new JObject();
wrapper.Add("paramsInput", parameters);
    
// send wrapper JObject to CallMessage

I don't want to change the structure of CallMessage function as it is a generic function that may be used multiple times by other sources.

Upvotes: 0

Views: 223

Answers (2)

MattB
MattB

Reputation: 186

Henk's answer is correct, You told the API to accept a string, thus you must send it the serialized version of your JSON in the string parameter.

I cannot add a comment to Henk's answer due to length restrictions, but I wanted to add a point of clarification here:

The Action is a bit of legacy nowadays and has been replaced by the Custom API. A Custom API pretty much works in the same way and as a bonus you do not need to create custom plugin steps.

You don't need to create a plugin supporting it if you're going to process it somewhere else ( Like PowerAutomate). This is technically called an "Event" in the Power Platform. You also don't need to create a plugin to support a custom action.

The fundamental difference is that "Custom Action" was implemented as a Workflow Operation, where "CustomAPI" is not. You gain a bit of perf as the system does not need to invoke the workflow runtime to run your operation.

Originally, 'Custom Action' was intended to be used as a custom workflow implementation (Think the way you use an HTTP trigger for PowerAutomate today.) However, folks quickly realized that you could wire up a Plugin to the SDK message to get it to act like a 'custom api' inside Dataverse.

Carrying that forward, the CustomAPI Definition is used for both Events and Custom Operations.

As Henk Said, 'Custom Action' is the older process for creating a callable interface for this.

Upvotes: 0

Henk van Boeijen
Henk van Boeijen

Reputation: 7918

The action's parameter paramsInput (containing the JSON) is a string parameter. Therefore, when this parameter is constructed, it must be stringified and your last code snippet would need to look like this:

Guid guid = Guid.NewGuid();
JObject parameters = new JObject();
parameters.Add("id", guid);
parameters.Add("code", 1);
JObject wrapper = new JObject();
wrapper.Add("paramsInput", JsonConvert.SerializeObject(parameters));
    
// send wrapper JObject to CallMessage

A few side notes:

  1. you decided to create your code executing Dataverse actions from scratch using HttpClient, which in itself is fine, but doing so you need to do all plumbing yourself. Another option would be to use the OrganizationRequest from the MS SDK libraries.
  2. The Action is a bit of legacy nowadays and has been replaced by the Custom API. A Custom API pretty much works in the same way and as a bonus you do not need to create custom plugin steps.

Upvotes: 1

Related Questions