Uninitialised JsonSerializer in Breeze SaveBundleT

2019-02-20 13:23发布

问题:

I'm attempting to use the SaveBundleToSaveMap snippet linked below to implement custom save handling on the server side of a breeze web api implementation.

SaveBundleToSaveMap

This sample does not work as is? (see below); their is a null reference exception which could use some attention.

The SaveWorkState(provider, entitiesArray) constructor calls the ContextProvider.CreateEntityInfoFromJson(...) method which then calls (the class scoped) JsonSerializer.Deserialize(new JTokenReader(jo), entityType) method.

The issue is that JsonSerializer is uninitialised and we get a null reference exeption. For e.g. I added this test hack to get the code running:

protected internal EntityInfo CreateEntityInfoFromJson(dynamic jo, Type entityType) {
      //temp fix to init JsonSerializer if SaveChanges has NOT been called
      if(JsonSerializer==null) JsonSerializer = CreateJsonSerializer();

      var entityInfo = CreateEntityInfo();

      entityInfo.Entity = JsonSerializer.Deserialize(new JTokenReader(jo), entityType);
      entityInfo.EntityState = (EntityState)Enum.Parse(typeof(EntityState), (String)jo.entityAspect.entityState);
      entityInfo.ContextProvider = this;

This issue does not occur in the standard release bits as CreateEntityInfoFromJson is always? called downstream from a SaveChanges() call which means the JsonSerializer gets initialised.

However, things would be better structured if an initialised JsonSerializer was passed to CreateEntityInfoFromJson as a parameter to avoid potential future null reference issues?

Alternately, is there a way to get the SaveBundleToSaveMap snippet to init the JsonSerializer? Its got a private setter :(

UPDATE

Implemented a very hacky stopgap solution. If anyone at IdeaBlade is watching, would be great to have a public API to convert to and from json saveBundle <-> saveMap.

/// <summary>
/// Convert a json saveBundle into a breeze SaveMap
/// </summary>`enter code here`
public static Dictionary<Type, List<EntityInfo>> SaveBundleToSaveMap(JObject saveBundle)
{
   var _dynSaveBundle = (dynamic)saveBundle;
   var _entitiesArray = (JArray)_dynSaveBundle.entities;
   var _provider = new BreezeAdapter();

   //Hack 1: Breeze.ContextProvider initializes a global JsonSerializer in its SaveChanges() method
   //We are bypassing SaveChanges() and bootstrapping directly into SaveWorkState logic to generate our saveMap
   //as such we need to init a serializer here and slipsteam it in via reflection (its got a private setter)
   var _serializerSettings = BreezeConfig.Instance.GetJsonSerializerSettings();
   var _bootstrappedJsonSerializer = JsonSerializer.Create(_serializerSettings);

   //Hack 2:
   //How to write to a private setter via reflection
   //http://stackoverflow.com/questions/3529270/how-can-a-private-member-accessable-in-derived-class-in-c
   PropertyInfo _jsonSerializerProperty = _provider.GetType().GetProperty("JsonSerializer", BindingFlags.Instance | BindingFlags.NonPublic);
   //Hack 3: JsonSerializer property is on Breeze.ContextProvider type; not our derived EFContextProvider type so...
   _jsonSerializerProperty =  _jsonSerializerProperty.DeclaringType.GetProperty("JsonSerializer", BindingFlags.Instance | BindingFlags.NonPublic);
   //Finally, we can init the JsonSerializer
   _jsonSerializerProperty.SetValue(_provider, _bootstrappedJsonSerializer);

   //saveWorkState constructor loads json entitiesArray into saveWorkState.EntityInfoGroups struct
   var _saveWorkState = new SaveWorkState(_provider, _entitiesArray);
   //BeforeSave logic loads saveWorkState.EntityInfoGroups metadata into saveWorkState.SaveMap 
   _saveWorkState.BeforeSave();
   var _saveMap = _saveWorkState.SaveMap; 

   return _saveMap;
}

回答1:

I looked into this. You don't actually need to make a change to the Breeze code to accomplish what you want. The ContextProvider is designed such that you can do just about whatever you want during save.

I'm curious: what "custom save handling" do you want to perform that you can't do today with the BeforeSave and AfterSave logic? I see in your "stopgap" code that you're calling BeforeSave on the SaveWorkState. What more do you need?

As an exercise, I wrote a NorthwindIBDoNotSaveContext that does what you want. Here's how it goes:

/// <summary>
/// A context whose SaveChanges method does not save
/// but it will prepare its <see cref="SaveWorkState"/> (with SaveMap)
/// so developers can do what they please with the same information.
/// See the <see cref="GetSaveMapFromSaveBundle"/> method;
/// </summary>
public class NorthwindIBDoNotSaveContext : EFContextProvider<NorthwindIBContext_CF>
{
  /// <summary>
  /// Open whatever is the "connection" to the "database" where you store entity data.
  /// This implementation does nothing.
  /// </summary>
  protected override void OpenDbConnection(){}

  /// <summary>
  /// Perform your custom save to wherever you store entity data.
  /// This implementation does nothing.
  /// </summary>
  protected override void SaveChangesCore(SaveWorkState saveWorkState) {}

  /// <summary>
  /// Return the SaveMap that Breeze prepares
  /// while performing <see cref="ContextProvider.SaveChanges"/>.
  /// </summary>
  /// <remarks>
  /// Calls SaveChanges which internally creates a <see cref="SaveWorkState"/>
  /// from the <see param="saveBundle"/> and then runs the BeforeSave and AfterSave logic (if any).
  /// <para>
  /// While this works, it is hacky if all you want is the SaveMap.
  /// The real purpose of this context is to demonstrate how to
  /// pare down a ContextProvider, benefit from the breeze save pre/post processing,
  /// and then do your own save inside the <see cref="SaveChangesCore"/>.
  /// </para>
  /// </remarks>
  /// <returns>
  /// Returns the <see cref="SaveWorkState.SaveMap"/>.
  /// </returns>
  public Dictionary<Type, List<EntityInfo>> GetSaveMapFromSaveBundle(JObject saveBundle)
  {
    SaveChanges(saveBundle); // creates the SaveWorkState and SaveMap as a side-effect
    return SaveWorkState.SaveMap;
  }
}

And here's how you could use it to get the SaveMap:

var saveMap = new NorthwindIBDoNotSaveContext().GetSaveMapFromSaveBundle(saveBundle);

Yes, it is "hacky", particularly if all you want is the SaveMap. But why do you just want the SaveMap?

We've designed the ContextProvider (and all of its sub-classes) such that you have free reign over the SaveChangesCore method. You could override that, further manipulate the SaveMap, then either delegate to the base implementation or do whatever else you have in mind for saving the entity data.

But while I don't see what you're after, it was not all that hard to extract the SaveChanges initialization logic into its own method.

So in the next release (after 1.5.2), you should find the following new method in the ContextProvider:

protected void InitializeSaveState(JObject saveBundle)
{
  JsonSerializer = CreateJsonSerializer();

  var dynSaveBundle = (dynamic)saveBundle;
  var entitiesArray = (JArray)dynSaveBundle.entities;
  var dynSaveOptions = dynSaveBundle.saveOptions;
  SaveOptions = (SaveOptions)JsonSerializer.Deserialize(new JTokenReader(dynSaveOptions), typeof(SaveOptions));
  SaveWorkState = new SaveWorkState(this, entitiesArray);
}

SaveChanges now calls that method before continuing on in its previous manner:

public SaveResult SaveChanges(JObject saveBundle, TransactionSettings transactionSettings = null) {

  if (SaveWorkState == null || SaveWorkState.WasUsed) {
    InitializeSaveState(saveBundle);
  }

  transactionSettings = transactionSettings ?? BreezeConfig.Instance.GetTransactionSettings();
  ...
}

Notice that SaveChanges won't call InitializeSaveState twice if you've already prepared the SaveWorkState by, say, calling InitializeSaveState externally and then called SaveChanges immediately thereafter. It also won't save twice with a "used" SaveWorkState.

The source is checked into github right now if you're interested.

You'll be able to get the SaveMap from a save bundle by adding this method to your sub-class of a ContextProvider as in this example:

public class NorthwindContextProvider: EFContextProvider<NorthwindIBContext_CF>  {
  ...
  public Dictionary<Type, List<EntityInfo>> GetSaveMapFromSaveBundle(JObject saveBundle) {
    InitializeSaveState(saveBundle); // Sets initial EntityInfos
    SaveWorkState.BeforeSave();      // Creates the SaveMap as byproduct of BeforeSave logic
    return SaveWorkState.SaveMap;
  }
  ...
}

Now you use that as follows:

  var saveMap =  ContextProvider.GetSaveMapFromSaveBundle(saveBundle);