Pregunta

I have a Survey that contains questions and also which users can/will participate in the survey.

like so

public virtual ICollection<User> ParticipatingUsers { get; set; }
public virtual ICollection<Question> SpecificQuestions { get; set; }

However, due to the ajaxy solution I create the questions first and then simply send in the ID of my created question with the survey data. So all I need to do is change the sortingIndex of the question and then add a reference to it in my Survey.

When it comes to users they belong to a Company entity and I only want to reference them from the survey not own them.

But currently I get all the id's for questions and users in my action method (.net mvc) and so currently I load all questions and users and attach them to my survey entity before sending the survey to the repository.

But when my Repository calls Add on dbset it clones the user and question data instead of simply referencing existing data.

I am lost, I have solved this exact problem for a normal navigation property by adding [Foreignkey] but i don't know how that would work with ICollection

For completeness

Here is my action method recieving the data

    [HttpPost]
    [FlexAuthorize(Roles = "RootAdmin")]
    public ActionResult SaveSurvey(EditSurveyViewModel editModel)
    {
        if (!ModelState.IsValid)
        {
            //We dont bother to send this in so we need to fetch the list again
            editModel.CompanyList = _companyRepository.GetAll();

            List<string> deletionList = new List<string>();

            //We clear out all questions from the state as we have custom logic to rerender them with the correct values
            foreach (var modelstateItem in ModelState)
            {
                if (modelstateItem.Key.StartsWith("Questions"))
                {
                    deletionList.Add(modelstateItem.Key);
                }
            }

            foreach (string key in deletionList)
            {
                ModelState.Remove(key);
            }

            return View("EditSurvey", editModel);
        }

        List<Question> questionlist = new List<Question>();
        int sort = 1;
        Question q;

        //We have questions sent in from the ui/client
        if (editModel.Questions != null)
        {
            //Go trough each questions sent in
            foreach (var question in editModel.Questions)
            {
                //if it's a page break, just assign our new question the sent in one and set sort index
                if (question.IsPageBreak)
                {
                    q = question;
                    q.SortIndex = sort;
                }
                else 
                {
                    //It's a question and all questions are already created with ajax from the client
                    //So we simply find the question and then set sort index and tie it to our survey
                    q = _questionRepository.GetById(question.Id);
                    q.SortIndex = sort;
                }

                questionlist.Add(q);
                sort++;
            }
        }
        //assign the new sorted questions to our Survey
        editModel.Item.SpecificQuestions = questionlist;

        List<User> userlist = new List<User>();

        foreach (int id in editModel.SelectedUsers)
        {
            userlist.Add(_userRepository.GetById(id));
        }

        editModel.Item.ParticipatingUsers = userlist.ToList();

        _surveyRepository.SaveSurveyBindAndSortQuestionsLinkUsers(editModel.Item);

        return RedirectToAction("Index");
    }

Here is the viewmodel the method gets sent in

public class EditSurveyViewModel
{
    public Survey Item { get; set; }
    public IEnumerable<Question> Questions { get; set; }
    public bool FullyEditable { get; set; }
    public IEnumerable<Company> CompanyList { get; set; }
    public IEnumerable<int> SelectedUsers { get; set; }
}

Finally here is the repo method (so far i only implemented insert, not update)

    public void SaveSurveyBindAndSortQuestionsLinkUsers(Survey item)
    {
        if (item.Id == 0)
        {
            Add(item);
        }
               ActiveContext.SaveChanges();
     }

Update/Edit

Moho: You are of course correct, I think to my shame I was testing some things and forgot to reset the method before pasting it in here. I have updated the action method above.

Slauma: Sorry for lack of details, here comes more.

All my repositories look like this

public class EFSurveyRepository : Repository<Survey>, ISurveyRepository

So they inherit a generic repository and implement an interface The generic repository (the part we use in code above, looks like this)

public abstract class Repository<T> : IRepository<T> where T : class
{
    public EFDbContext ActiveContext { get; private set; }
    private readonly IDbSet<T> dbset;

    public Repository()
    {
        this.ActiveContext = new EFDbContext("SurveyConnection");
        dbset = ActiveContext.Set<T>();
    }

    public virtual void Add(T entity)
    {
        dbset.Add(entity);
    }
    public virtual T GetById(int id)
    {
        return dbset.Find(id);
    }

I have noticed in the database that my User table (for User entity) now contains a Survey_Id field which i do not want it to have. I want a many-to-many where many surveys can link to many users (the same users) but the users should entity-wise still only belong to a Department in a Company.

Also, right now when I run the code (after I corrected my action method) I get the following error:

An entity object cannot be referenced by multiple instances of IEntityChangeTracker.

No InnerException, only that when i try to add the new survey.

¿Fue útil?

Solución

The problem is that you are using separate contexts per repository:

public Repository()
{
    this.ActiveContext = new EFDbContext("SurveyConnection");
    //...
}

In your POST action you have four repositories in place: _companyRepository, _questionRepository, _userRepository and _surveyRepository. It means you are working with four different contexts, i.e. you load data from different contexts, create relationships between entities that are attached to different contexts and save the data in yet another context.

That's the reason for the entity duplication in the database, for the "multiple instances of IEntityChangeTracker" exception and will be the source for many other problems you might encounter in future.

You must refactor the architecture so that you are using only one and the same context instance ("unit of work") in every repository, for example by injecting it into the constructor instead of creating a new one:

private readonly EFDbContext _activeContext;
private readonly IDbSet<T> _dbset;

public Repository(EFDbContext activeContext)
{
    _activeContext = activeContext;
    _dbset = activeContext.Set<T>();
}

Otros consejos

You build up questionList, set it to editModel.Item.SpecificQuestions, then overwrite the reference by settting that same property to editModel.Questions.ToList(), which is from your view model (i.e.: not loaded via your database context like questionList's question objects) and therefore appears to be new questions to your database context,

    editModel.Item.SpecificQuestions = questionlist;

    // what is this?  why?
    editModel.Item.SpecificQuestions = editModel.Questions.ToList();

Edit after question update:

Instead of using questionList and assigning to the questions property of the Survey, simply use the property directly.

Also, do you realize that if you're reusing Question records from the DB for multiple Surveys, you're updating the sort order at the question itself and not simply for that Survey? Each time you save a new survey that reuses questions, other surveys' question ordering will me altered. Looks like you need a relationship entity that will map Questions to Surveys where you can also store the sort order so that each survey can reuse question entities without messing up existing surveys question ordering.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top