In my web i am retrieving a question object Including the subcategories it falls in and the language related data. It also has the QuestionID that is Primary Key for Question. I place this object in PageSession and modify the properties if user wants to.
using (var bo = new BL.QuestionBO())
{
var question = bo.Get(QuestionID);
PageSession.CurrentQuestion = question;
}
public Question Get(long QuestionID)
{
return DataSource.Questions
.Include(q => q.TransQuestions)
.Include(q => q.SubCategories)
.FirstOrDefault(q => q.QuestionID == QuestionID);
}
When I add the object back to the DataContext it adds a new object instead of updating the privious one.
DataSource.Questions.Add(QuestionEnt);
DataSource.SaveChanges();
Am i missing something or coding wrongly?
I have below areas in which i may be wrong.
The data context objects are different while retrieving an object.
I am using Questions.Add()
The proxy object may be don't care about insert/update.
Below is the class generated by .edmx
public partial class Question
{
public Question()
{
this.ChallengeDetails = new HashSet<ChallengeDetail>();
this.TransQuestions = new HashSet<TransQuestion>();
this.SubCategories = new HashSet<SubCategory>();
}
public long QuestionID { get; set; }
public int QuestionTypeID { get; set; }
public int Status { get; set; }
public int InsertedBy { get; set; }
public Nullable<int> ModifiedBy { get; set; }
public virtual ICollection<ChallengeDetail> ChallengeDetails { get; set; }
public virtual QuestionType QuestionType { get; set; }
public virtual ICollection<TransQuestion> TransQuestions { get; set; }
public virtual ICollection<SubCategory> SubCategories { get; set; }
}
Here is the reason.
The reason it happens is that when you use the DbSet.Add method (that
is, Screencasts.Add), not only is the state of the root entity marked
“Added,” but everything in the graph that the context was not
previously aware of is marked Added as well. Even though the developer
may be aware that the Topic has an existing Id value, Entity Framework
honors its EntityState (Added) and creates an Insert database command
for the Topic, regardless of the existing Id.
Source
To update an entity, here are the options you could do.
1. Option A, Change the State
DataSource.Entry(QuestionEnt).State = EntityState.Modified;
DataSource.SaveChanges();
2. Option B, Get the entity from context
var questionEntDb = DataSource.Questions.Find(QuestionEnt.QuestionID);
questionEntDb.PropertyA = QuestionEnt.PropertyA;
questionEntDb.PropertyB = QuestionEnt.PropertyB;
questionEntDb.PropertyC = QuestionEnt.PropertyC;
DataSource.SaveChanges();
When you call Add method, even though it has the existing Id, the state of the entity becomes Added and if you call Attachafter calling Add, the state becomes Unchanged. In this case, it will not do the insert nor the update.
// State = Detached
DataSource.Questions.Add(QuestionEnt); // -> State = Added
DataSource.Questions.Attach(QuestionEnt); // -> State = Unchanged
DataSource.SaveChanges();
Related
We have been implementing our ERD in EF.
Code First for Project
public class Project
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ProjectID { get; set; }
[Index("IX_ProjectGUID", IsUnique = true)]
[Required]
public Guid GUID { get; set; }
[MaxLength(256), Index("IX_ProjectName", IsUnique = true)]
[Required]
public string Name { get; set; }
public virtual ICollection<UserAttribute> UserAttributes { get; set; }
}
Code First for UserAttributes
public class UserAttribute
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int UserAttributeID { get; set; }
[Index("IX_Project_Atttribute_Name", 1, IsUnique = true)]
public int ProjectID { get; set; }
[ForeignKey("ProjectID")]
public virtual Project Project{ get; set; }
[Index("IX_Project_Atttribute_Name", 2, IsUnique = true)]
public int AttributeTypeID { get; set; }
[ForeignKey("AttributeTypeID")]
public virtual SystemUserAttribute SystemUserAttribute { get; set; }
[MaxLength(256), Index("IX_Project_Atttribute_Name", 3, IsUnique = true)]
[Required]
public string Name { get; set; }
public virtual ICollection<UserBooleanAttribute> UserBooleanAttributes { get; set; }
}
So you can see from the last line of each of those classes a 1-many bidirectional relationship is setup.
Before I introduced this 1-many collection I would of been required to todo:
var jar = context.Projects
.Where(p=>p.ProjectID==1)
.Join(
context.UserAttributes,
a => a.ProjectID, b => b.ProjectID,
(a, b) => new {a, b});
Now I only have to do:
Projects.Where(prj=>prj.ProjectID==1).Select(ua=>ua.UserAttributes).Single()
Since Lazy-Loading in effect, is there really no degradation to performance?
*Single -- Why do I need to call this or something similar like FirstOrDefault?
Looks ok. A few quirks to it, like why you are assigning both an ID and GUID to a project. Really should be one or the other.
Single seems out of place because you are selecting the UserAttributes, which could be one or more of them. Single implies there is only one, and if that was true, your design is more complex than it should be.
I assume you'll be adding navigation properties for string and integer as well, which is just fine.
The User*Attributes classes should also have navigation properties to UserAttributes as well.
I've tried analyzing your design, and all I can say is it gives me a headache. I'm going to assume there is some outside reason you've chosen the PK's you have instead of using natural keys. From a glance, UserAttributes seems poorly named. It's not user attributes, it appears to be a project's attributes (or attributes that are assignable to a user for each project). I would also ask if breaking your attributes up into 3 separate tables instead of always serialzing the value into a string is worth the headaches, because it's not likely to save you any space (with the exception of integer -- perhaps) and greatly slow down every query you need to make.
I have added computed fields(Active and CreditsLeft) directly into my CodeFirst entity class. Is it good idea to add computed field logic inside CF Entity class?
public class User : Entity
{
public User()
{
Id = Helper.GetRandomInt(9);
DateStamp = DateTime.UtcNow;
TimeZone = TimeZoneInfo.Utc.Id;
}
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
[Required]
[MaxLength(50)]
public string Email { get; set; }
[Required]
[MaxLength(50)]
public string Password { get; set; }
[MaxLength(50)]
public string FirstName { get; set; }
[MaxLength(50)]
public string LastName { get; set; }
[Required]
public DateTime DateStamp { get; set; }
public virtual ICollection<Order> Orders { get; set; }
public virtual ICollection<Statistic> Statistics { get; set; }
public virtual ICollection<Notification> Notifications { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public bool Active
{
get
{
return Orders.Any(c => c.Active && (c.TransactionType == TransactionType.Order || c.TransactionType == TransactionType.Subscription));
}
}
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public int CreditsLeft
{
get
{
return Orders.Sum(p => p.Credits != null ? p.Credits.Value : 0);
}
}
}
Is it good idea to add computed field logic inside CF Entity class?
Sure, you can do this, but there are a few things you must take care of.
First, the attribute for a property that is computed by business logic is not [DatabaseGenerated(DatabaseGeneratedOption.Computed)], because this indicates that the value is computed in the database (as in a computed column). You should mark the property by the [NotMapped] attribute. This tells Entity Framework to ignore the property in database mapping.
Second, since both properties use Orders, you must make sure that the orders are loaded or can be lazy loaded when either property is accessed. So you may want to load Users with an Include statement (Include(user => user.Orders)). Or else you must ensure that the context is still alive when Active or CreditsLeft is accessed.
Third, you can't address the properties directly in an EF LINQ query, as in
db.Users.Select(u => u.Active);
because EF will throw an exception that it doesn't know Active. You can address the properties only on materialized user objects in memory.
Having just figured out how to populate my ViewModel from a model using Automapper, I am now onto the next challenge – populating the ViewModel properties from a joined table.
The image below depicts my simple database.
My ViewModel class is defined as:
public class PersonViewModel
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Nullable<int> Age { get; set; }
public Nullable<int> SuffixId { get; set; }
public string PersonStatus { get; set; }
public string PersonPreference { get; set; }
public virtual Suffix Suffix { get; set; }
} enter code here
Note the additional fields of PersonStatus and PersonPreference that will come from joining PersonInfo to Person.
Here is the code I use to instantiate my mapping configuration:
Mapper.CreateMap<PersonViewModel, Person>();
Mapper.CreateMap<Person, PersonViewModel>();
And now the code to populate the ViewModel
List<PersonViewModel> persons = null;
using (var context = new DemoEntities())
{
persons = (from p in context.People.AsEnumerable()
join i in context.PersonInfoes on p.Id equals i.PersonId
select Mapper.Map<PersonViewModel>(p)).ToList();
return persons;
}
How would I populate the two joined properties (PersonStatus and PersonPreference) from this setup?
Thanks in advance!
AutoMapper can automatically flatten an object structure if the source and target classes meet some requirements.
The source class, the class that the mapping starts from, should have reference property to the related class. In you case: Person.PersonInfo (1:1, but n:1 will also work).1
The target class should contain property names that allow AutoMapper to resolve the property path to the referenced object. in your case: PersonViewModel.PersonInfoStatus, so AutoMapper will see that there's a property PersonInfo in the source object and a property Status in the referenced object.
If you've got these things in place, you can do
persons = context.People.Project().To<PersonViewModel>().ToList();
The Project().To syntax is a relatively new API that allows working on IQueryables. For more details see Does AutoMapper support Linq?.
You will see that this sends a JOIN query to the database.
1AutoMapper can also aggregate/flatten associated collections, but that's a different chapter.
I have a base class, a set of subclasses and a collection container that I am using to create and populate controls dynamically using partial views.
// base class
public class SQControl
{
[Key]
public int ID { get; set; }
public string Type { get; set; }
public string Question { get; set; }
public string Previous { get; set; }
public virtual string Current { get; set; }
}
// subclass
public class SQTextBox : SQControl
{
public SQTextBox()
{
this.Type = typeof(SQTextBox).Name;
}
}
//subclass
public class SQDropDown : SQControl
{
public SQDropDown()
{
this.Type = typeof(SQDropDown).Name;
}
[UIHint("DropDown")]
public override string Current { get; set; }
}
// collection container used as the Model for a view
public class SQControlsCollection
{
public List<SQControl> Collection { get; set; }
public SQControlsCollection()
{
Collection = new List<SQControl>();
}
}
I populate the Collection control with different subclasses of SQControl as required at runtime, and in EditorTemplates I have a separate view for each subclass. Using Html.EditorFor on the collection items I can dynamically generate the form with the appropriate controls.
This all works fine.
The problem I have is that when I save the form, MVC binding cannot tell what subclass each item in the Collection was created with and instead bind them to instances of the base class SQControl.
This confuses the View engine as it cannot determine the proper views to load anymore and simply loads the default.
The current workaround I have is to save the "Type" of the subclass as a field in the model, and on postback, I copy the collection into a new container, re-creating each object with the proper subclass based on the information in the Type field.
public static SQControlsCollection Copy(SQControlsCollection target)
{
SQControlsCollection newCol = new SQControlsCollection();
foreach (SQControl control in target.Collection)
{
if (control.Type == "SQTextBox")
{
newCol.Collection.Add(new SQTextBox { Current = control.Current, Previous = control.Previous, ID = control.ID, Question = control.Question });
}
else if (control.Type == "SQDropDown")
{
newCol.Collection.Add(new SQDropDown { Current = control.Current, Previous = control.Previous, ID = control.ID, Question = control.Question });
}
...
}
return newCol;
}
So my Question is, is there any better way to maintain the type of items in a base-typed-collection between postbacks? I understand it's practice in MVC to have a typed view for each model, but I want to be able to build a view on the fly based on an XML document using reusable partial views.
I have a Model which contains an Address and Person twice, once for the "main" contact, and once for the "invoice" contact, and a boolean value called InvoiceContactSameAsMain - a clumsy name, but descriptive. The getter of the property checks to see if the Address and Contact objects for "main" and "invoice" are the same, and returns true if they are. The setter checks to see if the value is true, and if so, copies the main Person over the invoice Person , and the main Address over the invoice Address.
In my View, the boolean value is represented by a check box (as you'd expect). Attached to this is a small JS function which, if the check box is checked, hides the invoice fields and "switches off" the client-side validation by setting the data-val HTML attribute to false and forcing a re-parse of the unobtrusive validation attributes in the form. Un-checking the box naturally shows the fields and turns the validation back on.
All of this works fine, until I get to my Controller.
Despite the Model being "valid" and containing the correct fields (thanks to my InvoiceContactSameAsMain setter), ModelState.IsValid remains resolutely false, and I can't seem to find any way to revalidate the model. If I clear the ModelState, any and all errors disappear. I'd very much rather avoid digging through the fields in the ModelState by name, as the Person and Address objects are used throughout the project and may need to change or be extended at some point.
Is there something obvious I've missed here that will allow me to revalidate the ModelState? I've tried TryUpdateModel and TryValidateModel, but they both appear to use the cached ModelState values. I've even tried recursively calling my Action again, passing in the "fixed" model. I'm almost thankful that one didn't work.
Please let me know if any more detail or examples will help.
Edit: Obviously, if this is completely the wrong way to approach the problem, just let me know.
Edit 2: Added code samples as per Ron Sijm's suggestion.
The model is as follows:
public class Details
{
public int? UserID { get; set; }
public Company Company { get; set; }
public Address CompanyAddress { get; set; }
public Person MainPerson { get; set; }
public Address InvoiceAddress { get; set; }
public Person InvoiceContact { get; set; }
[Display(Name = "Promotional code")]
[StringLength(20, ErrorMessage = "Promotional code should not exceed 20 characters")]
public string PromotionalCode { get; set; }
[Display(Name = "Invoice contact same as main")]
public bool InvoiceContactSameasMain
{
get { return InvoiceContact.Equals(MainPerson); }
set
{
if (value)
{
InvoiceContact = MainPerson.Copy();
InvoiceAddress = CompanyAddress.Copy();
}
}
}
[_Common.MustAccept]
[Display(Name = "I agree with the Privacy Policy")]
public bool PrivacyFlag { get; set; }
[Display(Name = "Please subscribe to Sodexo News Letter")]
public bool MarketingOption { get; set; }
[Display(Name = "Contract number")]
public int? ContractNumber { get; set; }
public Details()
{
Company = new Company();
CompanyAddress = new Address();
MainPerson = new Person();
InvoiceAddress = new Address();
InvoiceContact = new Person();
}
}
This is wrapped in a ViewModel as there are a number of SelectLists involved in the page:
public class DetailsViewModel
{
public Details Details { get; set; }
public SelectList MainContactTitles { get; set; }
public SelectList InvoiceContactTitles { get; set; }
public SelectList SICCodes { get; set; }
public SelectList TypesOfBusiness { get; set; }
public SelectList NumbersOfEmployees { get; set; }
public DetailsViewModel()
{
}
}
The Controller's two relevant actions are as follows:
public class DetailsController : _ClientController
{
[Authorize]
public ActionResult Index()
{
DetailsViewModel viewModel = new DetailsViewModel();
if (Client == null)
{
viewModel.Details = DetailsFunctions.GetClient((int)UserId, null);
}
else
{
viewModel.Details = DetailsFunctions.GetClient((int)UserId, Client.ContractNumber);
}
viewModel.MainContactTitles = DetailsFunctions.GetTitles((int)UserId, viewModel.Details.MainPerson.title);
viewModel.InvoiceContactTitles = DetailsFunctions.GetTitles((int)UserId, viewModel.Details.InvoiceContact.title);
viewModel.SICCodes = DetailsFunctions.GetSICCodes(viewModel.Details.Company.sic_code);
viewModel.NumbersOfEmployees = DetailsFunctions.GetNumbersOfEmployees(viewModel.Details.Company.number_of_employees);
viewModel.TypesOfBusiness = DetailsFunctions.GetTypesOfBusiness(viewModel.Details.Company.public_private);
return View(viewModel);
}
[Authorize]
[HttpPost]
public ActionResult Index(DetailsViewModel ViewModel)
{
if (ModelState.IsValid)
{
//go to main page for now
DetailsFunctions.SetClient((int)UserId, ViewModel.Details);
return RedirectToAction("Index", "Home");
}
else
{
ViewModel.MainContactTitles = DetailsFunctions.GetTitles((int)UserId, ViewModel.Details.MainPerson.title);
ViewModel.InvoiceContactTitles = DetailsFunctions.GetTitles((int)UserId, ViewModel.Details.InvoiceContact.title);
ViewModel.SICCodes = DetailsFunctions.GetSICCodes(ViewModel.Details.Company.sic_code);
ViewModel.NumbersOfEmployees = DetailsFunctions.GetNumbersOfEmployees(ViewModel.Details.Company.number_of_employees);
ViewModel.TypesOfBusiness = DetailsFunctions.GetTypesOfBusiness(ViewModel.Details.Company.public_private);
return View(ViewModel);
}
}
}
I can provide the view and JS if needs be, but as the Model binding is all working just fine, I'm not sure how much help that is.
It's a moderately crap hack, but I've ended up just clearing the ModelState errors for the relevant fields in the controller before checking ModelState.IsValid:
if(ViewModel.Details.InvoiceContactSameasMain)
{
//iterate all ModelState values, grabbing the keys we want to clear errors from
foreach (string Key in ModelState.Keys)
{
if (Key.StartsWith("Details.InvoiceContact") || Key.Startwith("Details.InvoiceAddress"))
{
ModelState[Key].Errors.Clear();
}
}
}
The only upside is, if the Person or Address objects change, this code won't need to be altered.