Issue: How to trim text using asp-for tag & razor page?
Detail: I have a input field where user can enter there firstname. I noticed user can enter multi spaces or tabs. Since I am using asp-for tag, it will auto bind the data to Person object on OnPost() Method
I am trying not to use following methods Request.Form("firstname").trim() or pass value though OnPost(string firstname) Method; Because whole point of asp-for tag is to auto bind your values to object rather than manually getting each values like using name tag.
If there isn't any way to do this, than what is the point of asp.for tag? bc in real world you will end up checking for extra spaces any ways.
<input asp-for="Person.FirstName" />
<span asp-validation-for="Person.FirstName" class="text-danger"></span>
[BindProperty]
public Person_Model Person { get; set; } = default!;
[BindProperty(SupportsGet = true)]
public string? FirstName{ get; set; }
public async Task<IActionResult> OnPostAsync()
{
// here First_name can have spaces
_context2.my_DbSet.Add(Person);
await _context2.SaveChangesAsync();
}
public class Person_Model
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Int64 Id { get; set; }
public string? FirstName{ get; set; }
}
I tried adding RegularExpression, but this will just give me error if there is whtie space inside text-field. I just want to trim it.
[RegularExpression(#"[^\s]+")]
public string? FirstName{ get; set; }
First of all in case the FirstName is a required field, you have to add the Required attribute.
public class Person_Model
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Int64 Id { get; set; }
[Required]
public string? FirstName { get; set; }
}
Now in your OnPostAsync method you first have to check for validation errors and return to page in order to display the error messages.
You can also trim your FirstName if you want and then continue saving the record in your database.
public async Task<IActionResult> OnPostAsync()
{
// validate the model. in case there are validation errors return to page and display them
if (!ModelState.IsValid)
{
return Page();
}
// trim your first name in case there are spaces
Person.FirstName = Person.FirstName?.Trim();
// here First_name can have spaces
_context2.my_DbSet.Add(Person);
await _context2.SaveChangesAsync();
return Page();
}
I hope it helps.
The recommended method for centralising trimming of input strings is to create a custom model binder which does this as part of the model binding process.
Create a class called TrimModelBinder:
public class TrimModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult != ValueProviderResult.None)
{
bindingContext.Result = ModelBindingResult.Success(valueProviderResult.FirstValue?.Trim());
}
return Task.CompletedTask;
}
}
Create a ModelBinderProvider that returns your new model binder if the binding target is a string:
public class TrimModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(string))
{
return new BinderTypeModelBinder(typeof(TrimModelBinder));
}
return null;
}
}
Register this with the services container:
builder.Services.AddMvc(options => options.ModelBinderProviders.Insert(0, new TrimModelBinderProvider()));
Now you no longer have to call Trim() on every input string.
Related
I have a DTO class that is bound from body source to create my user:
public class UserDto
{
[Required()]
[MinLength(2)]
[MaxLength(30)]
public string FirstName { get; set; }
[Required()]
[MinLength(2)]
[MaxLength(30)]
public string LastName { get; set; }
[Required]
[SocialSerialNumber]
public string SSN { get; set; }
[Required]
[PhoneNumber]
public string PhoneNumber { get; set; }
[Required]
public bool? Gender { get; set; }
[Required]
[MinLength(6)]
[MaxLength(30)]
[IgnoreTrim] // this is what I need
public string Password { get; set; }
}
I want to trim (remove extra spaces from) all my strings in all of my models before validation. Trimming must be ignored for strings I explicitly specified as no-trim (maybe using an attribute called [IgnoreTrim]).
In the example above trimming for properties FirstName, LastName, PhoneNumber is needed but not Password.
I know that I can manually trim them in the controller action and then revalidate the model, But I'm looking for graceful way of doing this so that my action stays clean.
I Found a nice solution using custom model binder and custom JSON converter.
public class StringTrimmerBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext is null)
throw new ArgumentNullException(nameof(bindingContext));
var modelName = bindingContext.ModelName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
return Task.CompletedTask;
bindingContext.Result = ModelBindingResult.Success(valueProviderResult.FirstValue.Trim());
return Task.CompletedTask;
}
}
StringTrimmerBinder is my custom binder that transforms the string to a trimmed string using Trim() method. To use this binder I made a custom IModelBinderProvider that is is used to to resolve a binder for specified type (in my case string type).
public class CustomModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context is null)
throw new ArgumentNullException(nameof(context));
if (context.Metadata.ModelType == typeof(string) && context.BindingInfo.BindingSource!=BindingSource.Body)
return new StringTrimmerBinder();
return null;
}
}
So I register CustomModelBinderProvider like this:
services.AddControllers(opt =>
{
//registers CustomModelBinderProvider in the first place so that it is asked before other model binder providers.
opt.ModelBinderProviders.Insert(0, new CustomModelBinderProvider());
});
Until now everything is ok and all string models are trimmed except those which their binding source is request body ([FromBody]). Because default model binders uses System.Text.Json namespace to convert request body to model types, instead of making a new binder that to the same job, I create a JSON converter that customize converting body to model types.
Here is my custom converter:
public class StringTrimmerJsonConverter : JsonConverter<string>
{
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.GetString().Trim();
}
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
writer.WriteStringValue(value);
}
}
And here is the way to use this converter:
services.AddControllers(opt =>
{
opt.ModelBinderProviders.Insert(0, new CustomModelBinderProvider());
})
.AddJsonOptions(opt =>
{
opt.JsonSerializerOptions.Converters.Add(new StringTrimmerJsonConverter());
});
That's it, now all my strings whether in complex-type or in simple-type will be trimmed.
Update
I have included two model binders that work with JSON and Form fields data from the body.
Implement IModelBinder and do the following:
Bind from Json:
public class CustomModelBinderJson : IModelBinder
{
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
var request = bindingContext.HttpContext.Request;
string body;
using (StreamReader sr = new StreamReader(request.Body, Encoding.UTF8))
{
body = await sr.ReadToEndAsync();
}
var dataModel = JsonSerializer.Deserialize<DataModel>(body);
dataModel.Name = dataModel.Name.Trim();
bindingContext.Result = ModelBindingResult.Success(dataModel);
}
}
Or bind from form fields:
public class CustomModelBinderForm : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var request = bindingContext.HttpContext.Request;
var form = request.Form["Name"];
var id = request.Form["Id"];
var dataModel = new DataModel();
dataModel.Id = int.Parse(id);
dataModel.Name = form.ToString().Trim();
bindingContext.Result = ModelBindingResult.Success(dataModel);
return Task.CompletedTask;
}
}
Then
[ModelBinder(BinderType = typeof(CustomModelBinderForm))] //Or use the CustomModelBinderJson
public class DataModel
{
public int Id { get; set; }
public string Name { get; set; }
}
Your action should look someting like this:
[HttpPost]
public IActionResult Index([FromBody] DataModel dataModel)
{
//Do stuff
}
See MSDocs
You may use the getter to trim the value that's going to be stored in a private field. In this way, your validation will be against the trimmed value
For example with the property FirstName
public class UserDto
{
private string firstName;
[Required()]
[PersianChars]
[MinLength(2)]
[MaxLength(30)]
public string FirstName
{
get
{
return firstName?.Trim();
}
set
{
firstName = value;
}
}
}
Another solution is implementing a custom attribute that indicates which properties need to be trimmed before validation. At this point you can automate this process
To automate the process of trimming the values of the properties using a custom attribute:
You need to create a Custom Attribute
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/attributes/creating-custom-attributes
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class TrimmedValueAttribute : Attribute
{
}
Then, add the attribute to the desidered properties, for example FirstName
public class UserDto
{
[Required()]
[PersianChars]
[MinLength(2)]
[MaxLength(30)]
[TrimmedValue]
public string FirstName { get; set }
}
After that, you need to create an Action Filter which automatically trims the properties that has the TrimmedValueAttribute
https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-5.0#action-filters
public class TrimPropertiesActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
Use reflection to find all the properties which has the TrimValueAttribute
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
Then, add the filter on your ConfigureServices method
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews(options =>
{
options.Filters.Add(typeof(TrimPropertiesActionFilter));
});
}
You can change the order of the action filters by settings the second argument of the Add method
options.Filters.Add(typeof(TrimPropertiesActionFilter, order: 0));
You can create a custom middleware and with a reflection find the string properties and trim them.
In case you want to trim specific string properties, you may implement a custom interface that includes the properties you wish to trim, and then in the custom middleware run over all the properties of the custom interface and trim them.
You can see an example of custom middleware here.
And also see how to trim a string with a reflection here.
And another approach would be to build a custom model binding to manipulate the data.
I am learning Dependency Injection.
How can I make my "UserCreate" model to use the custom constructor I set when it is being used as a parameter on controller action? I want to pass the UserContext to my UserCreate model.
My action:
[HttpPost]
public JsonResult Post(UserCreate model)
{
var user = _repository.GetByUserName(model.Email);
if (user != null)
{
this.ModelState.AddModelError(nameof(model.Email), "Email already registered!");
}
else
{
if (ModelState.IsValid)
{
var userModel = _mapper.Map<User>(model);
_repository.Add(userModel);
_repository.SaveChanges();
return Json(new { success = "true" });
}
}
return Json(new { success = "false", errors = this.ModelErrors(this.ModelState) });
}
My Model
public class UserCreate : BaseModel
{
private readonly IUserRepo repo;
public UserCreate(UserContext context) : base(context){
repo = new UserRepository(context);
}
public UserCreate():base() { }
[Required]
[MaxLength(100)]
public string Email { get; set; }
[Required]
[MaxLength(30)]
public string Password { get; set; }
[Required]
[MaxLength(30)]
public string FirstName { get; set; }
[Required]
[MaxLength(30)]
public string MiddleName { get; set; }
[Required]
[MaxLength(30)]
public string LastName { get; set; }
[Required]
public int Age { get; set; }
[Required]
public DateTime Birthday { get; set; }
[Required]
[MaxLength(250)]
public string Adddress { get; set; }
public DateTime Created { get { return DateTime.Now; } }
}
I've set it on startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<UserContext>(opt => opt.UseSqlServer
(Configuration.GetConnectionString("Dev")));
services.AddControllers();
services.AddScoped<IUserRepo, UserRepository>();
services.AddScoped<ICardRepo, CardRepository>();
services.AddScoped<IUserContext, UserContext>();
services.AddScoped<TransactCreate, TransactCreate>();
services.AddSingleton<UserCreate>(x =>
new UserCreate(x.GetRequiredService<UserContext>()));
I have set the Addsingleton on startup however when i test my API, public UserCreate():base() { } constructor is called instead of the constructor with UserContext parameter. I am using netcore 3.1
the reason why I want do this is I to move my validations to model and i need to use UserContext from there.
Thanks!
I understand what you are asking, but please understand that your approach to this problem is very flawed. Your view model should absolutely know nothing about your repository.
In MVC, the Controller is responsible for handling HTTP requests (as well as model validation), and delegating actions to the rest of the application. The Model (UserCreate), should be a simple poco that only exists to transfer data from the client back to your controller. The controller should then delegate responsibility to the repository for handling the data.
Your controller should, instead, accept the repository via DI, and then send the UserCreate model through, after validating it. And your model, UserCreate, should 100% have a parameterless constructor, as the ModelBinder is going to build it up from the request.
however what I want to achieve is if I have multiple properties that i
need to validate from the database, i dont want to write them all in
my controller action. Can you recommend the right way to handle custom
validations?
According to your code and the previous discuss, I suppose you want to valid whether the entered value is exist in the database, if the value exist, display the error message, such as "Email already registered". If that is the case, it is better to use the [Remote] attribute:
Code as below:
[Remote(action: "VerifyEmail", controller: "Users")]
public string Email { get; set; }
and
[AcceptVerbs("GET", "POST")]
public IActionResult VerifyEmail(string email)
{
if (!_userService.VerifyEmail(email))
{
return Json($"Email {email} is already in use.");
}
return Json(true);
}
Besides, if you want to create custom validation, you can check this thread, then, in the Custom validation IsValid method, you could get the current dbcontext and check whether the entered data is valid or not. Code as below:
code in the model:
[Required(ErrorMessage ="Country is Required")]
public string Country { get; set; }
[RequiredIfHasState("Country", ErrorMessage ="State is Required")]
public string State { get; set; }
code in the custom valiation:
public class RequiredIfHasStateAttribute : ValidationAttribute
{
private readonly string _comparisonProperty;
public RequiredIfHasStateAttribute(string comparisonProperty)
{
_comparisonProperty = comparisonProperty;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
ErrorMessage = ErrorMessageString;
//get entered state value
var stateValue = (string)value;
var property = validationContext.ObjectType.GetProperty(_comparisonProperty);
if (property == null)
throw new ArgumentException("Property with this name not found");
//get the country value
var countryValue = (string)property.GetValue(validationContext.ObjectInstance);
//get the current dbcontext
var _context = (MvcMovieContext)validationContext.GetService(typeof(MvcMovieContext));
//query the database and check whether the country has state.
if (_context.Countries.Where(c => c.CountryCode == countryValue).Select(c => c).FirstOrDefault().HasState)
{
if(stateValue == null)
{
//if country has state and the state is null. return error message
return new ValidationResult(ErrorMessage);
}
else
{
//if country has state and the state is not found.
if(!_context.Countries.Where(c => c.CountryCode == countryValue).Any(c => c.States.Any(e => e.StateName == stateValue)))
{
return new ValidationResult("State not found");
}
}
}
return ValidationResult.Success;
}
}
Model:
public class Word
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime? WhenCreated { get; set; }
public ApplicationUser Author { get; set; }
[NotMapped]
public string AuthorName
{
get
{
if (Author != null)
{
return Author.UserName;
}
else {
return "";
}
}
}
public List<Definition> Definitions { get; set; }
}
Controller:
[HttpGet]
public IEnumerable<Word> Get()
{
return _db.Words.Include(x=>x.Author).ToList();
}
My Controller now returns entire ApplicationUser class which is one of properties of Word. I want to send only one property of ApplicationUser: UserName. How can I do that?
I've added AuthorName, which would return only data that I want from ApplicationUser. Unfortunately I still have to .Include(x=>x.Author) to make this property work. Can I somehow omit including Author in process of data serialization (to hide it when sending data to user)?
I know I can use .Select() method, but it requires me to type all properties I will need. If I modify my Model in the future, I will need to update all those .Select() which will would be inconvenient and waste of time.
How would you solve that?
You need to create a Dto object and assign the values to it and return the Dto instead.
Dto
public class WordDto
{
public string Title { get; set; }
public DateTime? WhenCreated { get; set; }
public string AuthorName { get; set; }
}
Then in your action
[HttpGet]
public async Task<IEnumerable<WordDto>> Get()
{
return _db.Words
.Include(x=>x.Author)
.Select(x =>
new WordDto
{
Title = x.Title,
DateTime = x.WhenCreated,
AuthorName = x.Author?.UserName ?? string.Empty
}
)
.ToListAsync();
}
You can try it as shown below.
Note : You don't need to use Include here.
[HttpGet]
public async Task<IEnumerable<Word>> Get()
{
return _db.Words.Select(x => new
{
Word = x,
AuthorName = x.Author.UserName
}
).ToList();
}
Create a View model and use AutoMapper to populate. Look at using AutoMapper and ProjectTo extension https://github.com/AutoMapper/AutoMapper/wiki/Queryable-Extensions
That way if you add properties to View model they will be automatically mapped if they exist on your EF model
So create a VM with required properties named appropriately (see AutoMapper docs on naming conventions):
public class WordVM
{
public string Title { get; set; }
public DateTime? WhenCreated { get; set; }
public string AuthorUserName { get; set; }
}
Then use AutoMapper to project (it will do any required includes so if you changed the VM later then it would handle that)
_db.Words.ProjectTo<WordVM>().ToList();
You don't need the NotMapped property AutoMapper would map the navigation property Author and the Author Property UserName to AuthorUserName
My workaround was to get all the related entities with .include(), then loop over them and omit the property values I did not want to return. It would require some maintenance in case your model changed, but surprisingly, it did not impact the response time dramatically.
I'm trying to add an object using a PartialView inside a popup. It's a simple Rental application for which the data model was generated Model First through Entity Framework. The Controllers and Views have mostly been Scaffolded by EF. The relationship between RentalApplication and RentalObject is 1 to many, meaning a RentalObject always has to have 1 RentalApplication.
My controller looks like this:
// GET: /Calendar/Add/1
// Create a PartialView using a RentalObject as the model.
// Use the provided ID to lock in the RentalApplication.
[HttpGet]
public PartialViewResult Add(int id)
{
return PartialView(
new RentalObject(db.RentalApplicationSet.FirstOrDefault(x => x.Id == id)));
}
// POST: /Calendar/Add
// Save the submitted RentalObject to the db
[HttpPost]
public ActionResult Add(RentalObject rentalobject)
{
if (ModelState.IsValid)
{
try
{
db.RentalObjectSet.Add(rentalobject);
db.SaveChanges();
return RedirectToAction("Index");
}
catch
{
return View();
}
}
return View();
}
My object looks like this:
public partial class RentalObject
{
public RentalObject()
{
this.Lease = new HashSet<Lease>();
}
public RentalObject(RentalApplication rentapp)
{
this.Lease = new HashSet<Lease>();
RentalApplication = rentapp;
PricePerHour = RentalApplication.DefaultPricePerHour;
Currency = RentalApplication.DefaultCurrency;
}
public int Id { get; set; }
public string Name { get; set; }
public bool IsAvailable { get; set; }
public string Illustration { get; set; }
public string Description { get; set; }
public decimal PricePerHour { get; set; }
public string Currency { get; set; }
public virtual ICollection<Lease> Lease { get; set; }
public virtual RentalApplication RentalApplication { get; set; }
}
So when I'm opening the popup (using #Ajax.ActionLink to GET the first controller Add action) I'm creating a RentalObject WITH a RentalApplication (2nd constructor) to use as the model. This works so far, the popup dialog shows the values PricePerHour and Currency from the RentalApplication.
However, when I submit the form in my PartialView popup everything gets copied over BUT the RentalApplication object. It somehow ends up creating a new RentalObject object using the PricePerHour and Currency from the original RentalApplication, but doesn't include the object itself under the RentalApplication property. My debugger even goes to the first constructor for RentalObject.
So I guess it's having trouble keeping a complex object inside another object when submitted from controller to view (GET) and back to controller (POST). Is this just poor practice on my part? Should I be using a ViewModel?
In the past I've had to use #Html.HiddenFor(m=>m.yourObjectHere) on objects that were not changed in the form to keep them from getting new-ed up again. I did this for every object I didn't use in the form (about 2 or 3).
Hope this helps.
In my project I have a model class that uses another class, like in the sample below.
One of the properties in the model depends for validation on on of the properties of a child object -- in this sample, LastName property depends for validation on the value of the Address.PostalCode property.
I implemented a custom validation attribute to validate my LastName property and it works great.
public class User
{
public static ValidationResult ValidateLastName(string lastName, ValidationContext context)
{
// Grab the model instance
var user = context.ObjectInstance as User;
if (user == null)
throw new NullReferenceException();
// Cross-property validation
if (user.Address.postalCode.Length < 10000)
return new ValidationResult("my LastName custom validation message.");
return ValidationResult.Success;
}
[Display(Name = "Last name")]
[CustomValidationAttribute(typeof(User), "ValidateLastName")]
public string LastName { get; set; }
[Display(Name = "First name")]
public string FirstName { get; set; }
[Display(Name = "Address:")]
[CustomValidationAttribute(typeof(User), "ValidateAddress")]
public AddressType Address { get; set; }
}
public class AddressType
{
public string streetName = "";
public string streetNumber = "";
public string postalCode = "";
}
The problem is in the controller the Address property does not get constructed from the view, and it is always null.
In this sample, user.Address is always null, regardless of what I send in the view.
Here is the controller code.
[HttpPost]
public ActionResult Create(User user)
{
if (ModelState.IsValid)
{
// creation code here
return RedirectToAction("Index");
}
else
{
return View(user);
}
}
Here is the view:
<div class="editor-label">
#Html.LabelFor(model => model.Address.postalCode)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Address.postalCode)
#Html.ValidationMessageFor(model => model.Address.postalCode)
</div>
To resolve this, I created a custom dummy binder to map the fields in the view to the properties in the model like so:
public class UserBinder : IModelBinder
{
private string GetValue(ModelBindingContext bindingContext, string key)
{
var result = bindingContext.ValueProvider.GetValue(key);
return (result == null) ? null : result.AttemptedValue;
}
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
User instance = new User();
instance.FirstName = GetValue(bindingContext, "FirstName"); //controllerContext.HttpContext.Request["FirstName"];
instance.LastName = GetValue(bindingContext, "LastName"); //controllerContext.HttpContext.Request["LastName"];
instance.Address = new AddressType();
string streetName = controllerContext.HttpContext.Request["Address.streetName"];
//ModelStateDictionary mState = bindingContext.ModelState;
//mState.Add("LastName", new ModelState { });
//mState.AddModelError("LastName", "There's an error.");
instance.Address.streetName = streetName;
...
return instance;
}
The binder works fine, but the validation attributes do not work anymore.
I think there must be a better way to do the binding than this, is there?
This binder is just mapping LastName to LastName and Address.streetName to Address.streetName, I imagine there should be a way to accomplish this without having to write all this tedious code and without breaking the custom validation mechanism.
You need to use properties instead of public fields in order for the default model binding to work properly.
Change your AddressType class to:
public class AddressType
{
public string streetName { get; set; }
public string streetNumber { get; set; }
public string postalCode { get; set; }
}
One solution is to use properties instead of public fields in my child class -- thanks to Oded for the answer!
Another solution is to call TryValidateModel in the controller, this enables my validation code even with the binder present.
[HttpPost]
public ActionResult Create(User user)
{
if (TryValidateModel(user))
{
// creation code here
return RedirectToAction("Index");
}
else
{
return View(user);
}
}