creating/updating breeze entities with one-to-one relationship

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?

Answers:

Answer

You've uncovered a bug ... or so it seems to me. I have filed it and we'll let you know when it is fixed.

On a different note, your BreezeJS client code is a little more tortured than it needs to be. Why create an uninitialized detached entity ... and then add it?

The line user.Person(user); doesn't seem right at all. That should fail. I think you meant user.Person(person);. But you won't need that either.

Try this:

var manager = new breeze.EntityManager('api/Db');
// other initialization stuff.

// adds new, initialized Person entity
var person = manager.createEntity('Person', {
        // perhaps init fields: firstName, lastName
    });

// adds new, initialized User entity, associated w/ parent Person
var user = manager.createEntity('User', {
       personId: person.Id(), // SETS THE KEY TO PARENT PERSON
       // perhaps init fields: emailAddress, password (really!?!)
});

manager.saveChanges().then(function() { ... etc.

Update 8 May 2013

Yes, that commit is the fix on the BreezeJS side. We'll release it soon ... although of course you are free to grab an intermediate commit. I wouldn't ... because intermediate, untagged commits haven't been properly QA'd.

I think you will still have a problem ... this time on the EntityFramework side. 1-1 is pretty trick in Code First; you may want to look at this StackOverflow thread.

To be specific to your case, I think you need to change your definition of User to this:

public class User {
    [Key]
    [ForeignKey("Person")]
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }

    //[Required]
    public virtual Person Person { get; set; }

    [Required]
    public string EmailAddress { get; set; }

    [Required]
    public string Password { get; set; }
}

You shouldn't need the "Fluent API" mapping for User that you show above and it might get in the way.

// DELETE THIS !!!
protected override void OnModelCreating(DbModelBuilder modelBuilder) {
     modelBuilder.Entity().HasRequired(x => x.Person);
}

FWIW, the Order/InternationalOrder relationship in DocCode is virtually the same as your Person/User. The test, "can save a new Northwind Order & InternationalOrder [1..(0,1) relationship]", in the saveNorthwindTests.js confirms the model definition and the Breeze fix.

Hope this helps.

Answer

Breeze v 1.3.3 is now available on the Breeze website and we did fix an issue with 1-1 mappings. Could you confirm whether this corrects your issue?

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.