Skip to main content
All docs
V24.1
.NET 6.0+

Validate Data Sent to Web API Endpoints

  • 5 minutes to read

This topic shows how you can validate user input if you use DevExpress Web API Service for data access.

The following technologies enable data validation:

XPO | EF Core
These ORM tools allow you to define your Business Model together with validation attributes.
XAF Validation Module
Allows you to apply 10+ predefined rules or any number of custom validation rules to your data objects. Enforces those rules in XAF applications.

Even if you don’t use predefined validation attributes in your data model, you can still use DevExpress Web API Service to validate user input. You can add custom data validation logic if you extend the basic implementation described in this article.

Note

This option of our Web API Service ships as part of the DevExpress Universal Subscription.

Validation API Availability

If you use DevExpress Web API Service endpoints to manage data and need to enable validation, use a specially designed IValidator service available in the following namespace: DevExpress.Persistent.Validation.

You can use the following methods to enable the service.

Enable the Validation Module in the Solution Wizard

If you create your Backend Web API project with the help of the Solution Wizard, a dedicated wizard screen allows you to enable the Validation module. For additional information, see Create a Standalone Web API Application.

Add the Validation Module to a Standalone Web API or XAF Blazor Application

Install the DevExpress.ExpressApp.Validation.Blazor NuGet package.

Register the Validation module and required services in Startup.cs. Use the Web API or XAF Application builder:

File: MySolution.WebApi\Startup.cs (MySolution.Blazor.Server\Startup.cs)

services.AddXafWebApi(builder => {
    //..
    builder.Modules
        .AddValidation(options => {
            //...
        })
    //...
}, Configuration);

Validation Does Not Run Automatically

CRUD endpoints don’t initiate data validation: you need to add that functionality. One reason for this behavior is performance optimization. Another reason has to do with current endpoint limitations. For example, we do not yet support requests that create a master object with their children at the same time. On the other hand, a validation rule may not let you create a master object without certain children.

Basic Data Validation Implementation

In a most basic scenario, you access your data using CRUD endpoints. When a user changes data, you enforce specified validation attributes in your data model.

The following example shows how you can enable such functionality. The code implements a custom IDataService. This service runs validation before it commits an object space (IObjectSpace).

For details about custom data services, please see the following article: Execute Custom Operations on Endpoint Requests.

using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Core;
using DevExpress.ExpressApp.DC;
using DevExpress.ExpressApp.WebApi.Services;
using DevExpress.Persistent.Validation;
// ...
public class CustomDataService : DataService {
    readonly IValidator validator;
    public CustomDataService(IObjectSpaceFactory objectSpaceFactory,
     ITypesInfo typesInfo, IValidator validator) 
     : base(objectSpaceFactory, typesInfo) {
        this.validator = validator;
    }

    protected override IObjectSpace CreateObjectSpace(Type objectType) {
        IObjectSpace objectSpace = base.CreateObjectSpace(objectType);
        objectSpace.Committing += ObjectSpace_Committing;
        return objectSpace;
    }

    private void ObjectSpace_Committing(object? sender, 
      System.ComponentModel.CancelEventArgs e) {
        IObjectSpace os = (IObjectSpace)sender!;
        var validationResult = validator.RuleSet.ValidateAllTargets(
            os, os.ModifiedObjects, DefaultContexts.Save
        );
        if(validationResult.ValidationOutcome == ValidationOutcome.Error) {
            throw new ValidationException(validationResult);
        }
    }
}

Configure the Request Locale

You can use the HttpRequestMessage API to change the request locale. The invalid validation results you obtain from the service will use the locale you specified.

// ...
httpClient.DefaultRequestHeaders.Add("Accept-Language", "de-DE");
// ...

Add Unit Tests

Test Scenario

This example checks validation rules for XAF’s standard ApplicationUser class. XAF declares this class in all new projects that use the Security feature.

The ApplicationUser class applies a RuleRequiredField attribute to the UserName property. In other words, you cannot create a new user with an empty name.

The test tries to create an ApplicationUser object with an empty UserName property and receives a validation error.

This example uses the IDataService implementation demonstrated above. You can find the same test included in our MainDemo.EFCore demo application.

Step-by-Step Instructions

Follow the steps below to add a test to your solution:

  1. If your solution does not include a testing project, add a new xUnit test project.
  2. Add a reference to the {SolutionName}.Blazor.Server project.
  3. Add the Microsoft.AspNetCore.Mvc.Testing package reference.
  4. Add the following test to the xUnit project.

    using System.Net;
    using System.Net.Http.Headers;
    using System.Net.Http.Json;
    using System.Text;
    using System.Text.Json;
    using MySolution.Module.BusinessObjects;
    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Mvc.Testing;
    using Xunit;
    
    public class CustomValidationTests_out : IClassFixture<WebApplicationFactory<MySolution.Blazor.Server.Startup>> {
        HttpClient httpClient;
        public CustomValidationTests_out(WebApplicationFactory<MySolution.Blazor.Server.Startup> webApplicationFactory) {
            httpClient = webApplicationFactory.CreateClient();
        }
    
        [Fact]
        public async System.Threading.Tasks.Task CreateApplicationUser_ValidateUserNameIsNotEmpty() {
            string tokenString = await GetUserTokenAsync("Admin", "", "/api/Authentication/Authenticate");
            var authorizationToken = new AuthenticationHeaderValue("Bearer", tokenString);
    
            string url = $"/api/odata/{typeof(ApplicationUser).Name}";
    
            string content = $"{{\"{nameof(ApplicationUser.ChangePasswordOnFirstLogon)}\":true}}";
            var httpRequest = new HttpRequestMessage(HttpMethod.Post, url);
    
            httpRequest.Content = new StringContent(content, Encoding.UTF8, "application/json");
            httpRequest.Headers.Authorization = authorizationToken;
            var basResponse = await httpClient.SendAsync(httpRequest);
    
            Assert.False(badResponse.IsSuccessStatusCode);
            string expectedErrorMessage =
                $"Bad Request : Data Validation Error: Please review and correct the data validation error(s) listed below to proceed.{Environment.NewLine}" +
                $" - The user name must not be empty";
            string actualErrorMessage;
            using(var stream = await badResponse.Content.ReadAsStreamAsync()) {
                using(StreamReader reader = new StreamReader(stream)) {
                    actualErrorMessage = badResponse.ReasonPhrase + " : " + reader.ReadToEnd();
                }
            }
            Assert.Equal(expectedErrorMessage, actualErrorMessage);
            Assert.Equal(HttpStatusCode.BadRequest, badResponse.StatusCode);
    
            //Correct request content
            content = $"{{\"{nameof(ApplicationUser.UserName)}\":\"TestUserName\",\"{nameof(ApplicationUser.ChangePasswordOnFirstLogon)}\":true}}";
            httpRequest = new HttpRequestMessage(HttpMethod.Post, url);
            httpRequest.Content = new StringContent(content, Encoding.UTF8, "application/json");
            httpRequest.Headers.Authorization = authorizationToken;
            var response = await httpClient.SendAsync(httpRequest);
            Assert.Equal(HttpStatusCode.Created, response.StatusCode);
    
            var jsonResult = await response.Content.ReadFromJsonAsync<JsonElement>();
            var newUser = jsonResult.Deserialize(typeof(ApplicationUser)) as ApplicationUser;
            ArgumentNullException.ThrowIfNull(newUser);
            try {
                Assert.Equal("TestUserName", newUser.UserName);
                Assert.True(newUser.ChangePasswordOnFirstLogon);
            }
            finally {
                //Delete a new user
                httpRequest = new HttpRequestMessage(HttpMethod.Delete, $"/api/odata/{typeof(ApplicationUser).Name}/{newUser.ID}");
                httpRequest.Headers.Authorization = authorizationToken;
                await httpClient.SendAsync(httpRequest);
            }
        }
    
        async Task<string> GetUserTokenAsync(string userName, string password, string requestPath) {
            var request = new HttpRequestMessage(HttpMethod.Post, requestPath);
            request.Content = new StringContent(
                $"{{ \"userName\": \"{userName}\", \"password\": \"{password}\" }}", Encoding.UTF8, "application/json");
    
            var httpResponse = await httpClient.SendAsync(request);
            if(!httpResponse.IsSuccessStatusCode) {
                throw new UnauthorizedAccessException($"Authorization request failed! Code {(int)httpResponse.StatusCode}, '{httpResponse.ReasonPhrase}'");
            }
            var tokenString = await httpResponse.Content.ReadAsStringAsync();
            return tokenString;
        }
    }
    
See Also