Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Injecting ModelState into Service

I am trying to inject the ModelState of the controller into my Service layer to add business logic related validation errors (e.g. entry already exists).

For this I created a new IValidationDictionary that gets injected into my service through an Initialize() function. Nothing new at all and judging by my google searches something quite some people do in MVC.

The Controller constructor looks like this:

public AccountController(IAccountService accountService)
{
   _accountService = accountService;
   _accountService.Initialize(new ValidationDictionary(ModelState));
}

This all works fine and I can add errors in my service. The issue comes when leaving the service again. At that point none of my errors are present in the controller ModelState. After some debugging I found out that the ModelState in the constructor of the controller is not the same as in the Action. At some point it seems to create a new ModelState.

One alternative seems to be to call the Initialize() to inject the ModelState at start of every Action. Before I do that, I wanted to ask if anyone has a more elegant way (as in less to type) of solving this.

Edit: The IValidationDictionary: On buisness layer:

public interface IValidationDictionary
    {
        void AddError(string key, string message);
        bool IsValid { get; }
    }

In controller:

public class ValidationDictionary : IValidationDictionary
    {
        private ModelStateDictionary _modelState;

        public ValidationDictionary(ModelStateDictionary modelState)
        {
            _modelState = modelState;
        }

        public bool IsValid
        {
            get
            {
                return _modelState.IsValid;
            }
        }

        public void AddError(string key, string message)
        {
            _modelState.AddModelError(key, message);
        }
    }
like image 628
Mats391 Avatar asked Nov 04 '16 15:11

Mats391


People also ask

What is the point of checking ModelState?

The ModelState has two purposes: to store the value submitted to the server, and to store the validation errors associated with those values.

Why is ModelState not valid MVC?

The error typically means that your Model doesn't meet the requirements to be validated. In your example, your FirstName , LastName , Email , Subject and Message properties are decorated with the [Required] attribute. This means that those values shouldn't be null and empty otherwise your condition if(ModelState.

What does ModelState remove do?

Remove(KeyValuePair<String,ModelState>) Removes the first occurrence of the specified object from the model-state dictionary.

What is ModelState Clear () in MVC?

Clear() is required to display back your model object. If you are getting your Model from a form and you want to manipulate the data that came from the client form and write it back to a view, you need to call ModelState. Clear() to clean the ModelState values.


2 Answers

First and foremost, you shouldn't do that because you will be mixing two things and break separation of concerns and tightly-couple your application.

Tightly coupling

ModelState property is of type ModelStateDictioanry which is a ASP.NET Core specific class. If you use it in your business layer, you create a dependency on ASP.NET Core, making it impossible to reuse your logic anywhere outside of ASP.NET Core, i.e. Background Worker process which is a pure console application because you neither reference ASP.NET Core nor you'll have HttpContext or anything else there.

Separation of concerns

Business validation and input validation are two different things and should be handled differently. ModelStateDictionary is used for input validation, to validate the input passed to your controller. It is not meant to validate business logic or anything like that!

Business validation on the other side is more than just a rough validation of the fields and its patterns. It contains logic and validation may be complex and depend on multiple properties/values as well as of the state of the current object. So for example, values that may pass the input validation may fail in business validation.

So by using both together, you will violate separation of concerns and have a class do more than one thing. This is bad for maintaining code in the long run.

How to work around it?

Convert IDicitionary<string, ModelStateEntry> to a custom validation model/ValidationResult

ValidationResult is defined in System.Components.DataAnnotations` assembly which is not tied to ASP.NET Core but is port of .NET Core/full .NET Framework, so you don't get a dependency on ASP.NET Core and can reuse it in console applications etc. and pass it around in your validation service using i.e. a factory class

public interface IAccoutServiceFactory
{
    IAccountService Create(List<ValidationResult> validationResults);
}

// in controller
List<ValidationResult> validationResults = ConvertToValidationResults(ModelState);
IAccountService accountService = accountServiceFactory.Create(

This solves the issue of dependencies, but you still violate separation of concerns, especially if you use the same model in your business layer as you use as controller parameter.

Custom validatior (completely separated)

Its bit more work at the beginning, but your validation will be completely independent and you can change one of it w/o affecting the other one.

For it you can use frameworks like Fluent Validations which may make the validation a bit easier and more managed

You write a custom validator, that will validate the logic of a certain class/model.

The custom validator can be from as simple as writing your own validators per model which may implement such an interface

public interface IValidator<T> where T : class
{
    bool TryValidate(T model, out List<ValidationErrorModel> validationResults);
    List<ValidationErrorModel> Validate(T model);
}

and wrap this around your validator class

public class ModelValidator : IModelValidator
{
    public List<ValidationErrorModel> Validate<T>(T model) 
    {
        // provider is a IServiceProvider
        var validator = provider.RequireService(typeof(IValidator<T>));
        return validator.Validate(model);
    }
}

Custom validatior (validation attribute based)

An alternation of the above, but using the validation attributes as base and custom logic. You can use Validator.TryValidateObject from System.ComponentModel.DataAnnotations to validate a models ValidatorAttributes. Be aware though, that it only will validate the passed models attributes and not of child models.

List<ValidationResult> results = new List<ValidationResult>();
var validationContext = new ValidationContext(model);

if(!Validator.TryValidateObject(model, validateContext, results))
{
    // validation failed
}

and then additionally perform custom logic. See this blog post on how to implement child model validation.

Imho the cleanest way to do is a custom validator, as its both separated and decoupled and easily allows you to change logic of a model w/o affecting the validation of other models.

If you are only validating messages (i.e. commands/queries in CQRS) you can use the second approach with the validation attributes.

like image 167
Tseng Avatar answered Nov 01 '22 06:11

Tseng


Thanks to user Ruard, here is how I did it with .NET Core 5.0

In Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    ...
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
    ...
}

In Controller

AccountService _service;

public AccountController(IActionContextAccessor actionContextAccessor)
{
      //this.actionContextAccessor = actionContextAccessor;
      _service = new AccountService(actionContextAccessor);
 }

In Service Layer class

public class AccountService
    {    
        private readonly IActionContextAccessor _actionContextAccessor;
        public AccountService(IActionContextAccessor actionContextAccessor)
        {
            _actionContextAccessor = actionContextAccessor;
        }

        public void Login(string emailAddress, string password)
        {  
            _actionContextAccessor.ActionContext.ModelState.AddModelError("Email", "Your error message");
        }
    }

In Action, you use like

   _service.Login(model.Email, model.Password);

  if(!ModelState.IsValid)
      return View(model);
like image 42
Nathan Avatar answered Nov 01 '22 08:11

Nathan