Question

I am trying to update a many to many relationship that I have setup in Entity Framework using Code First. I've created the following Models.

[Serializable]
public class ClientFormField : FormField
{
    public ClientFormField()
    {
        CanDisable = true;
    }

    public virtual ClientFormGroup Group { get; set; }
    public virtual ICollection<ClientValue> Values { get; set; }

    public virtual ICollection<LetterTemplate> LetterTemplates { get; set; }
}

[Serializable]
public class CaseFormField : FormField
{
    public CaseFormField()
    {
        CanDisable = true;
    }

    public virtual CaseFormGroup Group { get; set; }

    public virtual ICollection<LetterTemplate> LetterTemplates { get; set; }

    public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        foreach (var val in base.Validate(validationContext))
            yield return val;
    }
}

[Serializable]
public class SystemField : TrackableEntity
{
    public string Name { get; set; }
    public string Value { get; set; }
    public string VarName { get; set; }
    public virtual SystemFieldType SystemFieldType { get; set; }
    public int TypeId { get; set; }

    public ICollection<LetterTemplate> LetterTemplates { get; set; }

    public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (string.IsNullOrWhiteSpace(Name))
            yield return new ValidationResult("System field must have a name.", new[] { "SystemFieldName" });

        if (string.IsNullOrWhiteSpace(Value))
            yield return new ValidationResult("System field must have a value.", new[] { "SystemFieldValue" });

        var regex = new Regex(@"^[a-zA-Z0-9-_]+$");
        if (!string.IsNullOrWhiteSpace(VarName) && !regex.IsMatch(VarName))
            yield return
                new ValidationResult("Varname can only contain alphanumeric, underscore, or hyphen",
                                     new[] { "SystemFieldVarName" });

        if (TypeId <= 0)
            yield return new ValidationResult("System Field must have a type.", new[] { "SystemFieldType" });
    }
}

[Serializable]
public class LetterTemplate : TrackableEntity
{
    public LetterTemplate()
    {
        ClientFields = new Collection<ClientFormField>();
        CaseFields = new Collection<CaseFormField>();
        SystemFields = new Collection<SystemField>();
    }

    public string Name { get; set; }
    public string Data { get; set; }

    public virtual ICollection<ClientFormField> ClientFields { get; set; }
    public virtual ICollection<CaseFormField> CaseFields { get; set; }
    public virtual ICollection<SystemField> SystemFields { get; set; }

    public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if(string.IsNullOrWhiteSpace(Name))
            yield return new ValidationResult("Form Template must have a name", new[] { "Name" });

        if(string.IsNullOrWhiteSpace(Data))
            yield return new ValidationResult("Form Template must have content", new[] { "Data" });
    }
}

Below is the configuration for the LetterTemplate class.

public class LetterTemplateConfiguration : BaseTrackableEntityConfiguration<LetterTemplate>
{
    public LetterTemplateConfiguration()
    {
        HasMany(c => c.ClientFields).WithMany(c => c.LetterTemplates)
            .Map(m =>
                     {
                         m.MapLeftKey("LetterTemplateId");
                         m.MapRightKey("ClientFormFieldId");
                         m.ToTable("LetterTemplateClientFields");
                     });

        HasMany(c => c.CaseFields).WithMany(c => c.LetterTemplates)
            .Map(m =>
                     {
                         m.MapLeftKey("LetterTemplateId");
                         m.MapRightKey("CaseFormFieldId");
                         m.ToTable("LetterTemplateCaseFields");
                     });

        HasMany(c => c.SystemFields).WithMany(c => c.LetterTemplates)
            .Map(m =>
                     {
                         m.MapLeftKey("LetterTemplateId");
                         m.MapRightKey("SystemFieldId");
                         m.ToTable("LetterTemplateSystemFields");
                     });
    }
}

Here is the controller method for Add/Update and the server method that holds the business logic for Add/Update

[HttpPost]
[ValidateAntiForgeryToken]
[ValidateInput(false)]
public ActionResult Manage(LetterTemplate template)
{
    if(ModelState.IsValid)
    {
        if (_letterTemplateService.Save(template) != null)
            return RedirectToAction("List");
    }

    ViewBag.ClientFields = _clientFieldService.GetAllFields().OrderBy(f => f.Name);
    ViewBag.CaseFields = _caseFieldService.GetAllFields().OrderBy(f => f.Name);
    ViewBag.SystemFields = _systemFieldService.GetAllFields().OrderBy(f => f.Name);

    return View(template);
}

public LetterTemplate Save(LetterTemplate template)
{
    var dbTemplate = template;

    if (template.Id > 0)
    {
        dbTemplate = _letterTemplateRepo.GetById(template.Id);
        dbTemplate.Name = template.Name;
        dbTemplate.Data = template.Data;
    }

    dbTemplate.ClientFields.Clear();
    foreach (var field in _clientFieldRepo.All().Where(field => template.Data.Contains("~~" + field.VarName + "~~")))
        dbTemplate.ClientFields.Add(field);

    dbTemplate.CaseFields.Clear();
    foreach (var field in _caseFieldRepo.All().Where(field => template.Data.Contains("~~" + field.VarName + "~~")))
        dbTemplate.CaseFields.Add(field);

    dbTemplate.SystemFields.Clear();
    foreach (var field in _systemFieldRepo.All().Where(field => template.Data.Contains("~~" + field.VarName + "~~")))
        dbTemplate.SystemFields.Add(field);

    return template.Id <= 0 ? _letterTemplateRepo.Add(dbTemplate) : _letterTemplateRepo.Update(dbTemplate);
}

Here is the view for Add/Update of the Letter Template.

@section RightContent
{
<h4>Manage</h4>
<form method="POST" action="/LetterTemplate/Manage" id="templateForm">
    <div id="toolbar">
        <div style="float: left;padding: 3px 0px 0px 10px;">
            @Html.LabelFor(m => m.Name)
            @Html.TextBoxFor(m => m.Name)
        </div>
        <div class="item" onclick=" $('#templateForm').submit(); ">
            <span class="save"></span>Save
        </div>
        <div class="item" onclick=" window.location = '/LetterTemplate/List'; ">
            <span class="list"></span>Back To List
        </div>
    </div>
    <div class="formErrors">
        @Html.ValidationSummary()
    </div>
    @Html.HiddenFor(m => m.Id)
    @Html.HiddenFor(m => m.Active)
    @Html.HiddenFor(m => m.IsDeleted)
    @Html.TextAreaFor(m => m.Data)
    @Html.AntiForgeryToken()
</form>
}

When I am creating a new template from the view everything is working fine. I get fields populated in my Many to Many relationship tables as I expect. When I attempt to update the relationship which should clear out all existing relations and create new relations nothing happens. The tables are not affected at all. I've read through several different posts about issues with updating many to many tables but haven't found anything that fixes my issue. The is the first time I have attempted many to many with EF code first and followed many tutorials before hand but it seems that no matter what I do, EF will not update the relationship tables.

UPDATE:

Queries generated when adding a new template:

DECLARE @0 nvarchar = N'Test',
        @1 nvarchar = N'<p>~~case_desc~~</p>

<p>~~fname~~</p>

<p>~~lname~~</p>
',
        @2 bit = 1,
        @3 bit = 0,
        @4 int = 2,
        @5 int = 2,
        @6 DateTime2 = '2013-04-08T16:36:09',
        @7 DateTime2 = '2013-04-08T16:36:09'

insert [dbo].[LetterTemplates]([Name], [Data], [Active], [IsDeleted], [CreatedById], [ModifiedById], [DateCreated], [DateModified])
values (@0, @1, @2, @3, @4, @5, @6, @7)


DECLARE @0 int = 2,
        @1 int = 1

insert [dbo].[LetterTemplateClientFields]([LetterTemplateId], [ClientFormFieldId])
values (@0, @1)

DECLARE @0 int = 2,
        @1 int = 2

insert [dbo].[LetterTemplateClientFields]([LetterTemplateId], [ClientFormFieldId])
values (@0, @1)

DECLARE @0 int = 2,
        @1 int = 3

insert [dbo].[LetterTemplateClientFields]([LetterTemplateId], [ClientFormFieldId])
values (@0, @1)

Query Generated on update:

DECLARE @0 nvarchar = N'Test',
        @1 nvarchar = N'<p>~~case_desc~~</p>

<p> </p>

<p>~~fname~~</p>

<p> </p>

<p>~~dob~~</p>
',
        @2 bit = 1,
        @3 bit = 0,
        @4 int = 2,
        @5 int = 2,
        @6 DateTime2 = '2013-04-08T16:23:12',
        @7 DateTime2 = '2013-04-08T16:33:15',
        @8 int = 1

update [dbo].[LetterTemplates]
set [Name] = @0, [Data] = @1, [Active] = @2, [IsDeleted] = @3, [CreatedById] = @4, [ModifiedById] = @5, [DateCreated] = @6, [DateModified] = @7
where ([Id] = @8)

UPDATE

My repository pattern has 2 base generic classes. A Base Trackable Entity Repository and a Base Repository. The base trackable entity repo handles making sure deleted items are soft deleted, getting non deleted items, and managing the createdby/modifiedby and createdDate/UpdatedDate. The base repo handles the rest of the basic CRUD operations. Below is the update method and associated methods that get called when I call update through the LetterTemplateRepository. Since this repo inherits the base trackable entity repo it runs update from the base class.

public override T Update(T entity)
{
    return Update(entity, false);
}

public override T Update(T entity, bool attachOnly)
{
    InsertTeData(ref entity);
    entity.ModifiedById = CurrentUserId;
    entity.DateModified = DateTime.Now;
    _teDB.Attach(entity);
    _db.SetModified(entity);
    if (!attachOnly) _db.Commit();
    return entity;
}

private void InsertTeData(ref T entity)
{
    if (entity == null || entity == null) return;
    var dbEntity = GetById(entity.Id);
    if (dbEntity == null) return;
    _db.Detach(dbEntity);
    entity.CreatedById = dbEntity.CreatedById;
    entity.DateCreated = dbEntity.DateCreated;
    entity.ModifiedById = dbEntity.ModifiedById;
    entity.DateModified = dbEntity.DateModified;
}

The SetModified method in by DbContext just sets the EntityState to Modified. I use a Fake DbContext and DbSet in my unit tests so any EF specific calls I extend through the DbContext to allow my tests to work without having to create a bunch of Fake Repositories.

Was it helpful?

Solution

Turns out the issue was in the InsertTeData Method. When it was detaching the entity that I pulled from the db to make sure createdby and created date it caused the entity I was working with to lose all information about the many to many relationships. My guessing is the way the entity tracking works and they both had the same key.

I've removed the InsertTeData method and now manage everything as default values in the constructor of the abstract TrackableEntity class and everything is working now.

public override T Update(T entity, bool attachOnly)
{
    entity.ModifiedById = CurrentUserId;
    entity.DateModified = DateTime.Now;
    _teDB.Attach(entity);
    _db.SetModified(entity);
    if (!attachOnly) _db.Commit();
    return entity;
}

After running all my unit tests, integration tests, and some manual tests, everything passed so I am fine with this change.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top