DEV Community

Cover image for My second contribution to ASP.NET Core OData
Anas Ismail Khan
Anas Ismail Khan

Posted on • Originally published at anasismail.com

My second contribution to ASP.NET Core OData

A month ago, I wrote about my first contribution to an open-source, Microsoft-maintained, project, from June 2024, that got merged by November 2024. What I didn't mention at that time was that that wasn't my only contribution to that project. Shortly after that pull request got merged, I opened a second one. Once again, I created an issue, to report a bug, and a pull request, to solve the bug, back to back. However, this time I made them in the correct order.

The same project from the last article required me to enable ETags on the OData API that I was working on. An ETag is an HTTP header that's used for cache-invalidation and concurrency control. It's basically like a hash representing the state of the resource/data at said endpoint. The client may use it as a cache key and the server can use it for concurrency control when multiple requests attempt to update the resource at the same time. ETag values are based on a special field/column on the resource record that changes every time the record is updated.

Lets talk for a moment about how ETags help with concurrency control. There's a concept called Optimistic Concurrency Control. It's a non-locking mechanism that gets its name from the optimism of assuming that conflicts are very unlikely to occur. I like to think that it gets its name from the assumption that the user would be responsible enough to include the if-match header in the update request. This header, which holds the last known ETag value known by the client, is compared with the current value on the resource to detect conflicts.

Caching works in a similar manner. Client passes the ETag in a header called if-none-match in a GET request. If the value matches, the server sends back a 304 Not Modified status. Otherwise it sends back the whole record. This reduces bandwidth usage and speeds up the response time.

In C#, a popular convention, but by no means a rule, for these concurrency check fields is to use a byte[] field called RowVersion annotated with a [Timestamp] or [ConcurrencyCheck] attribute. The Microsoft.AspNetCore.OData package has a lot of abstraction built in to simplify the generation and validation of ETags. If your model has concurrency fields set up correctly, it automatically picks them up and handles conversions. It even goes one step further and adds a field called @odata.etag to the JSON response, rather than just putting it in the headers. And even more importantly, it gives you a these two prebuilt methods for computing and comparing ETags:

queryOptions.IfMatch.ApplyTo(IQueryable query)
Enter fullscreen mode Exit fullscreen mode

and

queryOptions.IfNoneMatch.ApplyTo(IQueryable query)
Enter fullscreen mode Exit fullscreen mode

The ApplyTo method, which exists on the ETag class, has a foreach loop that iterates over all the concurrency properties and compares them one by one with the values on the actual record. As you might have already guessed, for each field, it was performing a simple equality comparison. Well and good, until you introduce array fields like 90% of the users like to. Meaning, the IfMatch and IfNoneMatch headers were definitely not having the intended effect for what was undoubtedly the most popular use-case.

Initially, I thought it was just me and that I had configured something incorrectly. To make sure, I decided to take a look at the existing E2E tests for the ETag class. Surprisingly, not a single field in the model for the test was of the type byte[]. I proceeded to add one and sure enough, the test failed immediately. I went back to the ETag.ApplyTo method and added a check inside the loop for array types. I added some code to handle the array condition and guess what? The tests passed.

I opened an issue and a pull request immediately. I added detailed instructions for reproducing the bug using the existing E2E ETag Test code. This happened at the very end of October. Since the team was busy creating releases for the already merged bugs, it took them as late as January to actually respond. They asked for more tests and I explained how I brilliantly modified an existing one. I thought they'd have a problem with me modifying an existing test since I myself didn't feel all that great about it but all I had really done was add a new field to the test and make sure that all the existing test cases were ignoring that field except for the one test case that actually needed it.

Luckily, a few days from that, they approved my changes and then another few days after that, they merged it. They are yet to make a release that includes this fix; because including this fix, there's hardly been any changes to that branch however, I'm hoping it's just around the corner because we are forced to use a fork in our project, instead of the NuGet package, solely because of this bug.


Got a problem that needs solving?

➡️ Debugging is my superpower. Let me know if you have a bug that needs to be squashed at: contact@anasismail.com

This post first appeared on the my blog at https://anasismail.com/oops-i-did-it-again

Top comments (0)