How to use Breeze with Generic Unit of Work and Re

2019-05-10 04:11发布

问题:

Using this:

https://genericunitofworkandrepositories.codeplex.com/

and the following set of blog posts:

http://blog.longle.net/2013/05/11/genericizing-the-unit-of-work-pattern-repository-pattern-with-entity-framework-in-mvc/

We are trying to use those repositories with Breeze since it handles client side javascript and OData very well.

I was wondering how we could use these with Breeze to handle overriding the BeforeSaveEntity correctly.

We have quite a bit of business logic that needs to happen during the save (modifying properties like ModifiedBy, ModifiedTime, CreatedBy, etc) but when we change those they aren't updated by breeze, so we have to re query after the save (we've tried manually mapping the changes back but it requires us to duplicate all of the business logic).

Our second option was to check the type of each entity and then request the correct repository for it, handle the save internally, and then do a new get request on the client to get the updated information. This is chatty though so we were hoping there is a better way. What would the correct way of updating these objects while bypassing breeze's save without returning an error or having to reget the data afterward?

Any examples of Breeze with Business Logic during the save would be very helpful, especially if it happens in a service, repository or something else besides directly in the BeforeSaveEntity method.

回答1:

This is many questions rolled into one and each is a big topic. The best I can do is point you in some directions.

Before I get rolling, let me explain why you're not seeing the effects of setting "properties like ModifiedBy, ModifiedTime, CreatedBy, etc)". The EFContextProvider does not update every property of the modified entities but rather only those properties mentioned in the EntityInfo.OriginalValuesMap, a dictionary of the property names and original values of just the properties that have changed. If you want to save a property that is only set on the server, just add it to the original values map:

var map = EntityInfo.OriginalValuesMap;
map["ModifiedBy"]=null; // the original value does not matter
map["ModifiedTime"]=null;

Now Breeze knows to save these properties as well and their new values will be returned to the client.

Let's return to the bigger picture.

Breeze is first and foremost an client-side JavaScript library. You can do pretty much whatever you want on the server-side and make Breeze happy about it as long as your server speaks HTTP and JSON.

Writing a server that provides all the capabilities you need is not trivial no matter what technology you favor. The authors of Breeze offer some .NET components out of the box to make your job easier, especially when you choose the Web API, EF and SQL Server stacks.

Our .NET demos typically throw everything into one web application. That's not how we roll in practice. In real life we would never instantiate a Breeze EFContextProvider in our Web API controller. That controller (or multiple controllers) would delegate to an external class that is responsible for business logic and data access, perhaps a repository or unit-of-work (UoW) class.

Repository pattern with Breeze .NET components

We tend to create separate projects for the model (POCOs usually), data access (ORM) and web (Web API plus client assets) projects. You'll see this kind of separation in the DocCode Sample and also in John Papa's Code Camper sample, the companion to his PluralsSight course "Building Apps with Angular and Breeze".

Those samples also demonstrate an implementation of the repository pattern that blends the responsibilities of multiple repositories and UoW in one class. This makes sense for the small models in these samples. There is nothing to stop you from refactoring the repositories into separate classes.

We keep our repository class in the same project as the EF data access material as we see no particular value in creating yet another project for this small purpose. It's not difficult to refactor into a separate project if you're determined to do so.

Both the Breeze and Code Camper samples concentrate on Breeze client development. They are thin on server-side logic. That said, you will find valuable clues for applying custom business logic in the BeforeSaveEntities extension point in the "NorthwindRepository.cs" and `NorthwindEntitySaveGuard.cs" files in the DocCode sample. You'll see how to restrict saves to certain types and certain records of those types based on the user who is making the request.

The logic can be overwhelming if you try to channel all save changes requests through a single endpoint. You don't have to do that. You could have several save endpoints, each dedicated to a particular business operation that is limited to insert/updating/deleting entities of just a few types in a highly specific manner. You can be as granular as you please. See "Named Saves" in the "Saving Entities" topic.

Have it your way

Now there are a gazillion ways to implement repository and UoW patterns.

You could go the way set forth by the post you cited. In that case, you don't need the Breeze .NET components. It's pretty trivial to wire up your Web API query methods (IQueryable or not) to repository methods that return IQueryable (or just objects). The Web API doesn't have to know if you've got a Breeze EFContextProvider behind the scenes or something completely different.

Handling the Breeze client's SaveChanges request is a bit trickier. Maybe you can derive from ContextProvider or EFContextProvider; maybe not. Study the "ContextProvider.cs" documentation and the source code, especially the SaveChanges method, and you'll see what you need to do to keep Breeze client happy and interface with however you want to handle change-set saves with your UoW.

Assuming you change nothing on the client-side (that's an assumption, not a given ... you can change the save protocol if you want), your SaveChanges needs to do only two things:

  1. Interpret the "saveBundle" from the client.
  2. Return something structurally similar to the SaveResult

The saveBundle is a JSON package that you probably don't want to unpack yourself. Fortunately, you can derive a class from ContextProvider that you use simply to turn the saveBundle into a "SaveMap", a dictionary of EntityInfo objects that's pretty much what anyone would want to work with when analyzing a change-set for validation and save.

The following might do the trick:

using System;
using System.Collections.Generic;
using System.Data;
using Breeze.ContextProvider;
using Newtonsoft.Json.Linq;

public class SaveBundleToSaveMap : ContextProvider 
{
    // Never create a public instance
    private SaveBundleToSaveMap(){}

    /// <summary>
    /// Convert a saveBundle into a SaveMap
    /// </summary>
    public static Dictionary<Type, List<EntityInfo>> Convert(JObject saveBundle)
    {
        var dynSaveBundle = (dynamic) saveBundle;
        var entitiesArray = (JArray) dynSaveBundle.entities;
        var provider = new SaveBundleToSaveMap();
        var saveWorkState = new SaveWorkState(provider, entitiesArray);
        return saveWorkState.SaveMap;
    }

    // override abstract members but DO NOT USE ANY OF THEM

}

Then it's up to you how you make use of the "SaveMap" and dispatch to your business logic.

The SaveResult is a simple structure:

public class SaveResult {
    public List<Object> Entities; // each of the entity type you serialize to the client
    public List<KeyMapping> KeyMappings;
    public List<Object> Errors;
}

public class KeyMapping {
    public String EntityTypeName;
    public Object TempValue;
    public Object RealValue;
}

Use these classes as is or construct your own. Breeze client cares about the JSON, not these types.