When calling a view which displays properties for ASP.NET's ApplicationUser class which I have extended, I receive the below error when the code tries to render a line where I dive into a ApplicationUser class:
#model BaseballStatTracker.Models.ApplicationUser
#foreach (var stats in Model.GameStatistics)
{
<tr>
<td>
#Html.DisplayFor(modelItem => stat.Game.GameTime)
</td>
</tr>
}
...causes:
Any property accessed from within the "Game" property causes the error, and Razor gives no indication that there is a problem when editing the view. Just accessing regular properties on the GameStatistics works without issue.
I have two contexts; the standard ApplicationDbContext and my GamesContext which houses both the Game and GameStatistics entities. I have overridden the OnModelCreating method on the GamesContext per the following:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Game>().HasKey(i => i.GameId);
modelBuilder.Entity<Game>().HasOptional(i => i.HomeTeam).WithMany(i => i.HomeGames).HasForeignKey(i => i.HomeTeamId);
modelBuilder.Entity<Game>().HasOptional(i => i.AwayTeam).WithMany(i => i.AwayGames).HasForeignKey(i => i.AwayTeamId);
modelBuilder.Entity<Game>().HasOptional(i => i.Diamond).WithMany(i => i.Games).HasForeignKey(i => i.DiamondId);
modelBuilder.Entity<GameStatistics>().HasKey(i => i.GameStatisticsId);
modelBuilder.Entity<GameStatistics>().HasRequired(i => i.Game).WithMany(i => i.GameStatistics).HasForeignKey(i => i.GameId);
modelBuilder.Entity<GameStatistics>().HasRequired(i => i.Player).WithMany(i => i.GameStatistics).HasForeignKey(i => i.PlayerId);
...
}
The ApplicationUser, Game and GameStatistics classes look like the following:
ApplicationUser:
public class ApplicationUser : IdentityUser
{
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
return userIdentity;
}
[Required]
[Display(Name = "First Name")]
[StringLength(155, ErrorMessage = "First Name can only be max 155 characters in length.")]
public string FirstName { get; set; }
[Display(Name = "Last Name")]
[StringLength(155, ErrorMessage = "Last Name can only be max 155 characters in length.")]
public string LastName { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get
{
return FirstName + " " + LastName;
}
}
[Display(Name = "Player Number")]
public int PlayerNumber { get; set; }
[Display(Name = "Team")]
public virtual Guid TeamId { get; set; }
public virtual Team Team { get; set; }
public virtual ICollection<GameStatistics> GameStatistics { get; set; }
}
Game:
public class Game
{
[Key]
public Guid GameId { get; set; }
public string GameName { get
{
return AwayTeam.Name + " # " + HomeTeam.Name + ", " + GameDate.Value.Date.ToLongDateString();
} }
[DataType(DataType.Date)]
[Display(Name = "Game Date")]
public DateTime? GameDate { get; set; }
[DataType(DataType.Time)]
[Display(Name = "Game Time")]
public DateTime? GameTime { get; set; }
[Display(Name = "Game Location")]
public virtual Guid? DiamondId { get; set; }
public virtual Diamond Diamond { get; set; }
[Display(Name = "Home Team")]
public virtual Guid? HomeTeamId { get; set; }
public virtual Team HomeTeam { get; set; }
[Display(Name = "Away Team")]
public virtual Guid? AwayTeamId { get; set; }
public virtual Team AwayTeam { get; set; }
public virtual ICollection<GameStatistics> GameStatistics { get; set; }
}
GameStatistics:
public class GameStatistics
{
[Key]
public Guid GameStatisticsId { get; set; }
public Guid GameId { get; set; }
public virtual Game Game { get; set; }
public string PlayerId { get; set; }
public virtual ApplicationUser Player { get; set; }
public int AtBats { get; set; }
public int Hits { get; set; }
public int Walks { get; set; }
}
I have tried redoing the OnModelCreate method but I can't see what I have done wrong with the Game<-GameStatistics relationship. Any help would be much appreciated. Thank you in advance -
This issue was resolved after adding a couple of "Inverse Property" annotations to my Games model on the HomeTeam and AwayTeam properties. I also moved away from using the OnModelCreating override and switched to using the annotations completely. The relevant part of the change on the code was:
[Display(Name = "Home Team")]
public virtual Guid? HomeTeamId { get; set; }
[ForeignKey("HomeTeamId")]
[InverseProperty("HomeGames")] // Added this -
public virtual Team HomeTeam { get; set; }
[Display(Name = "Away Team")]
public virtual Guid? AwayTeamId { get; set; }
[ForeignKey("AwayTeamId")]
[InverseProperty("AwayGames")] // Added this -
public virtual Team AwayTeam { get; set; }
I am not sure why I was not able to achieve the same result setting the configuration in the data contexts - I am sure it was just a misunderstanding on my part but I would love to know why. Should EF not have known via the configuration what the inverse properties were?
I had tried changing the configuration setup but to no effect - I could not see anything wrong with it in the first place, however. Nevertheless, it is now working using the data annotations.
Related
I have a class named Restaurant:
namespace Futures.Api.Data.Models
{
public class Restaurant
{
public Restaurant()
{
Dishes = new HashSet<Dish>();
}
[Display(Name = "Restaurant Id")]
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int RestaurantId { get; set; }
[Display(Name = "Restaurant Name")]
public string RestaurantName { get; set; } = null!;
[Display(Name = "Category Name")]
public string CategoryName { get; set; } = null!;
[Display(Name = "Restaurant Address")]
public string RestaurantAddress { get; set; } = null!;
[Display(Name = "Zone Id")]
public int? ZoneId { get; set; }
public virtual FoodCategory CategoryNameNavigation { get; set; } = null!;
public virtual Zone? Zone { get; set; }
public virtual ICollection<Dish> Dishes { get; set; }
public virtual Dish Dish { get; set; }
}
}
And I have XUnit Test Class within which I am instantiating the class restaurant like so
Restaurant restaurant1 = new Restaurant
{
RestaurantName = "Khana",
RestaurantAddress = "Tokyo",
CategoryName = "Indian",
ZoneId = 2
};
And yet when I try to catch the name with the below method it returns a null exception, see below method
[Fact]
public async Task GetRestaurantByName()
{
Restaurant restaurant = await _repository.GetByName("Khana");
Assert.Equal("Khana", restaurant.RestaurantName);
}
See error 'System.NullReferenceException : Object reference not set to an instance of an object.'
When running the debug I see that restaurant1 doesn't contain the name or any other properties, apart from the 'CategoryNameNavigation', see below screenshot
Or here the autos
restaurant CategoryNameNavigation = null
Futures.Api.Data.Models.Restaurant
CategoryNameNavigation null
Futures.Api.Data.Models.FoodCategory
+ this
{Futures.Api.Data.Sqlite.Test2.SqliteRestaurantTest.FuturesConfigTests}
Futures.Api.Data.Sqlite.Test2.SqliteRestaurantTest.TestWithSqlite
{Futures.Api.Data.Sqlite.Test2.SqliteRestaurantTest.FuturesConfigTests}
As you can see, all the properties (such as RestaurantName) are gone :(
Here is the method GetByName
public async Task<Restaurant?> GetByName(string restaurantName)
{
return await _context.Restaurants
.Include(restaurantRecord => restaurantRecord.Dishes)
.Include(restaurantRecord => restaurantRecord.Zone)
.Include(restaurantRecord => restaurantRecord.CategoryNameNavigation)
.SingleOrDefaultAsync(record => record.RestaurantName.ToLower().Contains(restaurantName.ToLower()));
}
I would be grateful if anyone could hint me to where the issue here, any tips are welcome!
Being quite a newbie to c# I am sure there's something obvious I missed here.
I have a model class and I want to make on the parameter "required" only if the value of another parameter is something.
[JsonConverter(typeof(JsonStringEnumConverter))]
[Required]
public AddressTagEnum AddressTagId { get; set; }
[RequiredIf("AddressTagId", 3)]
[MaxLength(20)]
public string AddressTagOther { get; set; }
How can I achieve this?
Thanks in advance.
there are several ways to solve your problem:
there is no standard way to do this
there are librariers e.g. Expressive Annotations that should help
there is a built-in attribute Remote that lets you perform a server validation like: [Remote(action: "VerifyEmail", controller: "Users")] see docs
i prefer (i know that this is opinion based) a implementation with IValidatableObject. Citing the docs:
public class ValidatableMovie : IValidatableObject
{
private const int _classicYear = 1960;
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Title { get; set; }
[DataType(DataType.Date)]
[Display(Name = "Release Date")]
public DateTime ReleaseDate { get; set; }
[Required]
[StringLength(1000)]
public string Description { get; set; }
[Range(0, 999.99)]
public decimal Price { get; set; }
public Genre Genre { get; set; }
public bool Preorder { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Genre == Genre.Classic && ReleaseDate.Year > _classicYear)
{
yield return new ValidationResult(
$"Classic movies must have a release year no later than {_classicYear}.",
new[] { nameof(ReleaseDate) });
}
}
}
I recently finished an MVC course on udemy that built a video rental app. He covered how to check out a movie but left on our own to figure out how check it back in.
I have a Customer model:
public class Customer
{
public int Id { get; set; }
[Required(ErrorMessage = "Please enter customer's name.")]
[StringLength(255)]
public string Name { get; set; }
public bool IsSubscribedToNewsletter { get; set; }
[Display(Name = "Date of Birth")]
[Min18YearsIfAMember]
public DateTime? Birthdate { get; set; }
public MembershipType MembershipType { get; set; }
[Display(Name = "Membership Type")]
public byte MembershipTypeId { get; set; }
}
A Movie model:
public class Movie
{
public int Id { get; set; }
[Required]
[StringLength(255)]
public string Name { get; set; }
public Genre Genre { get; set; }
[Display(Name = "Genre")]
[Required]
public byte GenreId { get; set; }
[Display(Name = "Release Date")]
public DateTime ReleaseDate { get; set; }
public DateTime DateAdded { get; set; }
[Display(Name = "Number in Stock")]
[Range(1, 20)]
public byte NumberInStock { get; set; }
public byte NumberAvailable { get; set; }
}
And a Rental model that contains a customer and a movie:
public class Rental
{
public int Id { get; set; }
[Required]
public Customer Customer { get; set; }
[Display(Name = "Customer Name")]
public int CustomerId { get; set; }
[Required]
public Movie Movie { get; set; }
public DateTime DateRented { get; set; }
public DateTime? DateReturned { get; set; }
}
I made a view that will display the active rentals and I made a Delete action that will delete it as if the movie has been turned in, but I can't figure out how to put that movie back in stock by increasing the number of available movies(Movies.NumberAvailable). I tried to do it in the same action as the Delete action but I'm having no luck. Here's the Delete action:
public ActionResult Delete(int id)
{
Rental rental = _context.Rentals.Find(id);
//var movie = _context.Movies.Where(m => rental.Movie.Id.Contains(m.Id));
rental.Movie.NumberAvailable++;
_context.Rentals.Remove(rental);
_context.SaveChanges();
return RedirectToAction("List");
}
I tried to pull out just that movie into it's own variable and add 1 to that but the rental.Movie.Id part popped an error saying it doesn't have a definition for Contain. If I run it as it is above, I get an exception at rental.Movie.NumberAvailable++; saying The 'Object reference not set to an instance of an object.'
Does anyone have any ideas on I can do to fix this? Yes, I'm a noob.
try :
Rental rental = _context.Rentals.Where(r => r.Id == id).FirstOrDefault();
rental.Movie.NumberAvailable++;
There are a couple approaches I can suggest to tackle problems like this.
Track the movie instances and their status and calculate the totals on demand.
This would be something like a Movie class, but then each movie would have a collection of MovieDisc for instance:
public class MovieDisc
{
public int MovieDiscId{ get; set; }
public bool InStock { get; set; }
public string Barcode { get; set; }
}
Basically as movies are checked out or scanned in, their barcode identifies the "disc" or instance and sets the InStock. From the movie side:
public class Movie
{
// ...
public virtual ICollection<MovieDisc> Discs {get; internal set;} = new List<MovieDisc>();
public byte NumberInStock
{
get { return Discs.Count(x => x.InStock); }
}
public byte NumberAvailable
{
get { return Discs.Count(x => !x.InStock); }
}
}
The caveat of this approach is that to use these properties, the Discs collection needs to be eager loaded, or will trip a lazy load.
Adopt a DDD approach to the entities.
Domain Driven Design essentially adds controls to how state changes in the domain. Rather than using individual setters for values, you use methods or actions on the domain entity to validate and control the valid, and complete changes allowed against the domain.
public class Movie
{
public int Id { get; set; }
[Required]
[StringLength(255)]
public string Name { get; private set; }
public Genre Genre { get; private set; }
[Display(Name = "Genre")]
[Required]
public byte GenreId { get; private set; }
[Display(Name = "Release Date")]
public DateTime ReleaseDate { get; private set; }
public DateTime DateAdded { get; private set; }
[Display(Name = "Number in Stock")]
[Range(1, 20)]
public byte NumberInStock { get; private set; }
public byte NumberAvailable { get; private set; }
public void RentOneOut()
{
if (NumberInStock <= 0)
throw new InvalidOperation("Cannot rent out a movie that has no stock.");
if (NumberAvailable <= 0)
throw new InvalidOperation("All movie copies are out.");
NumberAvailable -= 1;
}
public void ReturnOneIn()
{
if (NumberInStock <= 0)
throw new InvalidOperation("Cannot return a movie that has no stock.");
if (NumberAvailable >= NumberInStock)
throw new InvalidOperation("All movie copies are already in. Stocktake needed.");
NumberAvailable += 1;
}
}
Note all setters are private. (or internal if you want to enable unit testing) The objective is to express valid operations against a domain entity as a method. This ensures that multiple checks and updates are performed as a whole so that multiple properties can be updated together rather than risking leaving an entity in an incomplete state.
This might be more practical for your rental scenario:
public function ReturnRental(Rental rental)
{
if (rental == null)
throw new ArgumentNullException("rental");
rental.Return();
}
// In Rental:
public class Rental
{
// ... private setters, like in Movie.
public void Return()
{
Movie.ReturnOneIn();
DateReturned = DateTime.Today;
}
}
You'd want to handle the scenario where the return failed for whatever reason. (data state out of sync)
Hopefully that gives you a few ideas.
Is there any way to somehow combine the data from two models and THEN map them both to the same viewModel in the context of an edit action?
I have never had to update several tables at once in an edit action in ASP.NET MVC with Entity Framework 6.1.3. This is the layout:
I have a DB table called "Address" which has fields for StreetNumber, StreetName, City, State, ZipCode. It has a one-to-one relationship with another table called Bars. As in, a bar can only have one address and one address can only have one bar.
Because I am storing this data in two separate tables, I am having a very difficult time trying to successfully implement an Edit action which takes data from one form (BarForm) and should update both the Bar and Address database tables. See my code:
BarController
public ActionResult Edit(int id)
{
var bar = _context.Bars.SingleOrDefault(m => m.Id == id);
var address = _context.Addresses.SingleOrDefault(a => a.BarId == id);
//Make sure that the id actually exists:
if (bar == null)
{
return HttpNotFound();
}
var viewModel = Mapper.Map<Bar, BarFormViewModel>(bar, new BarFormViewModel());
if (address == null)
{
address = new Address();
}
Mapper.Map<Address, BarFormViewModel>(address, viewModel);
viewModel.IsNew = false;
return View("BarForm", viewModel);
}
[ValidateAntiForgeryToken]
public ActionResult Save(BarFormViewModel bar)
{
if (!ModelState.IsValid)
{
var viewModel = Mapper.Map<BarFormViewModel, BarFormViewModel>(bar, new BarFormViewModel());
viewModel.IsNew = false;
return View("BarForm", viewModel);
}
if (bar.Id == 0)
{
var newbar = Mapper.Map<BarFormViewModel, Bar>(bar);
newbar.LastUpdated = DateTime.UtcNow;
_context.Bars.Add(newbar);
var addressToAdd = Mapper.Map<BarFormViewModel, Address>(bar);
_context.Addresses.Add(addressToAdd);
}
else
{
var barInDb = _context.Bars.Single(b => b.Id == bar.Id);
var addressInDb = _context.Addresses.Single(a => a.BarId == bar.Id);
Mapper.Map<BarFormViewModel, Bar>(bar, barInDb);
Mapper.Map<BarFormViewModel, Address>(bar, addressInDb);
}
_context.SaveChanges();
return RedirectToAction("Index", "Bar");
}
Domain Models:
public class Bar
{
public int Id { get; set; }
public string Name { get; set; }
[Required]
public string GooglePlaceId { get; set; }
public string SundayDiscounts { get; set; }
public string MondayDiscounts { get; set; }
public string TuesdayDiscounts { get; set; }
public string WednesdayDiscounts { get; set; }
public string ThursdayDiscounts { get; set; }
public string FridayDiscounts { get; set; }
public string SaturdayDiscounts { get; set; }
[Display(Name = "Last Updated")]
public DateTime LastUpdated { get; set; }
}
public class Address
{
public int Id { get; set; }
public int? Number { get; set; }
public string StreetName { get; set; }
public string City { get; set; }
public string State { get; set; }
[Required]
public int ZipCode { get; set; }
public Bar Bar { get; set; }
public int BarId { get; set; }
}
View Model which includes both Address and Bar properties:
{
public class BarFormViewModel
{
public int? Id { get; set; }
public string Name { get; set; }
[Required]
[Display(Name = "Google Place ID")]
public string GooglePlaceId { get; set; }
[Display(Name = "Sunday Happy Hour Info:")]
public string SundayDiscounts { get; set; }
[Display(Name = "Monday Happy Hour Info:")]
public string MondayDiscounts { get; set; }
[Display(Name = "Tuesday Happy Hour Info:")]
public string TuesdayDiscounts { get; set; }
[Display(Name = "Wednesday Happy Hour Info:")]
public string WednesdayDiscounts { get; set; }
[Display(Name = "Thursday Happy Hour Info:")]
public string ThursdayDiscounts { get; set; }
[Display(Name = "Friday Happy Hour Info:")]
public string FridayDiscounts { get; set; }
[Display(Name = "Saturday Happy Hour Info:")]
public string SaturdayDiscounts { get; set; }
[Display(Name = "Last Updated")]
public DateTime? LastUpdated { get; set; }
//Address Model Info
public Address Address { get; set; }
public int? AddressId { get; set; }
[RegularExpression("([1-9][0-9]*)", ErrorMessage = "Must be a number")]
public int? Number { get; set; }
public string StreetName { get; set; }
public string City { get; set; }
public string State { get; set; }
[Required]
public int? ZipCode { get; set; }
public bool IsNew { get; set; }
}
The problem here is that I am getting an empty AddressId with this setup, which is causing an exception when the Save action gets run. This is because the BarForm view is getting passed a ViewModel which has been mapped from a Bar object and the Bar domain model actually has no Address information in it, since it is not the Address model/table.
Is there any way to somehow combine the data from both the Address and Bar models and THEN map them both to the same viewModel?
I keep getting a Sequence Contains no Elements error for this line in the Save action:
var addressInDb = _context.Addresses.Single(a => a.Id == bar.AddressId);
I also tried:
var addressInDb = _context.Addresses.Single(a => a.BarId == bar.Id);
Neither work. I understand what the error is saying and have also checked the actual HTML for my hidden Addressid field and it is blank... See code in my BarForm View:
#Html.HiddenFor(m => m.Id)
#Html.HiddenFor(m => m.AddressId)
#Html.AntiForgeryToken()
Remove the new BarFormViewModel() as the second parameter in your mapping calls as it is not necessary.
In your post action, inside your if statement that checks if the ModelState is valid and if bar.Id == 0, bar is already a view model, so no need to mapping.
And when you create your AutoMapper mapping, you must create a custom property mapping because the Address.Id property will not map automatically to the AddressId property as the name is not the same.
AutoMapper.Mapper.CreateMap<Address, BarFormViewModel>()
.ForMember(dest => dest.AddressId, o => o.MapFrom(source => source.Id));
And then do the same for the inverse mapping.
I have an Attendee Class and An AttendeeViewModel
The datetime field on the Attendee Model gets set to the default .NET Datetime when i map it from AttendeeViewModel instead of the value that is already existing in the Attendee Model
Here's my AttendeeViewModel
public class AttendeeViewModel
{
public int Id { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[Display(Name = "First Name")]
public string FirstName { get; set; }
[Required]
[Display(Name = "Last Name")]
public string LastName { get; set; }
public int FEventId { get; set; }
public string DisplayName
{
get { return string.Format("{0}, {1}", FirstName, LastName); }
}
}
Here's my Base AttendeeModel
public class Attendee
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
public string Email { get; set; }
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
public DateTime CreatedAt { get; set; }
public bool IsActive { get; set; }
public int FEventId { get; set; }
public virtual ApplicationUser CreatedBy { get; set; }
public virtual FEvent FEvent { get; set; }
public ICollection<ProjectPledge> ProjectPledges { get; set; }
}
Here's My mapping configuration
public static void Configure()
{
Mapper.CreateMap<AttendeeViewModel, Attendee>().ForMember(dest=>dest.CreatedAt , opt=>opt.Ignore());
}
And heres's the Controller Action
[HttpPost]
[ValidateAntiForgeryToken]
public virtual ActionResult Edit(AttendeeViewModel attendee)
{
if (!_attendeeService.CanAddAttendee(attendee.Email, attendee.FilanthropyEventId))
{
AddEmailModelError();
}
if (ModelState.IsValid)
{
var mappedAttendee = _attendeeService.GetById(attendee.Id);
mappedAttendee = Mapper.Map<AttendeeViewModel, Attendee>(attendee);
_attendeeService.AddOrUpdate(mappedAttendee);
return RedirectToAction(MVC.Attendee.Index(mappedAttendee.FilanthropyEventId));
}
return View(attendee);
}
if I set the configuration to be this insetad of opt.Ignore()
Mapper.CreateMap<AttendeeViewModel, Attendee>().ForMember(dest=>dest.CreatedAt , opt=>opt.UseDestinationValue());
The Mapping fails giving this exception
Missing type map configuration or unsupported mapping.
Mapping types:
AttendeeViewModel -> DateTime
MyProject.Web.ViewModels.AttendeeViewModel -> System.DateTime
Destination path:
Attendee.CreatedAt.CreatedAt
Source value:
MyProject.Web.ViewModels.AttendeeViewModel
Any ideas on how i can resolve this?
If you want to map onto an existing object you need to use the overload that takes the existing destination:
Mapper.Map<Source, Destination>(source, destination);
that should do the trick.
Have you tried removing the ".ForMember" section and just let AutoMapper ignore it? In order to help you any more it would be helpful to see the two models for comparison.
Update: after lookind at your models I would suggest the following should solve the issue you are having...
Mapper.CreateMap <attendeeviewmodel, attendee>.ForMember (x => x.CreatedAt, opt => opt.MapFrom (src => datetime.utcnow));