Beginner 40 min.NET 10

C# essentials for MVC developers

Understand the types, properties, records, and nullability you will use in every MVC feature.

BY THE END OF THIS TUTORIAL

Here's what you'll be able to do.

  • Read a model class and its validation attributes
  • Distinguish nullable reference types from runtime validation
  • Use records and LINQ to shape view data
Before you begin

Run TaskBoard and locate the Models folder. Some experience with variables and conditions is helpful.

Understand the idea

MVC code becomes much easier to follow when you can tell what each C# type represents. An entity describes a persisted record. An input model describes fields accepted from a user. A view model packages exactly what a page needs. These types may have similar properties but serve different boundaries.

TaskBoard separates these responsibilities intentionally. The database entity has an OwnerId and an IsComplete flag. The create input does not accept those values: the server decides who owns the task and how a new task starts.

Choose your environment

Open TaskBoard.csproj from the example download. Install the ASP.NET and web development workload and a Visual Studio release supporting .NET 10. Select the TaskBoard launch profile and press F5. Use Solution Explorer to find the files named below.

Download the complete companion project
STEP 01

Read an entity as a contract

BoardTask is a class with properties. Id identifies a row. Title starts as an empty string, avoiding an uninitialized non-nullable property. Notes is marked string?, which tells the compiler that absence is allowed. CreatedUtc is initialized when a new instance is constructed.

Attributes are metadata interpreted by other systems. MaxLength informs the EF model, but it does not magically prevent arbitrary code from assigning a longer string. Input validation and database constraints are distinct layers, and providers differ in how they enforce length.

Models/BoardTask.cs
using System.ComponentModel.DataAnnotations;

namespace TaskBoard.Models;

public class BoardTask
{
    public int Id { get; set; }
    [MaxLength(100)] public string Title { get; set; } = "";
    [MaxLength(2000)] public string? Notes { get; set; }
    public bool IsComplete { get; set; }
    public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
    public string OwnerId { get; set; } = "";
}
STEP 02

Separate user input from stored data

TaskInput accepts only a title and notes. Its Required and StringLength attributes are evaluated during MVC validation. The compiler can warn about a null reference, but it cannot prove that a browser submits an acceptable title.

Notice that OwnerId is absent. If an attacker submits an additional OwnerId field, it has nowhere to bind on this input model. You still need authorization checks on reads and writes; a small input model is one part of a secure design.

Models/TaskInput.cs
using System.ComponentModel.DataAnnotations;

namespace TaskBoard.Models;

public class TaskInput
{
    [Required(ErrorMessage = "Give your task a title.")]
    [StringLength(100, MinimumLength = 3)]
    public string Title { get => _title; set => _title = value?.Trim() ?? ""; }
    private string _title = "";

    [StringLength(2000)]
    public string? Notes { get; set; }
}

public sealed record TaskListPage(IReadOnlyList<BoardTask> Tasks, string? Query, int Page, int TotalPages, int TotalCount);
STEP 03

Use records for small results

The Summary record represents an immutable pair of counts. Its generated equality behavior is based on values, so two Summary instances with the same counts compare equal. That makes small result objects convenient to inspect and test.

IReadOnlyList communicates that consumers should read the collection rather than add or remove items through this API. It does not make every contained entity immutable. When you need a hard boundary, project entities into purpose-built records.

C# example
public sealed record TaskCard(int Id, string Title, bool IsComplete);

var cards = new[]
{
    new TaskCard(1, "Learn MVC", false),
    new TaskCard(2, "Run the example", true)
};

var openTitles = cards
    .Where(task => !task.IsComplete)
    .Select(task => task.Title)
    .ToList();
// openTitles contains "Learn MVC".
STEP 04

Follow the data through an action

When a form posts, MVC creates TaskInput and assigns its properties. The action checks validation, then explicitly constructs BoardTask. This mapping makes the boundary visible: user input supplies Title and Notes; the authenticated principal supplies OwnerId.

Use meaningful names and explicit types at boundaries. Inside a short method, var is useful when the initializer makes the type clear. Avoid treating null-forgiving syntax as validation: the exclamation mark only suppresses compiler warnings. It does not check the value at runtime.

Controller excerpt
var task = new BoardTask
{
    Title = input.Title.Trim(),
    Notes = input.Notes,
    OwnerId = OwnerId
};
db.Tasks.Add(task);
await db.SaveChangesAsync(ct);
YOUR TURN

Make it your own.

Create a TaskCard record with Id, Title, and IsComplete. Project three sample tasks into cards, then select the titles of completed cards.

See the solution & reasoning

Define public sealed record TaskCard(int Id, string Title, bool IsComplete). Apply Where(t => t.IsComplete) before Select(t => t.Title). Materialize with ToList() when you need to inspect the result. Filtering first also makes your intent clear to the next reader.

When something doesn't work

A nullable-reference warning is a design signal, not a reason to append ! everywhere. Decide whether absence is valid and handle it explicitly.

If a model accepts values that should be server-controlled, introduce an input model instead of binding the full entity.

LINQ queries are often deferred. Changing the source before enumeration can change the result. Use ToList when you intentionally need a snapshot.

CHECK YOUR UNDERSTANDING

Does string? validate what a browser submits?

Does string? validate what a browser submits?

ONE STEP FURTHER

That's another skill in your toolkit.

Your progress is saved in this browser. Sign in to save completion to your account.