DEV Community

Cover image for Standardizing validation and error messages using Resource (.resx) files in .NET
Jean Vidal
Jean Vidal

Posted on

Standardizing validation and error messages using Resource (.resx) files in .NET

The Problem

This month I was analyzing one code project that I needed to work and when I saw the validations and errors handling my feeling was like there was something strange and that could be different.

The code was written more or less like this:

public class User
{
    public string Name { get; set; }
    public string Email { get; set; }
    public int Age { get; set; }
    public string Address { get; set; }
    public string PhoneNumber { get; set; }
}

[ApiController]
[Route("api/users")]
public class UserController: ControllerBase 
{
    [HttpPost]
    public IActionResult CreateUser(User user) 
        {
        try 
                {
            if (string.IsNullOrEmpty(user.Name)) 
                        {
                return BadRequest("Name field is required.");
            }
            if (string.IsNullOrEmpty(user.Email)) 
                        {
                return BadRequest("Email field is required.");
            }
            if (user.Age <= 0 || user.Age > 150) 
                        {
                return BadRequest("Age must be between 1 and 150.");
            }
            if (string.IsNullOrEmpty(user.Address)) 
                        {
                return BadRequest("Address field is required.");
            }
            if (string.IsNullOrEmpty(user.PhoneNumber)) 
                        {
                return BadRequest("PhoneNumber field is required.");
            }

            return Ok("User created successfully!");
        } 
                catch (Exception ex) 
                {
            return StatusCode(500, "An error occurred while processing the request.");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

When we need to use many IF statements, there will almost always be something wrong.

It can make the code harder to maintain and test. Furthermore, it violates the Single Responsibility Principle (SRP) because the controller's main responsibility should be handling HTTP requests and responses and not to write validation logic.

To improve code quality and maintainability, it's better to follow the Separation of Concerns principle and apply validation using separate components. One common approach is to use Data Annotations for model validation and to perform more complex validation using custom validation attributes or separate service classes.

It can work well, but i kept thought about how could i standardize messages and manage them in one place? How could I do it differently?

I thought for a while about it and then i remembered about an old project with something like a key-value pairs storing messages.

I looked it up and I found out what it was… .NET Resources (.resx) Files

The Proposal

What is .NET Resources (.resx) Files?

In summary is a XML based file, formatted as a collection of name-value pairs, that can store localizable resources like a string and binary data such as images, icons, audio, etc.

Sure, but how is it help me to solve the problem?

This resource is commonly used for localization purposes, allowing you to provide different language versions of your application and this is also useful for managing application-specific resources that may need to be changed without modifying the source code, such as error messages, user interface labels, and graphics.

Using it we can maintain the principle of separation of concerns, because the resource files enable separation of resources from code, promoting better maintainability and ease of localization.

My main idea was to keep all messages in one place for the entire system and standardize them.

For my proof of concept (PoC), I defined the following files and their respective messages:

ValidationMessages.resx

Name Value
Length The field {0} must be between {1} and {2} characters.
Email The field {0} is an invalid email address.
Required The field {0} is required.

ErrorMessages.resx

Name Value
400 Invalid request. Please provide valid data.
401 Authentication required. Please provide valid credentials.
403 Access denied. You are not authorized to perform this action.
404 We couldn't find what you were looking for.
500 Something went wrong on the server. Please contact the administrator.

After that I started to create a practical project to test my idea.

The Practical Project

For the PoC I thought of creating a project that demonstrated the use of Resource.resx Files to manage validations and organize validation and error messages.

To see my step-by-step process of creating the project, seeing the complete code and running it, visit the project's GitHub.

Now, I´ll describe my problems and insights to create this project and how I tested it.

The technologies and tools used for this practice was:

  • VS Code
  • .NET 7

At this steps I faced some annoying problems mainly related to VS Code.

Using the Visual Studio IDE we have in the context menu an option to create new Resources and when we use it, the necessary configurations are created automatically.

Easy isn't it?

Not exactly.

Using VS Code, it took a few more steps because I couldn't find any templates to run a command like dotnet new resource n ErrorMessages.

The following steps were taken to create the resource files.

  1. I created the .resx file manually. I needed to find out what the structure is like on the Microsoft website.

    <?xml version="1.0" encoding="utf-8"?>
    <root>
        <data name="Length" xml:space="preserve">
            <value>The field {0} must be between {1} and {2} characters.</value>
        </data>
    </root>
    
  2. I tried to manually create the resource designer file, that is generated automatically by IDE, but it doesn´t work well.

    using System.Resources;
    
    namespace WebApiResource.Resources2;
    
    public static class ErrorMessages
    {
        private static readonly Lazy<ResourceManager> _resourceManager = new Lazy<ResourceManager>(() =>
            new ResourceManager(typeof(ErrorMessages)));
    
        public static string GetString(string key, params object[] args)
        {
            return string.Format(_resourceManager.Value.GetString(key), args);
        }
    }
    

    Finally, I stopped to fight with this manually designer file and then created this configuration in the .csproj that generated the designer files automatically.

    <ItemGroup>
      <EmbeddedResource Update="Resources\ErrorMessages.resx">
        <Generator>PublicResXFileCodeGenerator</Generator>
        <StronglyTypedFileName>Resources\ErrorMessages.Designer.cs</StronglyTypedFileName>
        <StronglyTypedNamespace>WebApiResource.Resources</StronglyTypedNamespace>
        <StronglyTypedClassName>ErrorMessages</StronglyTypedClassName>
        <LastGenOutput>ErrorMessages.Designer.cs</LastGenOutput>
        <StronglyTypedLanguage>CSharp</StronglyTypedLanguage>
      </EmbeddedResource>
      <EmbeddedResource Update="Resources\ValidationMessages.resx">
        <Generator>PublicResXFileCodeGenerator</Generator>
        <StronglyTypedFileName>Resources\ValidationMessages.Designer.cs</StronglyTypedFileName>
        <StronglyTypedNamespace>WebApiResource.Resources</StronglyTypedNamespace>
        <StronglyTypedClassName>ValidationMessages</StronglyTypedClassName>
        <LastGenOutput>ValidationMessages.Designer.cs</LastGenOutput>
        <StronglyTypedLanguage>CSharp</StronglyTypedLanguage>
      </EmbeddedResource>
    </ItemGroup>
    
  3. Then, when I ran the command dotnet build the files were created.

With all the resources OK, I just needed set up my User model and my controller.

  1. For User Model I used Data Annotations and set up the Validation Resource and the Resource Names.

    using System.ComponentModel;
    using System.ComponentModel.DataAnnotations;
    using WebApiResource.Resources;
    
    namespace WebApiResource.Models;
    
    public class User
    {
        [DisplayName("Name")]
        [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ValidationMessages))]
        public required string Name { get; set; }
    
        [DisplayName("Email")]
        [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ValidationMessages))]
        [EmailAddress(ErrorMessageResourceName = "Email", ErrorMessageResourceType = typeof(ValidationMessages))]
        public required string Email { get; set; }
    
        [DisplayName("Description")]
        [Range(minimum: 5, maximum: 10, ErrorMessageResourceName = "Length", ErrorMessageResourceType = typeof(ValidationMessages))]
        public string? Description { get; set; }
    }
    
  2. For User Controller I called the validations from the Data Annotations in User Model and got the messages from the Resources for Validations and Errors.

    using Microsoft.AspNetCore.Mvc;
    using WebApiResource.Models;
    using WebApiResource.Resources;
    
    namespace WebApiResource.Controllers;
    
    [ApiController]
    [Route("api/v1/users")]
    public class UserController : ControllerBase
    {
    
        public UserController()
        {
        }
    
        [HttpPost]
        public IActionResult Post([FromBody] User userModel)
        {
            try
            {
                return Ok(userModel);
            }
            catch (Exception ex)
            {
                var statusCode = GetHttpStatusCodeFromException(ex);
    
                var errorMessage = ErrorMessages.ResourceManager.GetString(name: $"{statusCode}");
                return StatusCode(statusCode, errorMessage);
            }
    
        }
        private static int GetHttpStatusCodeFromException(Exception ex)
        {
            if (ex is HttpRequestException httpRequestException)
            {
                if (httpRequestException.StatusCode.HasValue)
                {
                    return (int)httpRequestException.StatusCode.Value;
                }
            }
            return 500;
        }
    }
    

So, what were the returns after all?

  1. All data Ok

    1. Request

      var myHeaders = new Headers();
      myHeaders.append("Content-Type", "application/json");
      
      var raw = JSON.stringify({
        "Name": "Jean Vidal",
        "Email": "j.vidalnunes@gmail.com",
        "Description": ""
      });
      
      var requestOptions = {
        method: 'POST',
        headers: myHeaders,
        body: raw,
        redirect: 'follow'
      };
      
      fetch("http://localhost:5041/api/v1/users", requestOptions)
        .then(response => response.text())
        .then(result => console.log(result))
        .catch(error => console.log('error', error));
      
    2. Response

      //STATUS 200 OK
      {
          "name": "Jean Vidal",
          "email": "j.vidalnunes@gmail.com",
          "description": ""
      }
      
  2. All data Inconsistent

    1. Request

      var myHeaders = new Headers();
      myHeaders.append("Content-Type", "application/json");
      
      var raw = JSON.stringify({
        "Name": "",
        "Email": "",
        "Description": "SS"
      });
      
      var requestOptions = {
        method: 'POST',
        headers: myHeaders,
        body: raw,
        redirect: 'follow'
      };
      
      fetch("http://localhost:5041/api/v1/users", requestOptions)
        .then(response => response.text())
        .then(result => console.log(result))
        .catch(error => console.log('error', error));
      
    2. Response

      // STATUS 400 BAD REQUEST
      {
          "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
          "title": "One or more validation errors occurred.",
          "status": 400,
          "traceId": "00-cdf77046ab5c73d044db6975adf1b07f-4f7e15bd478f56f2-00",
          "errors": {
              "Name": [
                  "The field Name is required."
              ],
              "Email": [
                  "The field Email is required.",
                  "The field Email is an invalid email address."
              ],
              "Description": [
                  "The field Description must be between 5 and 10 characters."
              ]
          }
      }
      

Next Steps

Now, as next steps I thought of encapsulate the logics:

  1. For Validations

    I´ll create a custom IValidationAttributeAdapterProvider to automatically find the validations error messages from the ValidationMessages Resource, without create a custom validation attribute or specifying ErrorMessageResourceType and ErrorMessageResourceName for each attribute.

  2. For Errors

    I´ll create a custom exception filter CustomExceptionFilterAttribute to encapsulate the logic for retrieve error messages from the ErrorMessages Resource. This filter is applied to all actions in your controllers.

Conclusion

This idea work well and can be one way to standardize all error and validation messages.

Here are some precautions that must be taken in this and any other case.

  1. There is no silver bullet -
    1. All cases must be evaluated, tested, and considered their pros and cons.
  2. This may not be the best solution to your problem -
    1. I read about FluentValidation library and it is a good choice for data validations.
    2. You should consider whether an additional library to your project/team will have any impact, plus you need to assess the learning curve. I even believe that it could be used with Resource without problems.
  3. Consider maintaining the Resource files -
    1. Resource files can become bloated if not managed properly, leading to increased memory usage and longer application startup times.
    2. If you have multilanguage applications, this can be a challenge to update and maintain multiple resource files for each supported language or culture.
    3. The process of deploying and updating resource files in distributed environments can be complex, requiring careful version management.

Despite everything, I believe it's a good solution if you don't have anything else worked on and you're starting to have problems or the need to standardize messages, both validation and errors.

  1. Localization, Multilanguage and Separation of Concerns -
    1. Enable separation of resources from code, facilitating localization and internationalization efforts.
    2. Support localization, enabling you to provide messages in multiple languages. This is crucial when building multi language applications, ensuring a consistent user experience across different locales.
    3. Placing error and validation messages in resource files separates them from the application logic, adhering to the principle of separation of concerns. This enhances code readability and maintainability.
  2. Native -
    1. Visual Studio provides tools for creating and managing resource files, while the .NET Framework offers APIs for accessing resources at runtime.
  3. Centralization -
    1. Allow you to centralize all error and validation messages in one place. This simplifies maintenance and makes it easier to update messages without having to modify code throughout the application.
  4. Consistency -
    1. By standardizing error and validation messages in resource files, you ensure a consistent and uniform messaging system throughout the application. This helps in providing a better user experience and reduces confusion for users.
  5. Reusable Messages -
    1. Allow you to define reusable messages that can be used across different parts of the application. This promotes code reusability and avoids duplication of messages.

We're done here folks.

I hope you all enjoyed it and that this helps in some way.

Top comments (2)

Collapse
 
gilsonricardopeloso profile image
Gilson Ricardo Peloso

Great job! Code reusability is fine! Thanks 👍

Collapse
 
jvidaln profile image
Jean Vidal

Tks so much Gilson.