How can I pass service layer validation messages back to the caller?
Asked Answered
D

4

10

I've done alot of research, including here on SO, and I can't seem to find clear direction. I currently have an ASP.NET MVC3 application, with a service layer that sits on top of a repository.

In my service layer, I have functions such as:

public class MyService{

    public void CreateDebitRequest(int userId, int cardId, decimal Amount, .... )
    {
    //perform some sort of validation on parameters, save to database
    }

    public void CreateCreditRequest(.....)
    }
        //perform some sort of validation on parameters, save to database
    }

    public void CreateBatchFile()
    {
        //construct a file using a semi-complex process which could fail
        //write the file to the server, which could fail
    }


    public PaymentTransaction ChargePaymentCard(int paymentCardId, decimal amount)
    {
        //validate customer is eligible for amount, call 3rd party payments api call,
        //...save to database, other potential failures, etc.
    }

}

I've seen people say that parameter validation isn't very exceptional, and so throwing an exception is not very fitting. I also don't love the idea of passing in an out paramater, such as a string, and checking for an empty value. I've considered implementing a ValidationDictionary class, and making it a property of any given service class (it would contain an IsValid boolean, and a List of error messages, and could be checked after any given function call in the service layer to see how things went). I could check the ValidationDictionary status after running any given function:

var svc = new MyService();
svc.CreateBatchFile();
if (svc.ValidationDictionary.IsValid)
    //proceed
else
   //display values from svc.ValidationDictionary.Messages...

The thing I don't like about this is that I would have to update it for every service layer function call, to avoid having it retain old values (if I chose not to use it for many or most functions, one would still expect it to have a meaningful or null value after running any given function). Another thing I've considered is passing in the ValidationDictionary for each function call that might have detailed validation information, but then I am back to using an out parameter...

Do any of you have recommendations? I can't seem to figure out any clean way of doing this. Sometimes returning null for a function is enough information, but sometimes I'd like a little more validation information passed back to the caller. Any advice would be appreciated!

Edit to clarify: My service layer is not aware that it is an MVC application that is consuming it. The service layer just has certain public functions such as CreateBatchFile() or AddDebitRequest(). Sometimes returning null is enough for the consumer (in this case a controller, but could be something else) to know what happened, and sometimes the consumer would like some more information from the service layer (maybe to pass along to ModelState if the consumer is a controller). How do I bubble this up from the service layer itself?

Disclaimer answered 11/4, 2012 at 16:14 Comment(0)
S
10

This is what I do. Have a class for your validation, and instead of passing parameters pass a view model. So in your case something like this, where ValidationResult is just a simple class w/ MemberName and ErrorMessage properties:

public class DebitRequestValidator{

  public IEnumerable<ValidationResult> Validate(DebitRequestModel model){

    //do some validation
    yield return new ValidationResult {
      MemberName = "cardId",
      ErrorMessage = "Invalid CardId."
    }
  }  

}

Then create a controller extension method to copy these validation results to the model state.

public static class ControllerExtensions
{
    public static void AddModelErrors(this ModelStateDictionary modelState, IEnumerable<ValidationResult> validationResults)
    {
        if (validationResults == null) return;

        foreach (var validationResult in validationResults)
        {
            modelState.AddModelError(validationResult.MemberName, validationResult.ErrorMessage);
        }
    }
}

Then in your controller do something like

[HttpPost]
public ActionResult DebitRequest(DebitRequestModel model) {
  var validator = new DebitRequestValidator();
  var results = validator.Validate(model);
  ModelState.AddModelErrors(results);
  if (!ModelState.IsValid)
    return View(model)

  //else do other stuff here
}

Then in your view you can display errors like normal.

@Html.ValidationMessageFor(m => m.CardId)
Snowbird answered 11/4, 2012 at 16:43 Comment(10)
My service layer is not aware that it is an MVC application that is consuming it. It just has certain public functions such as CreateBatchFile or AddDebitRequest. Sometimes returning null is enough for the controller to know what happened, and sometimes the controller would like some more information from the service layer (maybe to pass along to ModelState and whatnot). How do I bubble this up from the service layer itself?Disclaimer
@Disclaimer - The service layer in this example has no dependency on MVC, only the ValidationResult class, which is just a regular class I created. You could do the same thing by passing in ModelState, but I think returning an IEnumerable<ValidationResult> is a better way because it would be easier to test.Snowbird
Where would I return this IEnumberable<ValidationResult>? I understand that validation should be done on some level before I get to the service function call, but what happens if errors happen within that actual function in the service layer (for example within svc.CreateBatchFile())? Would I have to pass in an empty IEnumerable to the service function as an out param, and then check it after calling my service function? Ex: svc.CreateBatchFile(out validationResults)? ThanksDisclaimer
@Disclaimer - You can return the validationresults from the service functions, no need for out parameters. Then the controller extension (the ModelState.AddModelErrors stuff) will take care of adding it to the model state if there were any errors.Snowbird
In my case I have items like ChargePaymentCard(), which returns a PaymentTransaction, or CreateUser, which returns User. If I return the ValidationResults, then it seems I lose my ability to return the relevant item to the function...?Disclaimer
In the code in the answer above, the validation is performed from the controller (by a call to DebitRequestValidator) and not in the service layer, so there is no need to return the ValidationResults from the service layer methods.Sundberg
If you must perform the validation in the service layer, wrap the ModelStateDictionary in an interface and provide a default implementation (with a ModelStateDictionary). Send the interface to every service method (interface is used, no dependency on MVC in service layer). Because it is a reference type, it will work as both input and output parameter, ie it can be modified by the service layer by adding errors. This way the service layer can still return another object like PaymentTransaction.Sundberg
@Sundberg - Thanks for your comments. Not to keep beating a dead horse, but if I did end up validating in the service layer in certain circumstances, would it be better to pass this dictionary into the service constructor, and have it be accessible via a property of the service class? Then I wouldn't have to instantiate and pass it in for every function call. Or is it best to pass it into the function itself?Disclaimer
@Disclaimer - If you're using a IoC container you'll have hard time passing it in the constructor, so you would probably want to pass it into the function. Although I still think returning the list of validation results would be easier unit test wise.Snowbird
It is possible to pass the dictionary in the service constructor BUT it requires that the service is "newed" for every web request. This is possible to do with an IoC container and the lifestyle for the service has to be set to "transient" or "per web request". The lifestyle of the dictionary has to be set to transient, you will need a new empty instance for each web request. If you must do validation in the service layer, I would have used the ModelState property of the controller and passed it with every call to the service layer.Sundberg
H
1

I used a system where it was passing an array of messages (or collection of classes), each element had codes, descriptions, friendly messages. We used to simply check if anything was there. It worked great between UI and another "service" layer, all exception were caught nicely, they were translated into these validation rules...just an idea

Heal answered 11/4, 2012 at 16:20 Comment(0)
S
1

Use ViewModel objects that are passed between the Views and the Controller action methods. The ViewModel objects can handle Validation by a Validate(ValidationDictionary validationDictionary) method.

The controller will have to call the Validate method on ViewModel object before calling any method in the service layer. This should only be necessary for http POST actions.

Your views will then have to display validation messages.

This solution requires that the viewmodel objects are passed between the controller action and the view, but nowadays that is mostly handled by the ModelBinder in MVC.

Your controller (http post) actions will look something like this:

[HttpPost]
public ActionResult Foo(BarViewModel viewModel)
{
    viewModel.Validate(ValidationDictionary);

    if (!ModelState.IsValid)
    {
        return View(viewModel);
    }

    // Calls to servicelayer
}

Your Validate method in your ViewModel will look like this:

public void Validate(ValidationDictionary validationDictionary)
{
    if (SomeProperty.Length > 30)
    {
        validationDictionary.AddError("SomeProperty", "Max length is 30 chars");
    }
}
Sundberg answered 11/4, 2012 at 16:27 Comment(6)
Hmmm...I think I am talking about a lower level. We'll assume that the user has pressed a button, and the viewmodel validation has gone off without a hitch. My controller will then call a function within the service layer, and that function might fail for a variety of reasons (or maybe the viewmodel validation was more generous than the service actually requires, so when the service validates the params for itself, it fails). How would I pass this information from the service layer back up to the controller, to even get to the point where I could display with viewmodel or modelstate?Disclaimer
I make sure that all validation logic exists in the viewmodel. Why do you want the validation logic spread out all over the place? Input validation belongs in the user interface module. If something is wrong in the service layer you should throw exception, to provide logging with better error messages. Your MVC application can then redirect to friendly error pages based on exception / http code.Sundberg
Let's say I'm in the service layer, and validating that the date passed into CreateDebitRequest(...) is not more than three days in the future. It seems that I should be validating this here in the service layer, in addition to wherever I validated it higher in the chain. Would you agree with that? If so, then do you think that I actually should be performing validation in my service layer, but just throwing exceptions when validation fails? Or should I not be validating this date in my service layer at all (I would be nervous about relying on the service consumer)? Thanks...Disclaimer
I think you should do your user interface validation (like the date for CreateDebitRequest) in a level above the service layer. If you don't write the above layer yourself and don't trust that layer I think your service layer should throw exception. Is the service layer a web service layer that the MVC application is using? In that case the webservice layer on top of the service layer should do the input validation and throw exceptions accordingly with appropiate http codes and messages.Sundberg
Interesting. Nope, no web service at this point. So you are essentially saying that anything invalid that enters the service layer is exception-worthy (sounds reasonable, but just want to be clear)? It's the duty of the consumer to try not to throw exceptions by validating data before it goes in, but if the consumer is irresponsible, then the service layer throws an exception as a last resort. Does that sound accurate? It is appealing in its cleanliness.Disclaimer
Exactly! :-) And because the caller and service layer resides in the same application, an exception will only be thrown when you have a bug in your validation logic in the caller.Sundberg
L
1

If you're just doing ViewModel Validation, FluentValidation is an excellent library.

If you're wanting to include business validation as feedback to the user, you could use the adapter pattern, it'll give you what you want.

Create an interface (IValidationDictionary or something similar). This interface would define an AddError method and would be passed to your service in order to add error messages.

public interface IValidationDictionary
{
    void AddError(string key, string errorMessage);
}

Create a ModelStateAdapter for your mvc application.

public class ModelStateAdapter : IValidationDictionary
{
    private ModelStateDictionary _modelState;

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

    public void AddError(string key, string errorMessage)
    {
        _modelState.AddModelError(key, errorMessage);
    }
}

Your service calls that need validation would require the IValidationDictionary

    public class MyService
    {        
        public void CreateDebitRequest(int userId, int cardId, decimal Amount, .... , IValidationDictionary validationDictionary)
          {
                if(userId == 0)
                    validationDictionary.AddError("UserId", "UserId cannot be 0");
          }
     }

You would then have a dependency on IValidationDictionary but not on MVC which would also make your solution testable.

If you needed to implement the services in an app that didn't have a ModelStateDictionary, you would just implement the IValidationDictionary interface on a class used for holding your errors.

Controller example:

public ActionResult Test(ViewModel viewModel)
{
    var modelStateAdapter = new ModelStateAdapter(ModelState);
    _serviceName.CreateDebitRequest(viewModel.UserId, viewModel.CardId, ... , modelStateAdapter);

    if(ModelState.IsValid)
        return View("Success")

    return View(viewModel);
}

Pro's of this approach:

  • No dependency on the calling libraries
  • It's possible to mock the IValidationDictionary for tests.

Con's of this approach:

  • You need to pass IValidationDictionary to every method that you want to do validation on that's going to be returned to the user.

    Or

    you need to initialise the service's validation dictionary (if you decide to have IValidationDictionary as a private field), in each controller action you want to validate against.

Linkoski answered 9/5, 2014 at 6:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.