Skip to main content
.NET

The New Data Annotations in .NET 8

The latest LTS version, .NET 8, introduces new DataAnnotations for enhanced validation of strings and numbers. This article explores these annotations and shows you how to use them i practice.

Christian Schou

In the latest LTS version named .NET 8, we will get some new DataAnnotations for better validation of strings and numbers. In this article, I will show you the new options we are provided with.

To be more specific we are talking DataAnnotations to validate

  • Minimum and maximum string lengths.
  • Minimum and maximum range for numeric values.
  • The option to specify values to deny or allow.
  • Built-in validation of Base64 strings.

I have created a new .NET 8 Web API project based on the latest available template from dotnet to demonstrate these new DataAnnotaions in real-life. When you are done reading this post about the new data annotations in .NET 8, you will be fully up to date, and ready to implement them in your applications.

The Data Model

To give you a better understanding I have created a new model for a book where I have used the new data annotations to show you how they are used.

using System.ComponentModel.DataAnnotations;

namespace DataAnnotationsDemo.Models
{
    public class Book
    {
        public int Id { get; set; }

        [Length(5, 50)]
        public string? Name { get; set; }

        [Base64String]
        public string? Description { get; set; }

        [Range(1, 250, MinimumIsExclusive = true, MaximumIsExclusive = false)]
        public double Price { get; set; }

        [Length(10,10)]
        public string? ISBN { get; set; }

        [AllowedValues("Fantasy", "Horror", "Sci-Fi")]
        [DeniedValues("Cooking")]
        public string? Category { get; set; }

        [DeniedValues("Christian Schou")]
        public string? Author { get; set; }
    }
}

Great! For now, you don't have to worry about this model, I just want you to take a look at it and remember the data annotation attributes on the properties.

The Controller

With the model in place, let's create a new controller to submit our book requests to validate if the new attributes do their job. This is the code for my book controller.

using DataAnnotationsDemo.Models;
using Microsoft.AspNetCore.Mvc;

namespace DataAnnotationsDemo.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BooksController : ControllerBase
    {
        [HttpPost]
        public IActionResult CreateBook(Book book)
        {
            if(!ModelState.IsValid) return BadRequest();

            return Ok("All good!");
        }
    }
}

It's just a simple API controller in .NET inheriting from ControllerBase - nothing special here. I have created a new POST endpoint for creating a new book using the data model we just created.

Testing And Explanation Of Each Data Annotation

Great! Now that we have an API with a POST endpoint for creating new books, let's test the new annotations and see how the API will respond when we trigger the validation for each of the data annotations.

The Length Data Annotation

The first attribute I have used in my Book model is the Lengththe attribute. Let's see if we can trigger it by sending the wrong payload. The property I am targeting now is this one:

[Length(5, 50)]
public string? Name { get; set; }

The System.ComponentModel.DataAnnotations.LengthAttribute, specifies the lowest and upper bounds for strings or collections. In my example above the property Name requires the length to be at least 5 characters and a maximum of 50 characters.

Below is the data I submitted in the request.

{
  "id": 0,
  "name": "str"
}

This is the response I got from the API.

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Name": [
      "The field Name must be a string or collection type with a minimum length of '5' and maximum length of '50'."
    ]
  },
  "traceId": "00-70943710b17477143f7a56b9a52e04b8-a994545884d57e72-00"
}

Great! That works very well. We even got a nice error message telling us the exact problem with our request. Let's move on! ✌️

The Base64String Data Annotation

Next to Name of the book, we have the Description property, and we would like that one to be a well-formed Base64String.

This is the property we are targeting now.

[Base64String]
public string? Description { get; set; }

This is the payload I am submitting now.

{
  "id": 0,
  "name": "Data Annotations",
  "description": "string"
}

The response from the API resulted in this.

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Description": [
      "The Description field is not a valid Base64 encoding."
    ]
  },
  "traceId": "00-697eac3539e603124c4644bed62be036-6af83866f784fafb-00"
}

Whoops - we are not getting an error message telling us that our Description is not a valid Base64 string. Cool!

The Range Data Annotation

Now for the Price property with the Range attribute. This data annotation allows us to give clear instructions for the minimum and maximum allowed values. In this example, we allow a book to be as cheap as 1 and the max price to be 250.

[Range(0, 250, MinimumIsExclusive = true, MaximumIsExclusive = false)]
public double Price { get; set; }

Maybe you noted the two extra parameters in the Range attribute? 😅 Let me explain those to you.

  • The MinimumIsExclusive = true means that the minimum value (0) is not included in the valid range, so the Price must be greater than 0.
  • The MaximumIsExclusive = false means that the maximum value (250) is included in the valid range, so the Price can be equal to 250.

Let's test that out and see what the response we get back is. Here is the current payload for the POST request.

{
  "id": 0,
  "name": "Data Annotations",
  "description": "aHR0cHM6Ly9jaHJpc3RpYW4tc2Nob3UuZGsv",
  "price": 0
}

Here is the response from the API.

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Price": [
      "The field Price must be between 1 exclusive and 250."
    ]
  },
  "traceId": "00-e121ff1ee1a9247deadc3f229111181c-3650ddb2f150e2b0-00"
}

Wooha! And we even got told that the price must be between 1 exclusive, just like we specified on the property in the model!

The Allowed And Denied Values Data Annotation

The [AllowedValues] attribute specifies the valid values for the Category property. In this case, the Category can be "Fantasy", "Horror", or "Sci-Fi".

The [DeniedValues] attribute specifies the values that are not allowed for the Category property. In this case, the Category cannot be "Cooking". Here is the property.

[AllowedValues("Fantasy", "Horror", "Sci-Fi")]
[DeniedValues("Cooking")]
public string? Category { get; set; }

Let's see what happens if we intentionally trigger these attributes' validation. I am sending this POST request payload.

{
  "id": 0,
  "name": "Data Annotations",
  "description": "aHR0cHM6Ly9jaHJpc3RpYW4tc2Nob3UuZGsv",
  "price": 25,
  "category": "Cooking"
}

This is the response from the API.

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Category": [
      "The Category field does not equal any of the values specified in AllowedValuesAttribute.",
      "The Category field equals one of the values specified in DeniedValuesAttribute."
    ]
  },
  "traceId": "00-4f58eeadc604a2f3b1369da4cfcf1039-f21e0f80c56df026-00"
}

Yay! We get both error messages as I was expecting. We get them both because we do not send any of the allowed categories and the denied value was the exact one that is not allowed.

Final Payload

Now that we know what a payload cannot look like, I would like to show you the final one where everything is OK.

{
  "id": 0,
  "name": "Data Annotations",
  "description": "aHR0cHM6Ly9jaHJpc3RpYW4tc2Nob3UuZGsv",
  "price": 25,
  "category": "Horror",
  "author": "Tech with Christian"
}

The response from the API was just as expected. Status 200!

All good!

Summary 🎉

In this short .NET post about the new DataAnnotation's introduced in .NET 8, you learned that you now can have even more power in the validation pipeline of your properties.

It's now possible to do min- and max-length limits on properties for both strings and numeric values. My personal favorite is the Base64String validator! I can now go ahead and remove my custom implementations of this validation check in my validation behaviors.

If you learned something new, then remember to share it with your friends and colleagues, they might learn something as well. If you have any questions, please let me know in the comments below. Until next time - Happy coding! ✌️