Using DTOs With Breeze.js

7 November 2014

Currently doing a lot of work with an existing system implemented with Breeze.js in the frontend, talking to ASP.NET MVC endpoints / EntityFramework in the back.

Breeze is pretty powerful - it's a mini database sitting on the client side that just magically handles all the boring CRUD stuff for you. It's great when you're starting out and you can get off the ground super fast, but for some slightly more complex scenarios, you need to put a bit of work in.

Everything works flawlessly if your entities are identical between client and server, but what if you wanted to add in some server generated properties (that shouldn't be persisted to the database), or present a subset of all available properties as a different entity?

With Breeze pointing directly to the metadata from your EF provider, you'll quickly notice that:

  • Breeze will strip properties you try to pass back through that don't match your EF entity
  • You could create an empty table in your database and never actually write to it, but that's confusing and feels rightfully hacky
  • Extra properties passed back by via Breeze will fail at ContextProvider.SaveChanges()

So we really just need to be able to pass and handle Data Transfer Objects (DTOs) between our MVC endpoints and breeze. How I eventually got there:

  • created a custom dbContextProvider to provide DTO Metadata to Breeze
  • on the SaveChanges() endpoint on the backend, inspect the JSON save bundle and transform any DTOs back to their appropriate EF versions

For example, your DTO might look like:

namespace My.Model.DTO
{
    public class Booking                                    
    {
        public int ID { get; set; }
        [Required]
        public DateTime StartDate { get; set; }
        [Required]
        public DateTime EndDate { get; set; }

        //DTO Specific properties
        public string AvailableFrontEnd { get; set; }    

        public Booking(My.Model.Booking booking)
        {                     
            ID = booking.ID;
            StartDate = booking.StartDate;
            EndDate = booking.EndDate;

            AvailableFrontEnd = "Front End";                
        }
    }

}

And you can work with your DTO with a custom DBContext / Repository, to both convert outgoing entities to DTO and incoming back to your actual EF entities:

namespace My.DataAccess
{
    public class DtoContextProvider : EFContextProvider<DtoDbContext>
    {
        protected override string BuildJsonMetadata()
        {
            string _metadata = base.BuildJsonMetadata();
            //Converting meta data to JSON object
            JObject json = JObject.Parse(_metadata);
            var entityTypes = json["schema"]["entityType"].Children();
            //Looping through all entities
            foreach (var entityType in entityTypes)
            {
                var type = Type.GetType("My.Model." + entityType["name"].ToString() + ", My.Model");
                var properties = type.GetProperties();
                //Looping through all the properties
                foreach (var property in properties)
                {
                    //Checking to see if any property needs hiding from BreezeJS client
                    var attributes = property.GetCustomAttributes(typeof(ServerOnlyProperty), false);
                    if (attributes.Count() > 0)
                    {
                        //If a server only property is found then remove it from metadata
                        var propertyArray = entityType["property"] as Newtonsoft.Json.Linq.JArray;
                        var prop = propertyArray.Where(p => (string)p["name"] == property.Name).FirstOrDefault();
                        propertyArray.Remove(prop);
                    }
                }
            }
            _metadata = json.ToString();
            return _metadata;
        }

        protected override bool BeforeSaveEntity(EntityInfo entityInfo)
        {
            var entity = entityInfo.Entity;
            var properties = entity.GetType().GetProperties();
            foreach (var property in properties)
            {
                var attributes = property.GetCustomAttributes(typeof(ServerDefaultDate), false);
                if (attributes.Count() > 0)
                {
                    property.SetValue(entity, DateTime.Now);
                }
            }
            return base.BeforeSaveEntity(entityInfo);
        }
    }

    public class DtoRepository
    {            
        private readonly MyContextProvider _contextProvider = new MyContextProvider();                      //actual database context    
        private readonly DtoContextProvider _dtoContextProvider = new DtoContextProvider();             //client facing

        public MyDbContext Context { get { return _contextProvider.Context; } }

        public string Metadata
        {
            get
            {
                return _dtoContextProvider.Metadata();
            }
        }

        public List<DTO.Booking> Bookings(string userName)
        {
            User user = Context.Users.Where(u => u.Email.Equals(userName)).FirstOrDefault();
            List<DTO.Booking> bookingsList = new List<DTO.Booking>();

            var bookings = from b in Context.Bookings;

            foreach (Booking b in bookings)
            {                  
                bookingsList.Add(new DTO.Booking(b));
            }
            return bookingsList;
        }    

        public SaveResult SaveChanges(JObject saveBundle)
        {
            SaveResult saveResult = null;

            //mapping DTOs back to regular objects
            for(int i = 0; i < saveBundle["entities"].Count(); i++)
            {
                JToken entity = saveBundle["entities"][i];

                switch((string)entity["entityAspect"]["entityTypeName"])
                {
                    case "Booking:#My.Model.DTO":
                        ProcessBookingDTO(entity);
                        break;
                }
            }

            try
            {
                saveResult = _contextProvider.SaveChanges(saveBundle);
            }
            catch (System.Exception e)
            {
                //consider some logging here
                throw e;
            }

            return saveResult;
        }

        private void ProcessBookingDTO(JToken entity)
        {
            entity["entityAspect"]["entityTypeName"] = "Booking:#My.Model";
            entity["AvailableFrontEnd"].Parent.Remove();                  //the Parent is the JProperty (the indexer gives us the value)
        }        
    }
}

You can then provide to Breeze your DTO-specific Metadata endpoint:

    [HttpGet]
    [Route("breeze/dto/Metadata")]
    public string DtoMetadata()
    {
        return _dtoRepository.Metadata;   
    }

Because of the way we have the namespaces set up, you can see in the code above that transforming the DTO back to the proper EF Entity is pretty easy - it's just a matter of removing or modifying unused properties and renaming the entityTypeName in entityAspect.

With all that done, we no longer have a tight coupling of entities between the front and back end. It involves a bit more work and set-up, but it means more flexibility around how you present your data without having to give up on Breeze functionality in the front. Enjoy!

Tags: breeze.js, EF

Add a Comment

No Comments