Fire-and-Forget Key Values Made Easy
In a previous posting I described in some detail an all-too-complicated and restrictive mechanism for handling server generated key values which was, at the time, the best plan we had come up with. Since then, happily, we have been able to dramatically simplify things. The quick summary is:
You no longer need to specify server generated key values or client auto-generation in the CSDL file. Configure the server to generate values automatically and the framework automatically takes care of the rest.
Even better, the new mechanism works for all data types instead of just the restricted list of types supported by the old system.
Here's what we changed to get the new system to work:
Previously the EntityAdapter, which takes changes recorded by the ObjectStateManager and computes SQL update statements, required each entity to have unique values in the properties that make up the keys (not just unique EntityKey objects). So object services had to be aware of those entities where this was required and generate values using an algorithm that could guarantee they would be unique. Now, the EntityAdapter is able to use only the EntityKeys for uniqueness without requiring uniqueness from the properties that make up the keys.
Once we made that change we were able to switch temporary EntityKeys themselves to a much simpler mechanism—they now just use reference equality rather than determining equality by comparing key values (the way that permanent keys do). This means that temporary keys are guaranteed to be unique without generating any property key values.
The next change was to make all entities in the added state have temporary keys (instead of just entities specially marked in the CSDL file) so that we could remove this information that never really belonged there in the first place.
Now the flow of a typical new entity with a server-generated key value looks something like this:
- Construct the entity instance. At this point the key properties all have default values (null or 0).
- Add the instance to your context. A temporary key is created and used to store the entity in the ObjectStateManager.
- Call SaveChanges on the context. The EntityAdapter computes a SQL insert statement (or a stored proc invocation if so-specified in the mapping) and executes it.
- EntityAdapter writes the server-generated value back to the ObjectStateEntry for the entity. This assumes, of course, that the insert succeeded and that the SSDL was properly configured to indicate that there is a server generated value. Fortunately, if the SSDL is generated from the database schema by a tool, this configuration is automatically done as part of that generation.
- The ObjectStateEntry pushes the server-generated value into the entity itself. The EntityAdapter is written to operate solely at the conceptual model layer and uses the DataRecord interface on the ObjectStateEntry to read and write values from the entity. The ObjectStateEntry guarantees that the entity object and the DataRecord interface always contain the same data (for current values the DataRecord, in fact, is virtualized over the entity rather than storing its own copy of the data).
- When AcceptChanges is called on the ObjectStateEntry, a permanent EntityKey is computed using the new, unique property key values. The ObjectStateManager then does an internal fixup replacing all instances of the temporary key with the new permanent key.
Presto! Like magic, configuring automatic key values for the entity framework is the exact same as configuring them on the server.
Unfortunately, there's no such thing as a free lunch. In this case there's one main downside to the new system: If your entity key values are not server generated and you add two entities with the same key values to the same ObjectStateManager, in the old system this would be detected the moment you tried to add the second entity and an exception would be thrown. In the new system, however, both added entities will be given temporary keys so the ObjectStateManager will not detect the conflict at that point. When you call SaveChanges on the context, one of the added entities will save successfully while the second one will fail due to unique value constraints on the server. Fortunately, the EntityAdapter performs the inserts within a transaction so this failure will cause the full set of changes to roll back and you will then have an opportunity to correct the problem, but the failure comes at a later time than would be ideal. After much debate within the product team our conclusion was that the simplicity and reliability for the most common cases (either server generated values or natural keys which are unique) outweighed the late-failure in the case where there are duplicate key values.
So, when can I get my hands on this new system, you ask? The February CTP.