简体   繁体   English

.NET WebAPI的服务器端验证

[英]Server Side Validation with .NET WebAPI

For arguments sake let's say that I am on the Create View. 为了争辩起见,假设我在“创建视图”上。 If I left all of the textboxes empty and hit submit, I would get returned the same form but with validation messages under each textbox that was required, and that was done by client side validation. 如果我将所有文本框都留空,然后单击Submit,则将返回相同的表单,但在每个所需的文本框下均包含验证消息,这是由客户端验证完成的。 Now, when that happens, each textbox is decorated with a class name called input-validation-error , and if I style that I can make the box turn red to make it stand out more to the user. 现在,当发生这种情况时,每个文本框都装饰有一个名为input-validation-error的类名称,如果我设置样式,则可以使该框变为红色以使其对用户更加突出。

But now, let's say that one of the textboxes requires an email address. 但是现在,假设其中一个文本框需要一个电子邮件地址。 Email addresses are unique so in my webapi controller I have this: 电子邮件地址是唯一的,因此在我的webapi控制器中,我有以下地址:

// POST: api/ControllerName
[ResponseType(typeof(TestObject))]
public IHttpActionResult PostTestObject(TestObject testObject)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    if (
        db.TestObjects.Any(
            x =>
                x.Email.Equals(testObject.Email, StringComparison.CurrentCultureIgnoreCase) &&
                x.ID != testObject.ID))
    {
            ModelState.AddModelError("Email", "This Email Already Exists!");
            return BadRequest(ModelState);
    }

    db.TestObjects.Add(testObject);
    db.SaveChanges();

    return CreatedAtRoute("DefaultApi", new { id = testObject.ID }, testObject);
}

In my Create View I have this to display that exception message: 在我的创建视图中,我可以显示异常消息:

.error(function (jqXHR, textStatus, errorThrown) {
    var status = capitalizeFirstLetter(textStatus);
    var error = $.parseJSON(jqXHR.responseText);
    toastr.error(status + " - " + error.exceptionMessage);
 });

This displays the exception message in a toastr notification. 这将在烤面包机通知中显示异常消息。 But it doesn't give the email textbox a class name of input-validation-error , and I would like it to so that it will display the textbox in red. 但这并没有给电子邮件文本框提供input-validation-error的类名,我希望它具有红色的文本框。

Is there a way in WebApi controller methods to return something that will add that class to that textbox? WebApi控制器方法中是否有一种方法可以返回将该类添加到该文本框的内容? I know in regular .Net controllers I could do 我知道在常规.Net控制器中我可以做

ModelState.AddModelError("Email", "This email already exists!")
return View(testObject);

That would return the view with that textbox having the css class name. 那将返回带有css类名的文本框的视图。

Any help is appreciated. 任何帮助表示赞赏。

Based on Nkosi's answer below: 根据Nkosi的以下回答:

When I console.log(JSON.stringify(error)); 当我console.log(JSON.stringify(error));

The response is this: 响应是这样的:

{"$id":"1","message":"The request is invalid.","modelState":
{"$id":"2","email":["This Email Already Exists!"]}} 

Okay, so I have changed the formatting to fit the JSON response, and I have also changed the var id line to var id = "#" + key.replace('$', ''); 好的,因此我更改了格式以适合JSON响应,并且还将var id行更改为var id = "#" + key.replace('$', '');

Now I am receiving an error on valmsg.text(value.join()); 现在我在valmsg.text(value.join());上收到错误 saying Object doesn't support property or method 'join' .. so I consoled the value and it is 2 .. not "This Email Already Exists!" 说“ Object doesn't support property or method 'join' ..因此,我控制了该值,它是2 ..不是"This Email Already Exists!"

UPDATE 更新

.error(function (jqXHR, textStatus, errorThrown) {
    var error = jqXHR.responseJSON;
    console.log(JSON.stringify(error));
    var message = error.message;
    var modelState = error.modelState;

    $.each(modelState,
        function (key, value) {
            var id = "#" + key.replace('$', '');
            var input = $(id);
            console.log(id); // result is #id
            if (input) { // if element exists
                input.addClass('input-validation-error');
            }
            //get validation message
            var valmsg = $("[data-valmsg-for='" + key + "']");
            if (valmsg) {
                valmsg.text(value.join()); // Object doesn't support property or method 'join'
                valmsg.removeClass("field-validation-valid");
                valmsg.addClass("field-validation-error");
            }

UPDATE 更新

based on this sample data 基于此样本数据

{"$id":"1","message":"The request is invalid.","modelState":
{"$id":"2","email":["This Email Already Exists!"]}} 

The snippet to highlight the invalid elements would become 突出显示无效元素的代码段将变为

var handleError = function (jqXHR, textStatus, errorThrown) {
    var error = jqXHR.responseJSON;        
    var message = error.message;
    var modelState = error.modelState;
    //highlight invalid fields                    
    $.each(modelState, function (key, value) {
        var id = "#" + key; //construct id
        var input = $(id); //get the element
        if(input) { //if element exists
            input.addClass('input-validation-error'); //update class
        }            
    });
}

Original 原版的

The following POC was used to demonstrate the original issue 以下POC用于演示原始问题

WebApi WebApi

[HttpGet]
[Route("testobject")]
public IHttpActionResult TestObject() {
    ModelState.AddModelError("Email", "This Email Already Exists!");
    return BadRequest(ModelState);
}

MVC Controller MVC控制器

[HttpGet, Route("")]
public ActionResult Index() {
    var model = new TestVM();
    return View(model);
}

MVC View: Index MVC视图:索引

@model TestVM
@{
    ViewBag.Title = "Index";
}
<div class="container">
    <div class="form-group">
        @Html.LabelFor(m => m.Email)
        @Html.TextBoxFor(model => model.Email, new { data_bind = "value: Email", @class = "form-control" })
        @Html.ValidationMessageFor(model => model.Email)
    </div>
    <button type="button" data-bind="click: testEmail" class="btn btn-success submit">Test</button>
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval", "~/bundles/knockout")
    <script type="text/javascript">
        //Pay no attention to this. custom strongly typed helper for routes
        var url = '@(Url.HttpRouteUrl<TestsApiController>(c => c.TestObject()))';
        $(function () {
            /*using knockout for binding*/
            function viewModel() {
                var self = this;
                //properties
                self.Email = ko.observable(@(Model.Email));
                //methods
                self.testEmail = function () {
                    $.ajax({
                        url: url,
                        type: 'Get',
                        contentType: 'application/json',
                        dataType: 'json',
                        success: handleResponse,
                        error: handleError,
                    });
                };

                var handleError = function (jqXHR, textStatus, errorThrown) {
                    var error = jqXHR.responseJSON;
                    console.log(JSON.stringify(error));
                    var message = error.Message;
                    var modelState = error.ModelState;
                    //highlight invalid fields                    
                    $.each(modelState, function (key, value) {
                        var id = "#" + key;
                        $(id).addClass('input-validation-error');
                        //get validation message
                        var valmsg = $("[data-valmsg-for='" + key + "']");
                        if (valmsg) {
                            valmsg.text(value.join());
                            valmsg.removeClass("field-validation-valid");
                            valmsg.addClass("field-validation-error");
                        }
                    });
                }

                var handleResponse = function (data) {
                    //No-op
                };
            }
            var vm = new viewModel();
            ko.applyBindings(vm);
        });

    </script>
}

Using the above proof of concept based on the original example in the question, the resulting model returned looked like this. 使用以上基于问题原始示例的概念验证,返回的结果模型如下所示。

{"Message":"The request is invalid.","ModelState":{"Email":["This Email Already Exists!"]}}

Focusing primarily on handling the error response returned I was able to achieve the desired behavior using the following structure. 我主要专注于处理返回的错误响应,因此我可以使用以下结构实现所需的行为。

var handleError = function (jqXHR, textStatus, errorThrown) {
    var error = jqXHR.responseJSON;
    console.log(JSON.stringify(error));
    //logs {"Message":"The request is invalid.","ModelState":{"Email":["This Email Already Exists!"]}}
    var message = error.Message;
    var modelState = error.ModelState;
    //highlight invalid fields                    
    $.each(modelState, function (key, value) {
        var id = "#" + key;
        $(id).addClass('input-validation-error');
        //get validation message
        var valmsg = $("[data-valmsg-for='" + key + "']");
        if (valmsg) {
            valmsg.text(value.join());
            valmsg.removeClass("field-validation-valid");
            valmsg.addClass("field-validation-error");
        }
    });
}

The above when executed resulted in 以上执行时导致

图片

From a view that had the following 从具有以下观点

<div class="container">
    <div class="form-group">
        @Html.LabelFor(m => m.Email)
        @Html.TextBoxFor(model => model.Email, new { data_bind = "value: Email", @class = "form-control" })
        @Html.ValidationMessageFor(model => model.Email)
    </div>
    <button type="button" data-bind="click: testEmail" class="btn btn-success submit">Test</button>
</div>

Did you try 你试过了吗

return BadRequest("This Email Already Exists!");

another version of BadRequest instead of throwing exception? BadRequest的另一个版本,而不是引发异常?

When your view/html calls the Web API method then Web API basically has no idea your page or input boxes even exist. 当视图/ html调用Web API方法时,Web API基本上不知道您的页面或输入框是否存在。 It purely gets some input and returns some output. 它纯粹获取一些输入并返回一些输出。 In this case because you've thrown an exception in Web API the http response is then an error code 500 for a "internal server error". 在这种情况下,由于在Web API中引发了异常,因此http响应就是“内部服务器错误”的错误代码500。 This would cause the xhr error handler to run. 这将导致xhr错误处理程序运行。 It's not ideal because of something else ever went wrong (database down, client connection dropped, etc) you will be displaying the email validation error. 这是不理想的,因为发生了其他错误(数据库关闭,客户端连接断开等),您将显示电子邮件验证错误。 A better idea would be to return a more informative response giving results of validation on each field, but then you need a response type with a bit more in it than a TestObject, something like a Result where result has some fields for validation errors. 更好的主意是返回更多信息的响应,以提供每个字段的验证结果,但是您需要一个比TestObject多一些的响应类型,类似于Result,其中result具有一些用于验证错误的字段。

As a quick workaround you will probably want to use some front end UI library to manually add that class to the field. 作为一种快速的解决方法,您可能需要使用一些前端UI库将该类手动添加到字段中。 Example in JQuery, just before/after your toastr line: 在JQuery中的示例,恰好在您的Toastr行之前/之后:

$('#emailfield').addClass('input-validation-error')

You should have to add input-validation-error class in your .error function to desired text box or control. 您应该在.error函数中将input-validation-error类添加到所需的文本框或控件。

register following script to validate 注册以下脚本进行验证

$(".selector").validate({
    highlight: function(element, errorClass) {
        // Override the default behavior here
    }
});

If you are returning in your controller: 如果要返回控制器,请执行以下操作:

ModelState.AddModelError("Email", "This Email Already Exists!");
return BadRequest(ModelState);

Then the json returned should be: 然后返回的json应该是:

{"Email":["This Email Already Exists!"]}

In the HTML output on your view, you should have an an input for which will have the name attribute set to Email: 在视图的HTML输出中,您应该有一个输入,其name属性设置为Email:

<input name="Email" type="text" />

Similarly, all other keys in the error JSON will have a matching form controls with the name attributes matching those keys. 同样,错误JSON中的所有其他键将具有匹配的表单控件,其名称属性与这些键匹配。

So in your error function, you can loop through the keys and apply the appropriate CSS class: 因此,在错误函数中,可以遍历键并应用适当的CSS类:

.error(function (jqXHR, textStatus, errorThrown) {
    var error = $.parseJSON(jqXHR.responseText);
    $(error).each(function(i,e)
    {
        $('[name="' + e + '"]').addClass('input-validation-error');
    });

 });

I would recommend you to use FluentValidation . 我建议您使用FluentValidation

Check this great article . 检查这篇很棒的文章

So using this approach you will be able to move your validation logic out from the controller and make error responses follow the pattern. 因此,使用这种方法,您可以将验证逻辑从控制器中移出,并使错误响应遵循该模式。

But in order to add css classes on the client side you will have to replace public List<string> Errors { get; set; } 但是,为了在客户端添加CSS类,您将不得不替换public List<string> Errors { get; set; } public List<string> Errors { get; set; } public List<string> Errors { get; set; } in ResponsePackeage with public Dictionary<string, string> Errors { get; set; } public List<string> Errors { get; set; }在带有public Dictionary<string, string> Errors { get; set; } ResponsePackeagepublic Dictionary<string, string> Errors { get; set; } public Dictionary<string, string> Errors { get; set; } public Dictionary<string, string> Errors { get; set; } where key will be a property name and the value will be a related error message. public Dictionary<string, string> Errors { get; set; } ,其中key将是属性名称,而值将是相关的错误消息。

Good luck! 祝好运!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM