How to use a constructor from a Action model? - c#

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;
}
}

Related

Asp.net core not binding post model if invalid property values are present

I am migrating an application from legacy asp.net webapi to asp.net core mvc. I have noticed an issue. For some requests, we send partial or even invalid values in the POST body. And asp.net core is refusing to deserialize it.
E.g. post model
public class PostModel
{
public int Id { get; set; }
public Category? Category { get; set; }
}
public enum Category
{
Public,
Personal
}
action
[HttpPost]
public async Task<Response> Post([FromBody]PostModel model)
=> this.Service.Execute(model);
for the following sample request
POST /endpoint
{
id: 3,
category: "all"
}
The ModelState collection records an error - indicating that all is an invalid category, and the PostModel argument model is null. Is it possible to disable this behaviour and just attempt to bind all properties that are possible from the post body, and ignoring the ones it can't bind? This is how it was done for us in our legacy api and for now, I need to port this across.
Disabling the model validation did not help for us. The model argument is still null.
For FromBody, it will bind the request body to Model by JsonInputFormatter.
For JsonInputFormatter, it will call return InputFormatterResult.Success(model) when there is no error, and call return InputFormatterResult.Failure(); when there is any error. For return InputFormatterResult.Failure();, it will not bind the valid property.
For a solution, you could implement custom formatter to return return InputFormatterResult.Success(model).
Implement custom formatter CustomFormatter based on JsonInputFormatter.
Replace InputFormatterResult.Failure() with InputFormatterResult.Success(model).
if (!(exception is JsonException || exception is OverflowException))
{
var exceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception);
exceptionDispatchInfo.Throw();
}
return InputFormatterResult.Success(model);
Inject CustomFormatter in Startup.cs
services.AddMvc(o =>
{
var serviceProvider = services.BuildServiceProvider();
var customJsonInputFormatter = new CustomFormatter(
serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger<CustomFormatter>(),
serviceProvider.GetRequiredService<IOptions<MvcJsonOptions>>().Value.SerializerSettings,
serviceProvider.GetRequiredService<ArrayPool<char>>(),
serviceProvider.GetRequiredService<ObjectPoolProvider>(),
o,
serviceProvider.GetRequiredService<IOptions<MvcJsonOptions>>().Value
);
o.InputFormatters.Insert(0, customJsonInputFormatter);
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
Actually, your problem is related to Data Binding, not to validation, that's why disabling the model validation did not help. You can implement custom Binder and configure it to manually bind your properties, e.g.:
public class PostModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
string valueFromBody = string.Empty;
using (var sr = new StreamReader(bindingContext.HttpContext.Request.Body))
{
valueFromBody = sr.ReadToEnd();
}
if (string.IsNullOrEmpty(valueFromBody))
{
return Task.CompletedTask;
}
string idString = Convert.ToString(((JValue)JObject.Parse(valueFromBody)["id"]).Value);
string categoryString = Convert.ToString(((JValue)JObject.Parse(valueFromBody)["category"]).Value);
if (string.IsNullOrEmpty(idString) || !int.TryParse(idString, out int id))
{
return Task.CompletedTask;
}
Category? category = null;
if(Enum.TryParse(categoryString, out Category parsedCategory))
{
category = parsedCategory;
}
bindingContext.Result = ModelBindingResult.Success(new PostModel()
{
Id = id,
Category = category
});
return Task.CompletedTask;
}
}
Then you can apply this binder to your class:
[ModelBinder(BinderType = typeof(PostModelBinder))]
public class PostModel
{
public int Id { get; set; }
public Category? Category { get; set; }
}
or to action:
[HttpPost]
public async Task<Response> Post([ModelBinder(BinderType = typeof(PostModelBinder))][FromBody]PostModel model)
=> this.Service.Execute(model);
or create CustomModelBinderProvider:
public class CustomModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(PostModel))
return new PostModelBinder();
return null;
}
}
and register it in ConfigureServices methods of Startup class:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddMvc(
config => config.ModelBinderProviders.Insert(0, new CustomModelBinderProvider())
);
...
}
No you can't since the property tied to an enum. if you really want to be what you posted then change the model to be
public class PostModel
{
public int Id { get; set; }
public string Category { get; set; }
}
Then in your endpoint parse the string to enum like
Enum.TryParse("All", out Category cat);

Throwing a exception on Constructor

I'm creating a new webapi pattern and i decided use a new pattern to make my consistency.
I'm trying to set my error messages on the constructor of my class object Pessoa
public class PessoaModel
{
public int PessoaId { get; set; }
public string PessoaNome { get; set; }
public string PessoaNomeFantasia { get; set; }
public string PessoaCnpjCpf { get; set; }
public string PessoaEmail { get; set; }
PessoaModel()
{
if (PessoaNome == null)
throw new Exception("Preencha Nome");
if (PessoaEmail == null)
throw new Exception("Preencha Email");
if (PessoaCnpjCpf == null)
throw new Exception("Preencha Cpf ou Cnpj");
}
}
Then the Exception happens but the Controllers continues running
[HttpPost]
[Route("Pessoa")]
public IHttpActionResult Post(PessoaModel pessoa)
{
if (_pessoa.Insert(pessoa))
return Ok();
return BadRequest("Pessoa não inserida");
}
Someone know how this work or if have a better way to do this?
What you are doing doesn't make sense. You are forcing properties to have a value, where you have done nothing to give them a value. At that time, they can only be set from the constructor.
Since you are using MVC/Web API, I would consider to use data annotations, to enforce the model to have the correct values.
public class PessoaModel
{
[Required(ErrorMessage = "ID is required.")]
public int PessoaId { get; set; }
}
In your action:
if (!this.ModelState.IsValid)
{
return RedirectToAction("Error"); // give an error, do something else
}

ASP.NET Core MVC Mixed Route/FromBody Model Binding & Validation

I am using ASP.NET Core 1.1 MVC to build an JSON API. Given the following model and action method:
public class TestModel
{
public int Id { get; set; }
[Range(100, 999)]
public int RootId { get; set; }
[Required, MaxLength(200)]
public string Name { get; set; }
public string Description { get; set; }
}
[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho([FromBody] TestModel data)
{
return Json(new
{
data.Id,
data.RootId,
data.Name,
data.Description,
Errors = ModelState.IsValid ? null : ModelState.SelectMany(x => x.Value.Errors)
});
}
The [FromBody] on my action method parameter is causing the model to be bound from the JSON payload that is posted to the endpoint, however it also prevents the Id and RootId properties from being bound via the route parameters.
I could break this up into to separate models, one bound from the route and one from the body or I could also force any clients to send the id & rootId as part of the payload, but both of those solutions seem to complicate things more than I'd like and don't allow me to keep the validation logic in a single place. Is there any way to get this situation working where the model can be bound properly and I can keep my model & validation logic together?
After researching I came up with a solution of creating new model binder + binding source + attribute which combines functionality of BodyModelBinder and ComplexTypeModelBinder. It firstly uses BodyModelBinder to read from body and then ComplexModelBinder fills other fields. Code here:
public class BodyAndRouteBindingSource : BindingSource
{
public static readonly BindingSource BodyAndRoute = new BodyAndRouteBindingSource(
"BodyAndRoute",
"BodyAndRoute",
true,
true
);
public BodyAndRouteBindingSource(string id, string displayName, bool isGreedy, bool isFromRequest) : base(id, displayName, isGreedy, isFromRequest)
{
}
public override bool CanAcceptDataFrom(BindingSource bindingSource)
{
return bindingSource == Body || bindingSource == this;
}
}
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromBodyAndRouteAttribute : Attribute, IBindingSourceMetadata
{
public BindingSource BindingSource => BodyAndRouteBindingSource.BodyAndRoute;
}
public class BodyAndRouteModelBinder : IModelBinder
{
private readonly IModelBinder _bodyBinder;
private readonly IModelBinder _complexBinder;
public BodyAndRouteModelBinder(IModelBinder bodyBinder, IModelBinder complexBinder)
{
_bodyBinder = bodyBinder;
_complexBinder = complexBinder;
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
await _bodyBinder.BindModelAsync(bindingContext);
if (bindingContext.Result.IsModelSet)
{
bindingContext.Model = bindingContext.Result.Model;
}
await _complexBinder.BindModelAsync(bindingContext);
}
}
public class BodyAndRouteModelBinderProvider : IModelBinderProvider
{
private BodyModelBinderProvider _bodyModelBinderProvider;
private ComplexTypeModelBinderProvider _complexTypeModelBinderProvider;
public BodyAndRouteModelBinderProvider(BodyModelBinderProvider bodyModelBinderProvider, ComplexTypeModelBinderProvider complexTypeModelBinderProvider)
{
_bodyModelBinderProvider = bodyModelBinderProvider;
_complexTypeModelBinderProvider = complexTypeModelBinderProvider;
}
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
var bodyBinder = _bodyModelBinderProvider.GetBinder(context);
var complexBinder = _complexTypeModelBinderProvider.GetBinder(context);
if (context.BindingInfo.BindingSource != null
&& context.BindingInfo.BindingSource.CanAcceptDataFrom(BodyAndRouteBindingSource.BodyAndRoute))
{
return new BodyAndRouteModelBinder(bodyBinder, complexBinder);
}
else
{
return null;
}
}
}
public static class BodyAndRouteModelBinderProviderSetup
{
public static void InsertBodyAndRouteBinding(this IList<IModelBinderProvider> providers)
{
var bodyProvider = providers.Single(provider => provider.GetType() == typeof(BodyModelBinderProvider)) as BodyModelBinderProvider;
var complexProvider = providers.Single(provider => provider.GetType() == typeof(ComplexTypeModelBinderProvider)) as ComplexTypeModelBinderProvider;
var bodyAndRouteProvider = new BodyAndRouteModelBinderProvider(bodyProvider, complexProvider);
providers.Insert(0, bodyAndRouteProvider);
}
}
Install-Package HybridModelBinding
Add to Statrup:
services.AddMvc()
.AddHybridModelBinder();
Model:
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public string FavoriteColor { get; set; }
}
Controller:
[HttpPost]
[Route("people/{id}")]
public IActionResult Post([FromHybrid]Person model)
{ }
Request:
curl -X POST -H "Accept: application/json" -H "Content-Type:application/json" -d '{
"id": 999,
"name": "Bill Boga",
"favoriteColor": "Blue"
}' "https://localhost/people/123?name=William%20Boga"
Result:
{
"Id": 123,
"Name": "William Boga",
"FavoriteColor": "Blue"
}
There are other advanced features.
You can remove the [FromBody] decorator on your input and let MVC binding map the properties:
[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho(TestModel data)
{
return Json(new
{
data.Id,
data.RootId,
data.Name,
data.Description,
Errors = ModelState.IsValid ? null : ModelState.SelectMany(x => x.Value.Errors)
});
}
More info:
Model binding in ASP.NET Core MVC
UPDATE
Testing
UPDATE 2
#heavyd, you are right in that JSON data requires [FromBody] attribute to bind your model. So what I said above will work on form data but not with JSON data.
As alternative, you can create a custom model binder that binds the Id and RootId properties from the url, whilst it binds the rest of the properties from the request body.
public class TestModelBinder : IModelBinder
{
private BodyModelBinder defaultBinder;
public TestModelBinder(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory) // : base(formatters, readerFactory)
{
defaultBinder = new BodyModelBinder(formatters, readerFactory);
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
// callinng the default body binder
await defaultBinder.BindModelAsync(bindingContext);
if (bindingContext.Result.IsModelSet)
{
var data = bindingContext.Result.Model as TestModel;
if (data != null)
{
var value = bindingContext.ValueProvider.GetValue("Id").FirstValue;
int intValue = 0;
if (int.TryParse(value, out intValue))
{
// Override the Id property
data.Id = intValue;
}
value = bindingContext.ValueProvider.GetValue("RootId").FirstValue;
if (int.TryParse(value, out intValue))
{
// Override the RootId property
data.RootId = intValue;
}
bindingContext.Result = ModelBindingResult.Success(data);
}
}
}
}
Create a binder provider:
public class TestModelBinderProvider : IModelBinderProvider
{
private readonly IList<IInputFormatter> formatters;
private readonly IHttpRequestStreamReaderFactory readerFactory;
public TestModelBinderProvider(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory)
{
this.formatters = formatters;
this.readerFactory = readerFactory;
}
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(TestModel))
return new TestModelBinder(formatters, readerFactory);
return null;
}
}
And tell MVC to use it:
services.AddMvc()
.AddMvcOptions(options =>
{
IHttpRequestStreamReaderFactory readerFactory = services.BuildServiceProvider().GetRequiredService<IHttpRequestStreamReaderFactory>();
options.ModelBinderProviders.Insert(0, new TestModelBinderProvider(options.InputFormatters, readerFactory));
});
Then your controller has:
[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho(TestModel data)
{...}
Testing
You can add an Id and RootId to your JSON but they will be ignored as we are overwriting them in our model binder.
UPDATE 3
The above allows you to use your data model annotations for validating Id and RootId. But I think it may confuse other developers who would look at your API code. I would suggest to just simplify the API signature to accept a different model to use with [FromBody] and separate the other two properties that come from the uri.
[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho(int id, int rootId, [FromBody]TestModelNameAndAddress testModelNameAndAddress)
And you could just write a validator for all your input, like:
// This would return a list of tuples of property and error message.
var errors = validator.Validate(id, rootId, testModelNameAndAddress);
if (errors.Count() > 0)
{
foreach (var error in errors)
{
ModelState.AddModelError(error.Property, error.Message);
}
}
I have not tried this for your example but it should work as asp.net core support model binding like this.
You can create model like this.
public class TestModel
{
[FromRoute]
public int Id { get; set; }
[FromRoute]
[Range(100, 999)]
public int RootId { get; set; }
[FromBody]
[Required, MaxLength(200)]
public string Name { get; set; }
[FromBody]
public string Description { get; set; }
}
Update 1: Above will not work in case when stream is not rewindable. Mainly in your case when you post json data.
Custom Model binder is solution but if you still don't want to create that one and just want to manage with Model then you can create two Model.
public class TestModel
{
[FromRoute]
public int Id { get; set; }
[FromRoute]
[Range(100, 999)]
public int RootId { get; set; }
[FromBody]
public ChildModel OtherData { get; set; }
}
public class ChildModel
{
[Required, MaxLength(200)]
public string Name { get; set; }
public string Description { get; set; }
}
Note : This works perfectly with application/json binding as it is working bit differently then other content-type.
What I ended up doing (translated to your case) was:
Model
public class TestModel
{
public int Id { get; set; }
[Range(100, 999)]
public int RootId { get; set; }
[Required, MaxLength(200)]
public string Name { get; set; }
public string Description { get; set; }
}
Controller
[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho(int rootId, int id, TestModel data)
{
data.RootId = rootId;
data.Id = id;
return Json(new
{
data.Id,
data.RootId,
data.Name,
data.Description,
Errors = ModelState.IsValid ? null : ModelState.SelectMany(x => x.Value.Errors)
});
}
It might not be the same signature on the controller method. It may not look as elegant as only having the model in the signature. It was - however - easy, as it doesn't require any external packages to be downloaded and only requires small changes to your controller method (one extra line and declared parameter per added route parameter).

ASP.NET MVC: Custom Validation by DataAnnotation

I have a Model with 4 properties which are of type string. I know you can validate the length of a single property by using the StringLength annotation. However I want to validate the length of the 4 properties combined.
What is the MVC way to do this with data annotation?
I'm asking this because I'm new to MVC and want to do it the correct way before making my own solution.
You could write a custom validation attribute:
public class CombinedMinLengthAttribute: ValidationAttribute
{
public CombinedMinLengthAttribute(int minLength, params string[] propertyNames)
{
this.PropertyNames = propertyNames;
this.MinLength = minLength;
}
public string[] PropertyNames { get; private set; }
public int MinLength { get; private set; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var properties = this.PropertyNames.Select(validationContext.ObjectType.GetProperty);
var values = properties.Select(p => p.GetValue(validationContext.ObjectInstance, null)).OfType<string>();
var totalLength = values.Sum(x => x.Length) + Convert.ToString(value).Length;
if (totalLength < this.MinLength)
{
return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
}
return null;
}
}
and then you might have a view model and decorate one of its properties with it:
public class MyViewModel
{
[CombinedMinLength(20, "Bar", "Baz", ErrorMessage = "The combined minimum length of the Foo, Bar and Baz properties should be longer than 20")]
public string Foo { get; set; }
public string Bar { get; set; }
public string Baz { get; set; }
}
Self validated model
Your model should implement an interface IValidatableObject. Put your validation code in Validate method:
public class MyModel : IValidatableObject
{
public string Title { get; set; }
public string Description { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Title == null)
yield return new ValidationResult("*", new [] { nameof(Title) });
if (Description == null)
yield return new ValidationResult("*", new [] { nameof(Description) });
}
}
Please notice: this is a server-side validation. It doesn't work on client-side. You validation will be performed only after form submission.
ExpressiveAnnotations gives you such a possibility:
[Required]
[AssertThat("Length(FieldA) + Length(FieldB) + Length(FieldC) + Length(FieldD) > 50")]
public string FieldA { get; set; }
To improve Darin's answer, it can be bit shorter:
public class UniqueFileName : ValidationAttribute
{
private readonly NewsService _newsService = new NewsService();
public override bool IsValid(object value)
{
if (value == null) { return false; }
var file = (HttpPostedFile) value;
return _newsService.IsFileNameUnique(file.FileName);
}
}
Model:
[UniqueFileName(ErrorMessage = "This file name is not unique.")]
Do note that an error message is required, otherwise the error will be empty.
Background:
Model validations are required for ensuring that the received data we receive is valid and correct so that we can do the further processing with this data. We can validate a model in an action method. The built-in validation attributes are Compare, Range, RegularExpression, Required, StringLength. However we may have scenarios wherein we required validation attributes other than the built-in ones.
Custom Validation Attributes
public class EmployeeModel
{
[Required]
[UniqueEmailAddress]
public string EmailAddress {get;set;}
public string FirstName {get;set;}
public string LastName {get;set;}
public int OrganizationId {get;set;}
}
To create a custom validation attribute, you will have to derive this class from ValidationAttribute.
public class UniqueEmailAddress : ValidationAttribute
{
private IEmployeeRepository _employeeRepository;
[Inject]
public IEmployeeRepository EmployeeRepository
{
get { return _employeeRepository; }
set
{
_employeeRepository = value;
}
}
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
var model = (EmployeeModel)validationContext.ObjectInstance;
if(model.Field1 == null){
return new ValidationResult("Field1 is null");
}
if(model.Field2 == null){
return new ValidationResult("Field2 is null");
}
if(model.Field3 == null){
return new ValidationResult("Field3 is null");
}
return ValidationResult.Success;
}
}
Hope this helps. Cheers !
References
Code Project - Custom Validation Attribute in ASP.NET MVC3
Haacked - ASP.NET MVC 2 Custom Validation
A bit late to answer, but for who is searching.
You can easily do this by using an extra property with the data annotation:
public string foo { get; set; }
public string bar { get; set; }
[MinLength(20, ErrorMessage = "too short")]
public string foobar
{
get
{
return foo + bar;
}
}
That's all that is too it really. If you really want to display in a specific place the validation error as well, you can add this in your view:
#Html.ValidationMessage("foobar", "your combined text is too short")
doing this in the view can come in handy if you want to do localization.
Hope this helps!

how can I bind a complex class to a view, while preserving my custom validation attributes?

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);
}
}

Categories

Resources