Fluent Validation in MVC: specify RuleSet for Client-Side validation
Asked Answered
A

3

6

In my ASP.NET MVC 4 project I have validator for one of my view models, that contain rules definition for RuleSets. Edit ruleset used in Post action, when all client validation passed. Url and Email rule sets rules used in Edit ruleset (you can see it below) and in special ajax actions that validate only Email and only Url accordingly.

My problem is that view doesn't know that it should use Edit rule set for client html attributes generation, and use default rule set, which is empty. How can I tell view to use Edit rule set for input attributes generation?

Model:

public class ShopInfoViewModel
{
    public long ShopId { get; set; }

    public string Name { get; set; }

    public string Url { get; set; }

    public string Description { get; set; }

    public string Email { get; set; }
}

Validator:

public class ShopInfoViewModelValidator : AbstractValidator<ShopInfoViewModel>
{
    public ShopInfoViewModelValidator()
    {
        var shopManagementService = ServiceLocator.Instance.GetService<IShopService>();

        RuleSet("Edit", () =>
        {
            RuleFor(x => x.Name)
                .NotEmpty().WithMessage("Enter name.")
                .Length(0, 255).WithMessage("Name length should not exceed 255 chars.");

            RuleFor(x => x.Description)
                .NotEmpty().WithMessage("Enter name.")
                .Length(0, 10000).WithMessage("Name length should not exceed 10000 chars.");

            ApplyUrlRule(shopManagementService);
            ApplyEmailRule(shopManagementService);
        });

        RuleSet("Url", () => ApplyUrlRule(shopManagementService));
        RuleSet("Email", () => ApplyEmailRule(shopManagementService));
    }

    private void ApplyUrlRule(IShopService shopService)
    {
        RuleFor(x => x.Url)
            .NotEmpty().WithMessage("Enter url.")
            .Length(4, 30).WithMessage("Length between 4 and 30 chars.")
            .Matches(@"[a-z\-\d]").WithMessage("Incorrect format.")
            .Must((model, url) => shopService.Available(url, model.ShopId)).WithMessage("Shop with this url already exists.");
    }

    private void ApplyEmailRule(IShopService shopService)
    {
        // similar to url rule: not empty, length, regex and must check for unique
    }
}

Validation action example:

 public ActionResult ValidateShopInfoUrl([CustomizeValidator(RuleSet = "Url")]
        ShopInfoViewModel infoViewModel)
 {
     return Validation(ModelState);
 }

Get and Post actions for ShopInfoViewModel:

[HttpGet]
public ActionResult ShopInfo()
{
    var viewModel = OwnedShop.ToViewModel();
    return PartialView("_ShopInfo", viewModel);
}

[HttpPost]
public ActionResult ShopInfo(CustomizeValidator(RuleSet = "Edit")]ShopInfoViewModel infoViewModel)
    {
        var success = false;

        if (ModelState.IsValid)
        {
            // save logic goes here
        }
    }

View contains next code:

@{
    Html.EnableClientValidation(true);
    Html.EnableUnobtrusiveJavaScript(true);
}
<form class="master-form" action="@Url.RouteUrl(ManagementRoutes.ShopInfo)" method="POST" id="masterforminfo">
    @Html.TextBoxFor(x => x.Name)
    @Html.TextBoxFor(x => x.Url, new { validationUrl = Url.RouteUrl(ManagementRoutes.ValidateShopInfoUrl) })
    @Html.TextAreaFor(x => x.Description)
    @Html.TextBoxFor(x => x.Email, new { validationUrl = Url.RouteUrl(ManagementRoutes.ValidateShopInfoEmail) })
    <input type="submit" name="asdfasfd" value="Сохранить" style="display: none">
</form>

Result html input (without any client validation attributes):

<input name="Name" type="text" value="Super Shop"/> 
Annulate answered 13/11, 2014 at 12:11 Comment(0)
A
8

After digging in FluentValidation sources I found solution. To tell view that you want to use specific ruleset, decorate your action, that returns view, with RuleSetForClientSideMessagesAttribute:

[HttpGet]
[RuleSetForClientSideMessages("Edit")]
public ActionResult ShopInfo()
{
    var viewModel = OwnedShop.ToViewModel();
    return PartialView("_ShopInfo", viewModel);
}

If you need to specify more than one ruleset — use another constructor overload and separate rulesets with commas:

[RuleSetForClientSideMessages("Edit", "Email", "Url")]
public ActionResult ShopInfo()
{
    var viewModel = OwnedShop.ToViewModel();
    return PartialView("_ShopInfo", viewModel);
}

If you need to decide about which ruleset would be used directly in action — you can hack FluentValidation by putting array in HttpContext next way (RuleSetForClientSideMessagesAttribute currently is not designed to be overriden):

public ActionResult ShopInfo(validateOnlyEmail)
{
    var emailRuleSet = new[]{"Email"};
    var allRuleSet = new[]{"Edit", "Url", "Email"};

    var actualRuleSet = validateOnlyEmail ? emailRuleSet : allRuleSet;
    HttpContext.Items["_FV_ClientSideRuleSet"] = actualRuleSet;

    return PartialView("_ShopInfo", viewModel);
}

Unfortunately, there are no info about this attribute in official documentation.

UPDATE

In newest version we have special extension method for dynamic ruleset setting, that you should use inside your action method or inside OnActionExecuting/OnActionExecuted/OnResultExecuting override methods of controller:

ControllerContext.SetRulesetForClientsideMessages("Edit", "Email");

Or inside custom ActionFilter/ResultFilter:

public class MyFilter: ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        ((Controller)context.Controller).ControllerContext.SetRulesetForClientsideMessages("Edit", "Email");
        //same syntax for OnActionExecuted/OnResultExecuting
    }
}
Annulate answered 8/4, 2015 at 22:38 Comment(2)
The official documentation is a Wiki. Please add it there yourself.Problematic
One thing of note that tripped me up on a couple of refactorings is that the Items collection should be populated with type new string[], NOT stringSpiritualism
S
0

Adding to this as the library has been updated to account for this situation...

As of 7.4.0, it's possible to dynamically select one or multiple rule sets based on your specific conditions;

ControllerContext.SetRulesetForClientsideMessages("ruleset1", "ruleset2" /*...etc*);
Spiritualism answered 31/1, 2018 at 9:28 Comment(0)
T
0

Documentation on this can be found in the latest FluentValidation site: https://fluentvalidation.net/aspnet#asp-net-mvc-5

Adding the CustomizeValidator attribute to the action will apply the ruleset within the pipeline when the validator is being initialized and the model is being automatically validated.

   public ActionResult Save([CustomizeValidator(RuleSet="MyRuleset")] Customer cust) {
   // ...
   }
Thy answered 18/10, 2018 at 3:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.