MVC3 + Entity Framework, updating selective fields in an entity - c#

Assume I have Entity Framework setup correctly, and the POCO classes are have the proper relationships, etc.
<!-- language: c# -->
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string SomeFieldIDontWantUsersToEdit { get; set; } // there are a lot of these
public IList<Address> Addresses { get; set; }
}
public class Address
{
public string EntireAdressInOneLine { get; set; }
}
When I display things to the user, I only want to show FirstName and LastName, and the addresses. That works and the view is functioning properly.
This is my view:
<!-- language: c# -->
#using (Html.BeginForm(actionName: null, controllerName: null, method: FormMethod.Post))
{
#Html.TextBoxFor(model => Model.Person.FirstName)
#Html.TextBoxFor(model => Model.Person.LastName)
#Html.HiddenFor(model => Model.Person.Id)
}
But when it comes time to Update this entry, I'm lost. If this is the action for update:
<!-- language: c# -->
[HttpPost]
public ActionResult UpdatePerson(int personId, Person updatedPerson)
{
if (ModelState.IsValid)
{
}
}
When I submit the form, the model binder will pickup that I want a Person object. Cool, that works. Of course, SomeFieldIDontWantUsersToEdit will be null since it wasn't included in the view -- I understand that part.
My question is, is there some way to tell EF that I only want to update certain properties in this entity, and use whatever value was there already for all other properties.
I want the original values of SomeFieldIDontWantUsersToEdit to say the same, but want to allow the user to edit First and Last name. If it were only 1 or 2 fields, sure, I can do it manually, but it's a lot more fields in the actual code.
Maybe a better question is, what is the proper/recommended way of doing this?

Why not actually create a ViewModel, such as:
public class PersonalInfoModel
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
And just have your Action receive that type:
[HttpPost]
public ActionResult UpdatePerson(PersonalInfoModel updatedPerson)
{
if (ModelState.IsValid)
{
}
}
In this scenario, you should go to your context, load the real entity using PersonalInfoModel.Id, and just update the two properties you want to update.
It isn't mandatory that you use your real entities as Models, you can create your own representations of information (in several cases even recommended, instead of using the actual entities).

Use Automapper if you don't want to do the "mapping" by hand.

Related

How Should I Set Navigation Properties Before Inserting Into The Database?

Note: I asked a similar question yesterday, but I've since moved past that problem into another issue. Although it's very closely related, I think it's best expressed in a separate question.
I have three models: Account, AccountType, and Person. I want to make a single form page, through which a new Account, with a specific AccountType, and with specific Person information could be POSTed to the database.
public class AccountType
{
[Key]
public int AccountTypeID { get; set; }
[Required]
public string AccountTypeName { get; set; }
}
public class Person
{
[Key]
public int PersonID { get; set; }
// Bunch of properties not relevant to the question here...
}
public class Account
{
[Key]
public int AccountID { get; set; }
[ForeignKey("AccountType")]
public int AccountTypeID { get; set; }
[ForeignKey("Person")]
public int PersonID { get; set; }
// A few properties here...
public virtual AccountType AccountType { get; set; }
public virtual Person Person { get; set; }
}
Since creating a new account requires me to insert into the account as well as the person table, I created a view model for both of these models:
public class Register
{
public Account Account { get; set; }
public Person Person { get; set; }
}
In the Register view, I simply bound the properties from the Account and Person models to form fields. I also used a ViewBag to display a list of AccountTypes in a dropdown.
The part that I don't understand is in the POST controller:
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Register(Register Register)
{
if (ModelState.IsValid) {
_db.Accounts.Add(Register.Account);
_db.SaveChanges();
return View(Register);
}
// do something else
}
The ModelState check passes successfully, after having commented out the Nullable setting in the project file. However, Register.Account has null properties:
All the values that I bound in the Register view get set correctly, but I did not bind the navigation properties (Register.Account.AccountType and Register.Account.Person) to anything, since I did not know what to do with them.
Now, I can't insert into the database with the above code, because I get a Person foreign key constraint error. It seems that Register.Account cannot have null values for its Person or AccountType navigation properties. Apparently, they must be set (or, at least, the Person property must be).
I know that I can set these navigation properties manually in the controller. For Person, I can write something like this before saving to the DB: Register.Account.Person = Register.Person, and I can likewise come up with something for AccountTypes to give it its proper value. I've tested this, and it does insert into the database.
But, this doesn't strike me as the right approach. It seems to me that there must be a better, more proper way of clarifying the model or table relationships to .NET before inserting into the database.
Does anybody know a better way?
P.S.: I'm using .NET 6.
Per Jeremy's suggestion, I solved this problem by creating a new View Model, which only included the properties that I needed to bind, and omitting any navigation properties that weren't necessary to insert into the database successfully.

Web Api, Update DB, connection reset 200ok

I have asp.net web api application. I have the table Companies in the databse which have two fields: id and description. Recently I've updated the database and added a new column called CustomerID. After that when I am trying to call getCompanies
private readonly BackendContext _context;
public CompaniesController(BackendContext context)
{
_context = context;
}
// GET: api/Companies
[HttpGet]
public IEnumerable<Company> GetCompanies()
{
return _context.Companies;
}
I get
I think the controller tries to return the old companies model but can't achieve it because it doesnt exist now but I don't know how to fix this though the controller should return the updated model. Maybe I should somehow rebuild the app to make it use the updated version?
Additional code:
Context
public class BackendContext : Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityDbContext<IdentityUser>//DbContext
{
public BackendContext(DbContextOptions<BackendContext> options) : base(options) { }
public DbSet<Company> Companies { get; set; }
public DbSet<CompanyToProduct> CompanyToProducts { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<Customer> Customers { get; set; }
public DbSet<Vendor> Vendors { get; set; }
public DbSet<VendorToProduct> VendorToProducts { get; set; }
public DbSet<Invoice> Invoices { get; set; }
public DbSet<InvoiceItem> InvoiceItems { get; set; }
}
Model
public class Company
{
public int ID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public int CustomerID { get; set; }
public virtual Customer Customer { get; set; }
public virtual ICollection<CompanyToProduct> CompaniesToProducts { get; set; }
public virtual ICollection<Invoice> Invoices { get; set; }
}
UPDATE
I've added some values to the table and I got the response of the first company:
[{"id":1,"name":"Google","description":"free food","customerID":6,"customer":null,"companiesToProducts":null,"invoices":null}
BUT I also got the fields which is not specified in the table: customer, companiesToProducts,invoices. Invoices and companiesToProducts are tables in my database and I don't know what is customer referred to. I should also mention that these tables are connected by foreign key.
UPDATE
Error:
Based on the comments on the question above, it sounds like the related tables are all trying to serialize and the overall process is failing likely due to circular references in the object graph. This comment above in particular hints at a solution:
I want to return only the data about companies but the controller also returns another fields like customer, companiesToProducts,invoices
While it's convenient to just return directly from the data context, this has the added side-effect of coupling the API with the database (and with the data access framework, which appears to be the issue here). In API design in general it's always a good idea to explicitly define the "shape" of that API. The fields to return, etc.
Project your result into an explicitly defined shape and return only what you want to return:
var result = _context.Companies
.Select(c => new
{
c.ID,
c.Name,
c.Description,
c.CustomerID
})
.ToList();
This defines specifically what you want to return, fetches only that information from the backing data, materializes it into an in-memory list, and finally then returns it through the API.
There is a potential downside to this, however. Because now we also need to change the return type of your API method. There are a couple options there, such as returning a generic response object or creating a view model which closely approximates your already existing model and starts to feel like duplication.
As with just about anything, it's a balance. Too far in any one direction and that direction starts to become a problem. Personally I often go the route of defining a view model to return:
public class CompanyViewModel
{
public int ID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public int CustomerID { get; set; }
}
and returning that:
return _context.Companies
.Select(c => new CompanyViewModel
{
ID = c.ID,
Name = c.Name,
Description = c.Description,
CustomID = c.CustomerID
})
.ToList();
But the reason I normally do this is because I normally work in an environment where the web application is just one application attached to a common shared business domain, so the view models don't feel like code duplication. They're in a separate project, often take a different shape than the backing data objects, etc. But if your domain models are already in your web project and that's the only project you have, there's a strong desire to want to return those.
Another option when that's the case could be to universally set your JSON serialization to ignore circular references:
services.AddMvc()
.AddJsonOptions(
options => options.SerializerSettings.ReferenceLoopHandling
= Newtonsoft.Json.ReferenceLoopHandling.Ignore );
But do keep in mind that this still couples your API to your DB models. Maybe that's okay in this project, but if you ever add a column to your DB that you don't want users to see then it becomes an issue. As with anything, you have options.

Create View With One-Many Relationship

I have 2 simple models:
public class Country
{
public int ID { get; set; }
public string Name { get; set; }
public virtual ICollection<Region> Region { get; set; }
}
public partial class Region
{
public int ID { get; set; }
public string Name { get; set; }
public int CountryID { get; set; }
public virtual Country Country { get; set; }
}
Is it possible to have a single page to handle the creation of a country whereby the user inputs the country with multiple regions and then only posts to the server?
I've seen an implementation here where you create a custom ViewModel with numbered properties (Region1, Region2, Region3, etc) but it's limiting, any suggestions?
(I know AngularJS can be used to do this however I have no experience in this space as of yet.)
Thanks
Yes its very possible it just depends on how you plan to implement this.
My favourite style of implementing One to Many pages is initially creating the "one" (country) then redirecting to a page with a grid element where users can add the many (regions) to the one. It works well and its a very easy way for both the programmer to create and the user to understand.
As for creating a country with multiple regions in a single post, it could be done but you must think of how the implementation will work.
Sure, this is easy to do. You have defined your data model. Either you use that also as your View Model, or you can create a new model that is a complex object. The methods in your type:
public virtual Country Country { get; set; }
public virtual ICollection<Region> Region { get; set; }
These method being present normally indicates you're using Entity Framework and that these are "related entities" that you can traverse via this "navigation property" at run-time. You can create a Country and populate the Region collection on the fly when you try to use it.
Here is a good example of using a View Model:
What is ViewModel in MVC?
///Example of a Controller method creating a view model to display
[HttpGet]
public ActionResult Index()
{
var user = _userService.Get(User.Identity.Name);
var customerId = GlobalDataManager.GetCustomerId();
if (_error != null)
{
ModelState.AddModelError("", _error);
_error = null;
}
var model = new InboundListModel();
model.Initialize(customerId, user.CompanyId);
foreach (var campaign in model.Campaigns)
{
model.InitializeCallProggress(campaign.Id, _callInfoService.GetCallsCount(campaign.Id));
}
return View(model);
}
This View Model can be anything you want but it does need to be one type. So if you want 2 put 2 types in the ViewModel you just need a new container object:
public class ComplexViewModel
{
public Country Country { get; set; }
public ICollection<Region> Regions { get; set; }
}
Then you just need a way to populate the data like the example above where I call Initialize. This goes out to EF via a DAL project and retrieves the data for the model.

How should I pass values through to a validation action in a Controller if my values are linked to variables of an instance?

I have only just started using MVC and jQuery validate so please bear with me. I also have no idea what the title of my question should be. 8(
Overview
I am using MVC 4 with jQuery validate. My form is being validated on the client side. I have a scenario where two very alike objects need to be on my form. This has been achieved by means of a ModelView which has two properties. The ModelView is linked to the View and everything works excepting the remote validation. I need to validate a field based on a particular value in the object. Everything is linked together nicely excepting the parameters of the validation action in the controller. Before you give me disapproving tsk tsks, I made up the following code scenario.
The Code
Model class where Name requires remote validation depending on the value of GroupID. Essentially, the name is unique to the group.
public class Colour
{
[Key]
public int GroupID {get;set;}
[Required]
[Remote("ColourExists", "Validation", AdditionalFields = "GroupID")]
public string Name {get;set;}
}
Validation controller where the ColourExists action resides.
public class ValidationController :Controller {
public JsonResult ColourExists(string name, string groupID) {
// Add validation here later
return Json(false, JsonRequestBehavior.AllowGet);
}
}
The View and Controller is linked to a ModelView so that I can display two separate instances on my form. Typically I need to ask the user for a Bright and a Dark colour for one group. (Before you tsk, remember, this isn't for real)
public class ColourViewModel {
public Models.Colour BrightColour { get; set; }
public Models.Colour DarkColour {get;set;}
}
The generated HTML has input fields BrightColour_Name and DarkColour_Name. These fields have data-val-remote-additionalfields=*.Name attributes. On blur they GET the correct action and controller but the parameters are null. The expected parameters are InstanceName.VariableName such as BrightColour.Name and DarkColour.Name. The request is sent as follows Validation/ColourExists?BrightColour.Name=red&BrightColour.GroupID=10
So how should I pass the values through to the ColourExists action in the validation Controller if my values are linked to variables of an instance?
Edit
The view looks as follows:
#model Colours.ViewModels.ColourViewModel
#using (Html.BeginForm()) {
#Html.LabelFor(model => model.DarkColour.Name)
#Html.EditorFor(model => model.DarkColour.Name)
#Html.HiddenFor(model => model.DarkColour.GroupID)
<input type="submit" value="Save" />
}
Normally, in this situation you would use a prefix in your remote validation action like here:
public JsonResult ColourExists([Bind(Prefix = "BrightColour")] string name) {
// Add validation here later
return Json(false, JsonRequestBehavior.AllowGet);
}
But, you can't do that in your case, because you are using two same entities in your ViewModel (not ModelView), and each has its own Prefix. So, the binding fails.
So, you'll have to create two separate ViewModels:
public class BrightColourViewModel
{
public int GroupID { get; set; }
[Required]
[Remote("BrightColourExists", "Home", AdditionalFields = "GroupID")]
public string Name { get; set; }
}
public class DarkColourViewModel
{
public int GroupID { get; set; }
[Required]
[Remote("DarkColourExists", "Home", AdditionalFields = "GroupID")]
public string Name { get; set; }
}
Then, redefine your ColourViewModel like here:
public class ColourViewModel
{
public BrightColourViewModel BrightColour { get; set; }
public DarkColourViewModel DarkColour { get; set; }
}
And, then create two separate remote validation actions like this:
public JsonResult BrightColourExists(BrightColourViewModel brightColour)
{
// Call shared code to check if colour exists
return Json(false, JsonRequestBehavior.AllowGet);
}
public JsonResult DarkColourExists(DarkColourViewModel darkColour)
{
// Call shared code to check if colour exists
return Json(false, JsonRequestBehavior.AllowGet);
}

Mapping of ViewModel with SelectList in MVC3

Hi I'm struggling to find the correct approach on SO for what I am currently doing, so I thought I would ask.
Here is my simplified code:
The entities are nested types based on using them with EF CodeFirst and the ViewModel is being mapped with AutoMapper.
When posting the form the ModelState is not valid due to the dropdownlist being mapped to model.CourseId and displaying my Course data.. i.e. CourseId = 2, CourseList = Null, but also having the [Required] attribute, really only CourseId is required but I also needed a relevant error message.
I then thought that in my Create GET & POST actions the view should probably just have the CourseId but I still need to display it as a dropdown and populate it and I was unsure as how to do that correctly.
I may also not be understanding how this should be used correctly and if I even need CourseName, i.e. since the Course already exists in the database I just want a foreign key to it, which will still let me show the selected course.
I'm also planning to break out all this mapping and data setting in my controller actions into a separate service layer but at the moment its a small prototype.
// Entities
public class Recipe {
public int Id { get; set; }
public string Name { get; set; }
public Course Course { get; set; }
}
public class Course {
public int Id { get; set; }
public string Name { get; set; }
}
// View Model
public class RecipeCreateViewModel {
// Recipe properties
public int Id { get; set; }
public string Name { get; set; }
// Course properties, as primitives via AutoMapper
public int CourseId { get; set; }
public string CourseName { get; set; }
// For a drop down list of courses
[Required(ErrorMessage = "Please select a Course.")]
public SelectList CourseList { get; set; }
}
// Part of my View
#model EatRateShare.WebUI.ViewModels.RecipeCreateViewModel
...
<div class="editor-label">
Course
</div>
<div class="editor-field">
#* The first param for DropDownListFor will make sure the relevant property is selected *#
#Html.DropDownListFor(model => model.CourseId, Model.CourseList, "Choose...")
#Html.ValidationMessageFor(model => model.CourseId)
</div>
...
// Controller actions
public ActionResult Create() {
// map the Recipe to its View Model
var recipeCreateViewModel = Mapper.Map<Recipe, RecipeCreateViewModel>(new Recipe());
recipeCreateViewModel.CourseList = new SelectList(courseRepository.All, "Id", "Name");
return View(recipeCreateViewModel);
}
[HttpPost]
public ActionResult Create(RecipeCreateViewModel recipe) {
if (ModelState.IsValid) {
var recipeEntity = Mapper.Map<RecipeCreateViewModel, Recipe>(recipe);
recipeRepository.InsertOrUpdate(recipeEntity);
recipeRepository.Save();
return RedirectToAction("Index");
} else {
recipe.CourseList = new SelectList(courseRepository.All, "Id", "Name");
return View(recipe);
}
}
I fixed my particular problem just by doing the below.
[Required(ErrorMessage = "Please select a Course.")]
public int CourseId { get; set; }
// public string CourseName { get; set; }
public SelectList CourseList { get; set; }
The view will use the DropDownListFor helper to map the drop down to my CourseId and that's all I really needed.
On to another problem now with AutoMapper and why it is not mapping back to the Recipe entity in the POST Create action.
I probably first need to find a way to store the relevant Course name in the "CourseName" property.

Categories

Resources