Tutorials / Build a REST API with C# and .NET · Lesson 28 of 34

Concept

On this page

    Small C# Shortcuts

    This lesson is optional. The course works without it, and nothing here changes what the API does. It is good to know because it shortens your code, and the lessons that follow use these shortcuts, so come back when you meet one.

    Look at a line like this one from the fake book service:

    public List<Book> Books { get; } = new List<Book>();
    

    It says List<Book> twice. The compiler already knows the type from the left side, so the second one only repeats it. Modern C# lets you leave out the repeated words. This lesson shows three small shortcuts of that kind: new(), [] and required. They are not about records. They work on any class, and each one is a few characters saved on code the course already has.

    Our goal: meet new(), [] and required, each with the problem it solves, and use the first two to shorten the fakes in the tests, without changing anything the API does.

    Here is the route:

    1. See new() and [], which both let the compiler fill in a type it already knows.
    2. See required, which makes forgetting a value an error.
    3. Rewrite the three fakes in the tests with the first two.
    4. Prove that nothing changed: same tests, same JSON.

    This lesson leaves out the rest of the newer C# syntax (such as pattern matching), because the course code does not need it.

    Where we are

    This is the solution from the end of records. Only three test files change, and none is added:

    BookShop/
      BookShop.slnx
      Directory.Build.props
      Directory.Packages.props
      src/
        BookShop.Api/
          Program.cs
          GlobalExceptionHandler.cs
          Controllers/
            BooksController.cs
            ReportsController.cs
        BookShop.Data/
          Book.cs
          BookShopDbContext.cs
          IBookService.cs
          BookService.cs
          SellResult.cs
          CategoryStock.cs
          IStockReportService.cs
          StockReportService.cs
          Migrations/
      tests/
        BookShop.Data.Tests/
          BookServiceTests.cs
          BookValidationTests.cs
          StockReportServiceTests.cs
        BookShop.Api.Tests/
          FakeBookService.cs                 <- changes (two shortcuts)
          FakeLogger.cs                      <- changes (one shortcut)
          BooksControllerTests.cs
          GlobalExceptionHandlerTests.cs
          FakeStockReportService.cs          <- changes (one shortcut)
          ReportsControllerTests.cs
    

    Run every command from the solution folder, BookShop/. No project is added, so there is no solution map.


    Let the compiler fill in the type

    Think of a shelf in the shop with a label on it: "Cookbooks". Every book put on that shelf does not need its own sticker saying "cookbook", because the label already says it. A line of C# can work the same way: when the type is written on the left of the =, the right side does not have to say it again.

    The type is written once, on the left Repeated List<Book> = new List<Book>() Said once List<Book> = [] The compiler reads the type from the left, so the right side can be short.

    new(): do not repeat the type

    The fake book service has:

    public SellResult SellResult { get; set; } = new SellResult();
    

    The left side says SellResult, so the right side can be:

    public SellResult SellResult { get; set; } = new();
    

    The compiler fills in the type from the left. It works only where the type is written on the left (a property, a field, a parameter). After var, nothing says what the type is, and the compiler refuses:

    var sale = new();
    
    error CS8754: There is no target type for 'new()'
    

    []: an empty list, or a list of items

    An empty list used to be written new List<Book>(). It can now be written [], which is called a collection expression:

    public List<Book> Books { get; } = new List<Book>();
    public List<Book> Books { get; } = [];
    

    The two lines mean the same. The type on the left (List<Book>) decides what [] is. With items, it reads as a list too:

    List<int> numbers = [1, 2, 3];
    

    That is a list of three numbers. Again the type must be known from the left. Without it:

    var books = [];
    
    error CS9176: There is no target type for the collection expression.
    

    required: a value that must be set

    This one solves a different problem. Take a plain class like the old report line, with a body and a { get; set; } for each value:

    public class CategoryStockClass
    {
        public string Category { get; set; } = "";
        public int Titles { get; set; }
        public int Copies { get; set; }
        public decimal StockValue { get; set; }
    }
    

    It can be built with a value left out, and nothing complains. The number just stays 0:

    var line = new CategoryStockClass { Category = "Cooking" };
    Console.WriteLine(line.Titles);
    
    0
    

    A report line that says "Cooking, 0 titles" is wrong, and nobody was told. When you want a value to be given, put required in front of the property:

    public class CategoryStockClass
    {
        public required string Category { get; set; }
        public required int Titles { get; set; }
        public required int Copies { get; set; }
        public required decimal StockValue { get; set; }
    }
    

    Building it with only the category is now an error, before the program even runs. The compiler gives one error for each missing value; the first is:

    error CS9035: Required member 'CategoryStockClass.Titles' must be set in the object initializer or attribute constructor.
    
    A forgotten value: a quiet zero, or an error Without required it builds, and Titles is 0 With required it does not build Same forgotten value. The error shows up before anyone sees a wrong report.

    We do not put required on Book: our tests build books with only the fields they care about (new Book { Id = 1, Title = "Clean Code" }), and required would stop all of them. It is a choice about how strict a class should be. This course does not use required in its own code: the classes it builds later use default values instead, because they give a clearer error to a client. It is here because you will meet it in other people's projects.


    The fakes, with the shortcuts

    The three fakes in the tests have lines that new() and [] can shorten. Nothing else in them changes.

    Open tests/BookShop.Api.Tests/FakeStockReportService.cs. The whole file is:

    using BookShop.Data;
    
    namespace BookShop.Api.Tests;
    
    public class FakeStockReportService : IStockReportService
    {
        public List<CategoryStock> Report { get; } = [];                        // new
        // public List<CategoryStock> Report { get; } = new List<CategoryStock>();   // replaced
    
        public Task<List<CategoryStock>> GetByCategoryAsync()
        {
            return Task.FromResult(Report);
        }
    }
    

    Open tests/BookShop.Api.Tests/FakeLogger.cs. The whole file is:

    using Microsoft.Extensions.Logging;
    
    namespace BookShop.Api.Tests;
    
    public class FakeLogger<T> : ILogger<T>
    {
        public List<LogLevel> Levels { get; } = [];                             // new
        public List<Exception?> Exceptions { get; } = [];                       // new
        // public List<LogLevel> Levels { get; } = new List<LogLevel>();        // replaced
        // public List<Exception?> Exceptions { get; } = new List<Exception?>();     // replaced
    
        public IDisposable? BeginScope<TState>(TState state) where TState : notnull
        {
            return null;
        }
    
        public bool IsEnabled(LogLevel logLevel)
        {
            return true;
        }
    
        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
            Exception? exception, Func<TState, Exception?, string> formatter)
        {
            Levels.Add(logLevel);
            Exceptions.Add(exception);
        }
    }
    

    Open tests/BookShop.Api.Tests/FakeBookService.cs. The whole file is:

    using BookShop.Data;
    
    namespace BookShop.Api.Tests;
    
    public class FakeBookService : IBookService
    {
        public List<Book> Books { get; } = [];                                  // new
        public SellResult SellResult { get; set; } = new();                     // new
        // public List<Book> Books { get; } = new List<Book>();                 // replaced
        // public SellResult SellResult { get; set; } = new SellResult();       // replaced
    
        public Task<List<Book>> GetAllAsync()
        {
            return Task.FromResult(Books);
        }
    
        public Task<Book?> GetByIdAsync(int id)
        {
            var book = Books.FirstOrDefault(b => b.Id == id);
            return Task.FromResult(book);
        }
    
        public Task<Book> CreateAsync(Book book)
        {
            book.Id = Books.Count + 1;
            Books.Add(book);
            return Task.FromResult(book);
        }
    
        public Task<bool> UpdateAsync(int id, Book book)
        {
            var index = Books.FindIndex(b => b.Id == id);
            if (index < 0)
            {
                return Task.FromResult(false);
            }
    
            book.Id = id;
            Books[index] = book;
            return Task.FromResult(true);
        }
    
        public Task<bool> DeleteAsync(int id)
        {
            var existing = Books.FirstOrDefault(b => b.Id == id);
            if (existing == null)
            {
                return Task.FromResult(false);
            }
    
            Books.Remove(existing);
            return Task.FromResult(true);
        }
    
        public Task<SellResult> SellAsync(int id, int quantity)
        {
            return Task.FromResult(SellResult);
        }
    }
    

    Did anything change?

    A rewrite that changes nothing is a refactoring, and the way to prove it is the tests and the endpoint. Run all tests from the solution folder:

    dotnet test
    
    Passed!  - Failed:     0, Passed:    18, Skipped:     0, Total:    18, Duration: 155 ms - BookShop.Api.Tests.dll (net10.0)
    Passed!  - Failed:     0, Passed:    23, Skipped:     0, Total:    23, Duration: 1 s - BookShop.Data.Tests.dll (net10.0)
    

    The same forty-one tests as before. (The durations may differ.) Then start the app and send the same request as in the stock report:

    curl http://localhost:5101/reports/stock
    
    [
      {
        "category": "Cooking",
        "titles": 2,
        "copies": 6,
        "stockValue": 181.0
      },
      {
        "category": "Programming",
        "titles": 3,
        "copies": 22,
        "stockValue": 852.3
      }
    ]
    

    (The real body is one long line.) Letter for letter the same JSON as before the rewrite, so a frontend would not notice.


    Where we ended

    Compare with your solution. Only the three fakes are shorter:

    BookShop/
      BookShop.slnx
      Directory.Build.props
      Directory.Packages.props
      src/
        BookShop.Api/
          Program.cs
          GlobalExceptionHandler.cs
          Controllers/
            BooksController.cs
            ReportsController.cs
        BookShop.Data/
          Book.cs
          BookShopDbContext.cs
          IBookService.cs
          BookService.cs
          SellResult.cs
          CategoryStock.cs
          IStockReportService.cs
          StockReportService.cs
          Migrations/
      tests/
        BookShop.Data.Tests/
          BookServiceTests.cs
          BookValidationTests.cs
          StockReportServiceTests.cs
        BookShop.Api.Tests/
          FakeBookService.cs                 [] and new()
          FakeLogger.cs                      []
          BooksControllerTests.cs
          GlobalExceptionHandlerTests.cs
          FakeStockReportService.cs          []
          ReportsControllerTests.cs
    

    Recap

    • new() and [] save repeating a type that the left side already names. They work only where the type is written on the left, not after var.
    • [] is an empty list, or a list of items: [1, 2, 3].
    • required in front of a property makes forgetting a value an error when you build, instead of a quiet 0 or an empty text.
    • None of these three is about records. They work on any class.
    • A rewrite that changes nothing is proved by the same tests and the same JSON.

    Questions to think about

    Each question has its answer folded away underneath. Guess first, then open it to check.

    1. Why does new() not work after var?
    Show answer

    The compiler reads the type from the left side. After var nothing on the left names a type, so there is nothing to fill in.

    1. What does [] mean, and what decides its type?
    Show answer

    It is an empty list (or a list of the items inside it). The type written on the left, such as List<Book>, decides what kind of list it is.

    1. What does required protect against?
    Show answer

    Forgetting to give a value. Without it the value quietly stays 0 or empty. With it, building the object without that value is an error.

    1. How do we know a rewrite like this one did not break anything?
    Show answer

    The same tests pass and the same JSON comes out.

    Next lesson: DTOs for the books endpoints, where the books endpoints stop sending and receiving the table class.

    ← RecordsDTOs for the Books Endpoints →