Simplify your ASP.NET Core API models with C# 9 records

In this post, a quick tip about how to use records to simplify your API models.

Dave Brock
Dave Brock

Out of all the new capabilities C# 9 brings, records are my favorite. With positional syntax, they are immutable by default, which makes working with data classes a snap. I love the possibility of maintaining mutable state in C# where appropriate, like for business logic, and maintaining immutability (and data equality!) with records.

And did you know that with ASP.NET Core 5, model binding and validation supports record types?

In the last post about OpenAPI support in ASP.NET Core 5, I used a sample project that worked with three very simple endpoints (or controllers): Bands, Movies, and People. Each model was in its own class, like this:

Band.cs:

using System.ComponentModel.DataAnnotations;

namespace HttpReplApi.Models
{
    public class Band
    {
        public int Id { get; set; }

        [Required]
        public string Name { get; set; }
    }
}

Movie.cs:

using System.ComponentModel.DataAnnotations;

namespace HttpReplApi.Models
{
    public class Movie
    {
        public int Id { get; set; }

        [Required]
        public string Name { get; set; }
        public int ReleaseYear { get; set; }

    }
}

Person.cs:

using System.ComponentModel.DataAnnotations;

namespace HttpReplApi.Models
{
    public class Person
    {
        public int Id { get; set; }

        [Required]
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
}

I can simplify these with using records instead. In the root, I’ll just create an ApiModels.cs file (I could have them in the controllers themselves, but that feels … messy):

using System.ComponentModel.DataAnnotations;

namespace HttpReplApi
{
    public record Band(int Id, [Required] string Name);
    public record Movie(int Id, [Required] string Name, int ReleaseYear);
    public record Person(int Id, [Required] string FirstName, string LastName);
}

After I change my SeedData class to use positional parameters, I am good to go—this took me about 90 seconds.

For some fun, if I grab a movie by ID, I can use the deconstruction support to get out my properties. (I’m using a discard since I’m not doing anything with the first argument, the Id.)

[HttpGet("{id}")]
public async Task<ActionResult<Movie>> GetById(int id)
{
    var movie = await _context.Movies.FindAsync(id);

    if (movie is null)
    {
        return NotFound();
    }

    var (_, name, year) = movie;
    _logger.LogInformation($"We have {name} from {year}");

    return movie;
}
CSharpAPIsASP.NET Core