I'm working on a project using Breeze and I came across a problem with some entities I had created using a one-to-one relationship. This is a bit of a long story, but it has a happy ending, so bear with me :)
Here is a cut down version my C# code:
public class Person {
[Key]
public int Id { get; set; }
public virtual User User { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class User {
[Key]
public int Id { get; set; }
[Required]
public virtual Person Person { get; set; }
[Required]
public string EmailAddress { get; set; }
[Required]
public string Password { get; set; }
}
public class MyDbContext : DbContext {
public DbSet<Person> Person { get; set; }
public DbSet<User> User { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder) {
modelBuilder.Entity<User>().HasRequired(x => x.Person);
}
}
This creates a Persons database table with an auto generated primary key, and a Users table with a manually entered primary key, which is a subset of the Persons table.
I create these entities, and attempt to save them in my javascript code with something like:
var manager = new breeze.EntityManager('api/Db');
// other breeze initialization stuff, metadata etc.
var person=manager.createEntity('Person', undefined, breeze.EntityState.Detached);
var user=manager.createEntity('User', undefined, breeze.EntityState.Detached);
// set other fields, name, email, password
user.Person(user);
manager.addEntity(user);
manager.saveChanges().then(function() { // etc
When the SaveChanges function is called in my BreezeController I get this exception:
Validation error: Property: 'Person', Error: 'The Person field is required.'
Upon examination of the JSON that is send to the BreezeController I find that the Id of both the Person and the User was '-1', and that the JSON did not nest the Person entity inside the Person property of the User. So there was no way for the EFContextProvider to know that there was supposed to be a relationship between these objects.
My first attempt to solve this was to send the 'Person' entity to the backend with saveChanges, then send the 'User' in a seperate transaction. Eg:
manager.addEntity(person);
manager.saveChanges().then(function() {
user.Person(user); // this line moved from previous example,
// breeze throws error here
manager.addEntity(user);
manager.saveChanges().then(function() { // etc
Using this approach I got this error from breeze.js:
Cannot attach an object to an EntityManager without first setting its key
or setting its entityType 'AutoGeneratedKeyType' property to something other
than 'None' at checkEntityKey (http://localhost:12151/scripts/breeze.debug.js)
So I tried to set the key on the User to be the value filled in when the Person was saved. Using "user.Id(person.Id);" Now in the BreezeController I still get the same error:
Validation error: Property: 'Person', Error: 'The Person field is required.'
Next I tried to modify the SaveChanges method of my BreezeController to check for an incoming 'User' object which doesn't have a 'Person' property set, then Find the person in the database, and assign it to the Person before calling _efContextProvider.SaveChanges(saveBundle); I lost the code for exactly how I did this, but it's irrelevant as it didn't work properly either... It created a SECOND 'Person' in the database, exactly the same as the one that was saved in the first manager.saveChanges, but it had a new primary key generated. This did however successfully associate the second Person with the User.
After thinking about it overnight I came up with a working solution by subclassing the EFContextProvider with this:
public class MyEfContextProvider : EFContextProvider<MyDbContext>
{
protected override Dictionary<Type, List<EntityInfo>> BeforeSaveEntities(
Dictionary<Type, List<EntityInfo>> saveMap)
{
AssociateOneToOne(saveMap, typeof(Person), typeof(User));
return saveMap;
}
private static void AssociateOneToOne(IReadOnlyDictionary<Type,
List<EntityInfo>> saveMap, Type parent, Type child)
{
if (!(saveMap.ContainsKey(parent) && saveMap.ContainsKey(child))) return;
Func<EntityInfo, object> idFunc = delegate(EntityInfo info)
{
var o = info.Entity;
return o.GetType().GetProperty("Id").GetValue(o);
};
var childProp = child.GetProperty(parent.Name);
var childMap = saveMap[child].ToDictionary(idFunc, info => info.Entity);
var parentMap = saveMap[parent].ToDictionary(idFunc, info => info.Entity);
foreach (var childEntry in childMap)
{
childProp.SetValue(childEntry.Value, parentMap[childEntry.Key]);
}
}
}
Now I realise this has been a long question, and thanks for reading. But the only question I have is, why do I have to do it this way? Is this something that Breeze doesn't have implemented yet? If so, is the way I've done it ok?