The Power Apps Solution Checker provides developers with a great mechanism to verify the quality of custom code targeting either Dynamics 365 Online or the Common Data Service. We can use the recommendations it produces to help write better, more optimised code, that will better benefit our deployments and the individuals working with the apps we’ve built. Typically, the recommendations it produces will be informative enough to get you on your way; Microsoft will provide a summary of the problem and a link to the relevant Docs article that contains links/code samples for you to adapt accordingly. However, this is not always the case, and you may need to scratch your head a little harder to get to the root of the problem. This happened to me recently when I was dealing with the following recommendation:
The Docs article link for this recommendation doesn’t give us much to go on when figuring out what to do to get our code fixed. But we must still, somehow, address the problem it raises - the idea of potentially conflicting transactions and generic SQL errors is enough, I’d warrant, to scare even the most hardened Dynamics 365 / Power Platform developer. 🙂 So let’s take a look at how I resolved the issue within this specific scenario. Hopefully, it might help you if you find yourself facing the same problem.
As this is a code related issues, let’s, first of all, take a look at the offending blocks of code in question. First of all, we had the following method that would call an external OData endpoint. This formed part of a Custom Data Provider for a virtual entity that would require OAuth 2.0 authentication; something that, regrettably, is not supported by the default OData provider Microsoft provides us with:
private async Task<T> GetRecordById<T>(ITracingService tracer, string id, string entity, string token)
{
//Generate the OData endpoint request
string oDataURL = $"{this.aadConfig.ODataURL}/{entity}({id})";
tracer.Trace(oDataURL);
HttpRequestMessage oDataReq = new HttpRequestMessage(HttpMethod.Get, new Uri(oDataURL));
oDataReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
using (HttpClient client = HttpHelper.GetHttpClient())
{
try
{
HttpResponseMessage oDataResponse = await client.SendAsync(oDataReq);
if (!oDataResponse.IsSuccessStatusCode)
{
tracer.Trace(oDataResponse.StatusCode.ToString());
throw new GenericDataAccessException($"A problem occurred when retrieving the {entity} data.");
}
//Retrieve data from the endpoint using the bearer token.
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(oDataResponse.Content.ReadAsStringAsync().Result)))
{
DataContractJsonSerializerSettings settings = new DataContractJsonSerializerSettings()
{
UseSimpleDictionaryFormat = true
};
DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(T), settings);
return (T)ser.ReadObject(stream);
}
}
catch (Exception e)
{
tracer.Trace(e.Message);
throw new GenericDataAccessException($"A problem occurred when retrieving the {entity} data.");
}
}
}
Note the following with this method:
- It has been set to execute asynchronously as a task, as indicated by the async and Task keyword.
- The core HTTP call is wrapped within a using statement and executed as a HttpClient request.
- The method is generic in terms of its return type, therefore allowing us to retrieve and then cast out to a variety of different classes if required.
This method was then called at relevant points elsewhere in the code using the snippet below:
var getEntityByIdTask = Task.Run(async () => await GetRecordById<Entity>(tracer, target.Id.ToString(), "Entity", getAccessToken.Result.access_token));
Task.WaitAll(getEntityByIdTask);
var result = getEntityByIdTask.Result;
tracer.Trace($"Entity found: {result.ID}");
Here, and as mentioned earlier, we are calling the asynchronous task method to return us a result set of type Entity, which represents our result from the OData endpoint.
Going back then to the recommendation raised earlier, we can best summarise the main issues with the “as-is” code as follows:
- The method will always run as a separate, asynchronous call within the current database transaction. As well as being the cause of a solution checker recommendation, this could also lead to longer-running transactions and us unintentionally hitting the two-minute execution limit for sandbox, if our outbound call takes far longer to execute than expected.
- The HttpClient class supports asynchronous execution only; there is no way of forcing it to execute synchronously, meaning that we must instead resort to using other class types, such as HttpWebRequest and HttpWebResponse
- The code snippet itself is expecting the method to run asynchronously; this needs to be adjusted accordingly to match any changes to the GetRecordById method.
With all this in mind, we can look to tweak our method…
private T GetRecordById<T>(ITracingService tracer, string id, string entity, string token)
{
//Generate the OData endpoint request
try
{
string oDataURL = $"{this.aadConfig.ODataURL}/{entity}({id})";
tracer.Trace(oDataURL);
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(new Uri(oDataURL));
req.Method = "GET";
req.KeepAlive = false;
req.Headers.Add("Authorization", "Bearer " + token);
var res = (HttpWebResponse)req.GetResponse();
if (res.StatusCode != HttpStatusCode.OK)
{
tracer.Trace(res.StatusCode.ToString());
throw new GenericDataAccessException($"A problem occurred when retrieving the {entity} data.");
}
using (var stream = new StreamReader(res.GetResponseStream(), Encoding.UTF8))
{
DataContractJsonSerializerSettings settings = new DataContractJsonSerializerSettings
{
UseSimpleDictionaryFormat = true
};
DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(T), settings);
return (T)ser.ReadObject(stream.BaseStream);
}
}
catch (Exception e)
{
tracer.Trace(e.Message);
throw new GenericDataAccessException($"A problem occurred when retrieving the {entity} data.");
}
}
…and the related snippet that calls it…
var entity = GetRecordById<Entity>(tracer, target.Id.ToString(), "Entity", getAccessToken.Result.access_token);
tracer.Trace($"Entity found: {entity.ID}");
Now, it is worth highlighting that Microsoft advises not to use the HttpWebRequest class for new development work. However, I have struggled to find an alternative solution that fixes this problem. Answers on a postcode if you think there’s a better way of doing this, and I will happily update this post and provide full credit for any solution that gets around this; but otherwise, looks like we are stuck with using this!
The Solution Checker is a fantastic tool to have in your arsenal to provide you with an automated, bespoke way of flagging common issues with your Dynamics 365 / Power Apps solution. Granted, it will never take the place of a formal code review alongside a senior developer. Still, it can be a potential lifesaver if you find yourself performing solo development work targeting the application. Give it a go if you haven’t already and, hopefully, if you see yourself getting the same recommendation outlined in this post, you now know what to do to get your code sorted.