Chapter one: the night of disappearance
The cold wind was howling in the streets when Holmes knocked on Elvis’ door. “It just doesn’t make sense”, was the first thing Elvis said, “it’s simply… gone”…
Aight, pause please. I’m enjoying high 80s (Fahrenheit) at the moment here in the Caribbean, there was no door (just skype) and Elvis is not the actual name of the developer I was helping. I’ll try fiction again in a couple of years, let’s just get through the story of this missing LightSwitch record I encountered last week because I’m pretty sure one day, someone is going to stumble upon a similar issue and this blog post will save him/her half an hour of being dumbfounded…
Chapter one: (for real this time): a record went missing
So here’s what really happened: Elvis called me up because the LightSwitch desktop application that he was working on was throwing an unexplainable nullpointer exception. Unexplainable, because his flow was really basic, textbook easy even:
a) create a record from a “create new entity” screen (the entity was called a “case”)
b) when the screen has successfully saved the new record, use code to close the “create new entity” screen and open the default detail screen for that case enity
c) a nullpointer exception happens in the _initializeDataWorkspace method on this.Case
I’d never seen this before. Right after successfully saving, we show a detail screen on the same entity and the entity just disappears. I’m pretty sure at that point I spent at least 10 minutes of mumbling “I’m dumbfounded“. We started debugging and piece by piece we were able to reassemble the complete puzzle…
See, each screen in a LightSwitch desktop application has its own DataWorkspace. This is LightSwitch’ implementation of the unit-of-work pattern, whatever changes happen in one screen get committed as a whole upon saving, and either all changes are persisted to the data source or, in the case of an exception, none.
Since each screen has it’s own DataWorkspace, a single entity can exist multiple times (multiple instances) in different DataWorkspaces. This also means that, unlike the LS HTML client, when you type code like
this.Application.ShowDefaultScreen(this.Case);
the framework will actually take this.Case, an entity which belongs to the DataWorkspace of the “Create new entity” screen, and pass only the ID field(s) to the new “Detail” screen. The new screen will then actually use the ID(s) to retrieve the entity again, this time in its own DataWorkspace.
Chapter 2: the resource segment cannot be found
Sure enough, Fiddler soon revealed that when the second screen tried to retrieve the entity again, something went wrong.
Really wrong.
The exception returned was something along the lines of “the resource segment cannot be found for ‘http://localhost:xxxx/ApplicationData.svc/Case'”
Another ten minutes of mumbling about how dumbfounded I was, went by, before we got an Epiphany…
LightSwitch uses the OData protocol to interact with the server tier. OData uses http verbs (GET, POST, etc) for the ‘action’ (***), and a URI to state which entity you want to do your action on. URI is the keyword here: unique resource identifier. Or translated into normal language: your call to “blah blah blah.ApplicationData.svc/case/123” means you want me to retrieve the ‘Case’ entities and return the slice identified by “123”.
Get all resources for “Case” and only return slice (fragment) 123.
So basically this was the “OData way” of saying: case 123 can’t be found.
Thanks for being straightforward and all…
But… we just created it. And according to our select * from Case, the record is in the database, then why could LightSwitch not find it?
Chapter 3: if it looks like a duck
The mystery continued with 10 more minutes of mumbling about dumbfoundedness… But then we remembered that there’s actually a filter active.
LightSwitch supports row-level filtering, ideal for slicing your records in multi-tenant or advanced security/permission situations.
And sure enough, this was the case
if(!Application.Current.User.HasPermission(Permissions.FamilyLaw) query = query.Where( case => case.AssignedCategory.Name != "Family");
The user we were testing with did not have the FamilyLaw permission any more, effectively causing all cases to be filtered to only show the ones where the assigned category is not “Family”.
Right?
Right?
Turns out, our problem was even a little more subtle…
This LINQ query filters out all the cases that have an AssignedCategory that is not “Family”.
Read that again… The linq query is actually, unintentionally I assure you, adding two filter criteria behind the scenes:
case.AssignedCategory != "Family" seems to be interpreted as... case.AssignedCategory != null && case.AssignedCategory.Name != "Family"
Really, LINQ? Really?
Once we rewrote the LINQ query (to allow cases with AssignedCategory == null), our freshly created case entity (which has not yet been assigned to any category) now found its way down to the details screen.
*** PS: LightSwitch doesn’t really use HTTP verbs like (PUT, PATCH, …) directly on the OData service. Because each screen has it’s own “unit of work”, all changes get posted just once, together, to an URI called $batch. This HTTP “batch” call actually contains all different changes that happened in a screen (delete this, modify that, etc) in a single HTTP call. Using transactional techniques, all changes then all get persisted in the data source together or, in the case something goes wrong, none of them get persisted.