MVC5 - How to validate compound checks? - c#

I am working on a MVC Razor project that needs some more complex input validations. We are using ViewModels to remove all access to the data model from the controller without going through a logic layer. What we need to solve is how to do the following types of validations:
User selected date is after another date value:
// Read Only for user
public DateTime StartDate { get; set; }
// Must be after StartDate
public DateTime OccurredAt { get; set; }
Sum of user inputs for N (variable) fields do not exceed the value of another field.
// Read only for user
public double StartingAmount { get; set; }
// Sum of these fields must be less than starting amount
public double AmountTransfered { get; set; }
public double AmountLosses { get; set; }
public double AmountSampled { get; set; }
// Validation Check
if (StartingAmount - (AmountTransfered + AmountLosses + AmountSampled) > 0)
isValid = true;
I am new to MVC, and most validation things I find on Google are from 2010 and are loaded with JavaScript to do custom implementations.
I am hoping that there are newer mechanisms to perform compound validation using attributes to define what fields are related. I suspect that the solution for both of these will be very similar, just a different set of parameters and data types.

Related

Form for object with computed property in terms of a navigation property

Context
Here's a model class:
public class HealthRecord
{
public int Id { get; set; }
[DataType(DataType.Date)]
public DateTime Date { get; set; }
[DataType(DataType.Time)]
public DateTime Time { get; set; }
public int FoodItemId { get; set; }
public FoodItem FoodItem { get; set; } // navigation
public decimal Amount { get; set; }
public decimal Calories => FoodItem.Calories * Amount;
}
Note that the following is a calculated property:
public decimal Calories => FoodItem.Calories * Amount;
And that FoodItem is a navigation property.
The issue
If I fill out the create form for a new HealthRecord object:
and press Create, I get the following:
Note 1
I set a breakpoint on CreateModel.OnPostAsync in my HealthRecords page model. However, it appears that it doesn't even get that far.
Note 2
It seems that ASP.NET Core is, for some reason, trying to access the HealthRecord.Calories property of the object that results from submitting the form, but the FoodItem property on that object is still null (although FoodItemId is set).
So for example, if I simply comment out the calculated property in HealthRecord:
// public decimal Calories => FoodItem.Calories * Amount;
the create form works.
Similarly, we can add a null check on FoodItem as a workaround:
public decimal Calories => FoodItem != null ? FoodItem.Calories * Amount : 0;
Question
It seems the null-check above is a kludge and shouldn't be necessary.
What's a good way to allow for the Calories property to exist and for the form to still work properly?
Project
Whole project on github: link
HealthRecord class:
Models/HealthRecord.cs
Page model for HealthRecord create page:
HealthRecords/Create.cshtml.cs
Update
Charleh in a comment below suggests the following:
[ValidateNever]
public decimal Calories => FoodItem.Calories * Amount;
And yes, it does seem to work!
This property is mentioned here:
Advanced \ Model validation \ Built-in attributes

Merge properties from mapping table to single class

I have a website that is using EF Core 3.1 to access its data. The primary table it uses is [Story] Each user can store some metadata about each story [StoryUserMapping]. What I would like to do is when I read in a Story object, for EF to automatically load in the metadata (if it exists) for that story.
Classes:
public class Story
{
[Key]
public int StoryId { get; set; }
public long Words { get; set; }
...
}
public class StoryUserMapping
{
public string UserId { get; set; }
public int StoryId { get; set; }
public bool ToRead { get; set; }
public bool Read { get; set; }
public bool WontRead { get; set; }
public bool NotInterested { get; set; }
public byte Rating { get; set; }
}
public class User
{
[Key]
public string UserId { get; set; }
...
}
StoryUserMapping has composite key ([UserId], [StoryId]).
What I would like to see is:
public class Story
{
[Key]
public int StoryId { get; set; }
public bool ToRead { get; set; } //From user mapping table for currently logged in user
public bool Read { get; set; } //From user mapping table for currently logged in user
public bool WontRead { get; set; } //From user mapping table for currently logged in user
public bool NotInterested { get; set; } //From user mapping table for currently logged in user
public byte Rating { get; set; } //From user mapping table for currently logged in user
...
}
Is there a way to do this in EF Core? My current system is to load the StoryUserMapping object as a property of the Story object, then have Non-Mapped property accessors on the Story object that read into the StoryUserMapping object if it exists. This generally feels like something EF probably handles more elegantly.
Use Cases
Setup: I have 1 million stories, 1000 users, Worst-case scenario I have a StoryUserMapping for each: 1 billion records.
Use case 1: I want to see all of the stories that I (logged in user) have marked as "to read" with more than 100,000 words
Use case 2: I want to see all stories where I have NOT marked them NotInterested or WontRead
I am not concerned with querying multiple StoryUserMappings per story, e.g. I will not be asking the question: What stories have been marked as read by more than n users. I would rather not restrict against this if that changes in future, but if I need to that would be fine.
Create yourself an aggregate view model object that you can use to display the data in your view, similar to what you've ended up with under the Story entity at the moment:
public class UserStoryViewModel
{
public int StoryId { get; set; }
public bool ToRead { get; set; }
public bool Read { get; set; }
public bool WontRead { get; set; }
public bool NotInterested { get; set; }
public byte Rating { get; set; }
...
}
This view model is concerned only about aggregating the data to display in the view. This way, you don't need to skew your existing entities to fit how you would display the data elsewhere.
Your database entity models should be as close to "dumb" objects as possible (apart from navigation properties) - they look very sensible as they are the moment.
In this case, remove the unnecessary [NotMapped] properties from your existing Story that you'd added previously.
In your controller/service, you can then query your data as per your use cases you mentioned. Once you've got the results of the query, you can then map your result(s) to your aggregate view model to use in the view.
Here's an example for the use case of getting all Storys for the current user:
public class UserStoryService
{
private readonly YourDbContext _dbContext;
public UserStoryService(YourDbContext dbContext)
{
_dbContext = dbContext;
}
public Task<IEnumerable<UserStoryViewModel>> GetAllForUser(string currentUserId)
{
// at this point you're not executing any queries, you're just creating a query to execute later
var allUserStoriesForUser = _dbContext.StoryUserMappings
.Where(mapping => mapping.UserId == currentUserId)
.Select(mapping => new
{
story = _dbContext.Stories.Single(story => story.StoryId == mapping.StoryId),
mapping
})
.Select(x => new UserStoryViewModel
{
// use the projected properties from previous to map to your UserStoryViewModel aggregate
...
});
// calling .ToList()/.ToListAsync() will then execute the query and return the results
return allUserStoriesForUser.ToListAsync();
}
}
You can then create a similar method to get only the current user's Storys that aren't marked NotInterested or WontRead.
It's virtually the same as before, but with the filter in the Where to ensure you don't retrieve the ones that are NotInterested or WontRead:
public Task<IEnumerable<UserStoryViewModel>> GetForUserThatMightRead(string currentUserId)
{
var storiesUserMightRead = _dbContext.StoryUserMappings
.Where(mapping => mapping.UserId == currentUserId && !mapping.NotInterested && !mapping.WontRead)
.Select(mapping => new
{
story = _dbContext.Stories.Single(story => story.StoryId == mapping.StoryId),
mapping
})
.Select(x => new UserStoryViewModel
{
// use the projected properties from previous to map to your UserStoryViewModel aggregate
...
});
return storiesUserMightRead.ToListAsync();
}
Then all you will need to do is to update your View's #model to use your new aggregate UserStoryViewModel instead of your entity.
It's always good practice to keep a good level of separation between what is "domain" or database code/entities from what will be used in your view.
I would recommend on having a good read up on this and keep practicing so you can get into the right habits and thinking as you go forward.
NOTE:
Whilst the above suggestions should work absolutely fine (I haven't tested locally, so you may need to improvise/fix, but you get the general gist) - I would also recommend a couple of other things to supplement the approach above.
I would look at introducing a navigation property on the UserStoryMapping entity (unless you already have this in; can't tell from your question's code). This will eliminate the step from above where we're .Selecting into an anonymous object and adding to the query to get the Storys from the database, by the mapping's StoryId. You'd be able to reference the stories belonging to the mapping simply by it being a child navigation property.
Then, you should also be able to look into some kind of mapping library, rather than mapping each individual property yourself for every call. Something like AutoMapper will do the trick (I'm sure other mappers are available). You could set up the mappings to do all the heavy lifting between your database entities and view models. There's a nifty .ProjectTo<T>() which will project your queried results to the desired type using those mappings you've specified.

How to validate textboxes in ASP.NET MVC

I am new to ASP.NET MVC and am trying to validate a text-box. Basically, if user inputs less than 2 or a non number how can I get the error to display. Here's the tutorial I am trying to follow.
I have my code below.
Create View:
<%= Html.ValidationSummary()%>
<%= using (HtmlBeginForm()){%>
<div class="half-col">
<label for="Amount">Amount:</label>
<%= Html.TextBox("Amount")%>
<%= Html.ValidationMessage("Amount", "*")%>
</div>
Create Controller:
[AcceptVerbs (HttpVerbs.Post)]
public ActionResult Create([Bind(Exclude ="ID")] Charity productToCreate)
{
//Validation
if (productToCreate.Amount < 2)
ModelState.AddModelError("Amount, Greater than 2 please");
return View(db.Donations.OrderByDescending(x => x.ID).Take(5).ToList()); //Display 5 recent records from table
}
Model:
public class Charity
{
public int ID { get; set; }
public string DisplayName { get; set; }
public DateTime Date { get; set; }
public Double Amount { get; set; }
public Double TaxBonus { get; set; }
public String Comment { get; set; }
}
Error:
CS1501 No overload for method 'AddModelError' takes 1 CharitySite
You are adding the error to your modelstate incorrectly. You can read more about the ModelStateDictionary on the MSDN
AddModelError takes 2 parameters, so you would want:
ModelState.AddModelError("Amount", "Greater Than 2 Please.");
Having said that, you can use attributes to validate your model properties so you don't have to write all of that code by hand. Below is an example using the Range attribute. The RegularExpression attribute could also work. Here is an MSDN article containing information about the different types of attributes.
public class Charity
{
public int ID { get; set; }
public string DisplayName { get; set; }
public DateTime Date { get; set; }
[Range(2, Int32.MaxValue, ErrorMessage = "The value must be greater than 2")]
public Double Amount { get; set; }
public Double TaxBonus { get; set; }
public String Comment { get; set; }
}
Also as a side note, the tutorial you are following is for MVC 1&2. Unless you HAVE to use / learn that. I would recommend following the tutorial for MVC 5 here.
Change this line:
ModelState.AddModelError("Amount, Greater than 2 please");
to:
ModelState.AddModelError("Amount ", "Amount, Greater than 2 please");
The first parameter is the member of the model being validated; it can be an empty string just to indicate an error not associated to a field. By specifying the Amount field, internally it uses that to highlight the erroring field (the control should have input-validation-error CSS class added to it) if you are using all of the client-side validation pieces.
ModelState.AddModelError takes 2 arguments, not 1. Link to MSDN ModelStateDictionary.AddModelError Method.
ModelState.AddModelError("Amount", "Greater than 2 please");
if (productToCreate.Amount < 2)
ModelState.AddModelError("Amount", "Greater than 2 please");

ASP.NET MVC data annotations attribute Range set from another property value

Hi I have following in my Asp.net MVc Model
TestModel.cs
public class TestModel
{
public double OpeningAmount { get; set; }
[Required(ErrorMessage="Required")]
[Display(Name = "amount")]
[Range(0 , double.MaxValue, ErrorMessage = "The value must be greater than 0")]
public string amount { get; set; }
}
Now from my controller "OpeningAmount " is assign .
Finaly when I submit form I want to check that "amount" must be greater than "OpeningAmonut" . so want to set Range dynamically like
[Range(minimum = OpeningAmount , double.MaxValue, ErrorMessage = "The value must be greater than 0")]
I do not want to use only Jquery or javascript because it will check only client side so possible I can set Range attribute minimum dynamically than it would be great for.
Recently there's been an amazing nuget that does just that: dynamic annotations and it's called ExpressiveAnnotations
It allows you to do things that weren't possible before such as
[AssertThat("ReturnDate >= Today()")]
public DateTime? ReturnDate { get; set; }
or even
public bool GoAbroad { get; set; }
[RequiredIf("GoAbroad == true")]
public string PassportNumber { get; set; }
There's no built-in attribute which can work with dependence between properties.
So if you want to work with attributes, you'll have to write a custom one.
Se here for an example of what you need.
You can also take a look at dataannotationsextensions.org
Another solution would be to work with a validation library, like (the very nice) FluentValidation .

Validating Against Property of 2nd Model

As part of my objective of learning a new skill at work I am attempting to develop an employee management system in ASP.NET MVC (MVC 4).
I am trying to follow the convention of performing all validation at the model level (not only because this is what I have read is recommended but also as there is talk of a desktop app that may use parts of the model so I want to ensure any constraints are enforced in that app too!).
My issue is, I have some data on the Person class (RemainingHoliday). When create a HolidayRequest I want to ensure that the request is not for a greater number of days than the person has remaining.
How would I go about doing this? I know that I can create my own validation rules by extending the ValidationAttribute, but how would I get from the HolidayRequest class to the Person class within here?
A snippet of the models:
public class Person
{
public string PersonID { get; set; } // this is populated with Users AD Guid
public string HolidayEntitlement { get; set; }
public virtual ICollection<HolidayRequest> Holidays { get; set; }
public int TotalEntitlement(int year = -1)
{
return this.HolidayEntitlement + this.HolidayAdjustments.Where(a => a.LeaveYear.Year == year).Sum(a => a.Adjustment);
}
public int RemainingHoliday(int year = -1)
{
return this.TotalEntitlement(year) - this.Holidays.Where(h => h.Start.Year == year).Where(h => h.Status != HolidayStatus.Rejected).Sum(h => h.Duration);
}
}
public class HolidayRequest
{
public string HolidayId { get; set; }
public DateTime Start { get; set; }
public DateTime Finish { get; set; }
public int Duration { get; set; } // This cannot be greater than Person.RemainingHoliday
}
I would really appreciate any pointers or samples for this, or perhaps I am trying to be too ideal and this cannot be done in the model?
It seems that you are missing a reference to the person requesting the holiday in the HolidayRequest object. I would have expected to see a Person or PersonId in the HolidayRequest object. Once there, you could compute the difference between Duration and RemainingHoliday.

Categories