In this tutorial, you build a console app that models daily temperatures by using records and record structs.
In this tutorial, you learn how to:
- Declare positional records and record structs.
- Build a small record hierarchy.
- Use compiler-generated equality and formatting.
- Use
withexpressions for nondestructive mutation.
Prerequisites
author: BillWagner ms.author: wiwagn ms.topic: include ms.date: 03/18/2025
- The latest .NET SDK
- Visual Studio Code editor
- The C# DevKit
Installation instructions
On Windows, this WinGet configuration file to install all prerequisites. If you already have something installed, WinGet will skip that step.
- Download the file and double-click to run it.
- Read the license agreement, type y, and select Enter when prompted to accept.
- If you get a flashing User Account Control (UAC) prompt in your Taskbar, allow the installation to continue.
On other platforms, you need to install each of these components separately.
- Download the recommended installer from the .NET SDK download page and double-click to run it. The download page detects your platform and recommends the latest installer for your platform.
- Download the latest installer from the Visual Studio Code home page and double click to run it. That page also detects your platform and the link should be correct for your system.
- Click the "Install" button on the C# DevKit extension page. That opens Visual Studio code, and asks if you want to install or enable the extension. Select "install".
Create the app and your first record
Create a folder for your app, run dotnet new console, and open the generated project.
Add a file named DailyTemperature.cs, and add a positional readonly record struct for temperature values:
sample · csharpdocs/csharp/fundamentals/tutorials/snippets/records/DailyTemperature.cs
namespace record_types;
// <TemperatureRecord>
public readonly record struct DailyTemperature(double HighTemp, double LowTemp)
{
public double Mean => (HighTemp + LowTemp) / 2.0;
}
// </TemperatureRecord>Add a file named Program.cs, and create sample temperature data:
sample · csharpdocs/csharp/fundamentals/tutorials/snippets/records/Program.cs
namespace record_types;
class Program
{
// <DeclareData>
private static DailyTemperature[] data = [
new DailyTemperature(HighTemp: 57, LowTemp: 30),
new DailyTemperature(60, 35),
new DailyTemperature(63, 33),
new DailyTemperature(68, 29),
new DailyTemperature(72, 47),
new DailyTemperature(75, 55),
new DailyTemperature(77, 55),
new DailyTemperature(72, 58),
new DailyTemperature(70, 47),
new DailyTemperature(77, 59),
new DailyTemperature(85, 65),
new DailyTemperature(87, 65),
new DailyTemperature(85, 72),
new DailyTemperature(83, 68),
new DailyTemperature(77, 65),
new DailyTemperature(72, 58),
new DailyTemperature(77, 55),
new DailyTemperature(76, 53),
new DailyTemperature(80, 60),
new DailyTemperature(85, 66)
];
// </DeclareData>
static void Main(string[] args)
{
// <HeatingAndCooling>
var heatingDegreeDays = new HeatingDegreeDays(65, data);
Console.WriteLine(heatingDegreeDays);
var coolingDegreeDays = new CoolingDegreeDays(65, data);
Console.WriteLine(coolingDegreeDays);
// </HeatingAndCooling>
// <GrowingDegreeDays>
// Growing degree days measure warming to determine plant growing rates
var growingDegreeDays = coolingDegreeDays with { BaseTemperature = 41 };
Console.WriteLine(growingDegreeDays);
// </GrowingDegreeDays>
// <RunningFiveDayTotal>
// showing moving accumulation of 5 days using range syntax
List<CoolingDegreeDays> movingAccumulation = new();
int rangeSize = (data.Length > 5) ? 5 : data.Length;
for (int start = 0; start < data.Length - rangeSize; start++)
{
var fiveDayTotal = growingDegreeDays with { TempRecords = data[start..(start + rangeSize)] };
movingAccumulation.Add(fiveDayTotal);
}
Console.WriteLine();
Console.WriteLine("Total degree days in the last five days");
foreach(var item in movingAccumulation)
{
Console.WriteLine(item);
}
// </RunningFiveDayTotal>
}
}This syntax gives you concise data modeling with immutable value semantics.
Add behavior to the record struct
In DailyTemperature.cs, the record struct already has a computed Mean property:
public double Mean => (HighTemp + LowTemp) / 2.0;A record struct works well here because each value is small and self-contained.
Build record types for degree-day calculations
Create a file named DegreeDays.cs with a hierarchy for heating and cooling degree-day calculations:
sample · csharpdocs/csharp/fundamentals/tutorials/snippets/records/InterimSteps.cs
namespace InterimSteps
{
// <DailyRecord>
public readonly record struct DailyTemperature(double HighTemp, double LowTemp);
// </DailyRecord>
}
namespace InterimSteps2
{
public readonly record struct DailyTemperature(double HighTemp, double LowTemp)
{
public double Mean => (HighTemp + LowTemp) / 2.0;
}
// <DegreeDaysRecords>
public abstract record DegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords);
public sealed record HeatingDegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords)
: DegreeDays(BaseTemperature, TempRecords)
{
public double DegreeDays => TempRecords.Where(s => s.Mean < BaseTemperature).Sum(s => BaseTemperature - s.Mean);
}
public sealed record CoolingDegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords)
: DegreeDays(BaseTemperature, TempRecords)
{
public double DegreeDays => TempRecords.Where(s => s.Mean > BaseTemperature).Sum(s => s.Mean - BaseTemperature);
}
// </DegreeDaysRecords>
}Now calculate totals from your Main method in Program.cs:
sample · csharpdocs/csharp/fundamentals/tutorials/snippets/records/Program.cs
namespace record_types;
class Program
{
// <DeclareData>
private static DailyTemperature[] data = [
new DailyTemperature(HighTemp: 57, LowTemp: 30),
new DailyTemperature(60, 35),
new DailyTemperature(63, 33),
new DailyTemperature(68, 29),
new DailyTemperature(72, 47),
new DailyTemperature(75, 55),
new DailyTemperature(77, 55),
new DailyTemperature(72, 58),
new DailyTemperature(70, 47),
new DailyTemperature(77, 59),
new DailyTemperature(85, 65),
new DailyTemperature(87, 65),
new DailyTemperature(85, 72),
new DailyTemperature(83, 68),
new DailyTemperature(77, 65),
new DailyTemperature(72, 58),
new DailyTemperature(77, 55),
new DailyTemperature(76, 53),
new DailyTemperature(80, 60),
new DailyTemperature(85, 66)
];
// </DeclareData>
static void Main(string[] args)
{
// <HeatingAndCooling>
var heatingDegreeDays = new HeatingDegreeDays(65, data);
Console.WriteLine(heatingDegreeDays);
var coolingDegreeDays = new CoolingDegreeDays(65, data);
Console.WriteLine(coolingDegreeDays);
// </HeatingAndCooling>
// <GrowingDegreeDays>
// Growing degree days measure warming to determine plant growing rates
var growingDegreeDays = coolingDegreeDays with { BaseTemperature = 41 };
Console.WriteLine(growingDegreeDays);
// </GrowingDegreeDays>
// <RunningFiveDayTotal>
// showing moving accumulation of 5 days using range syntax
List<CoolingDegreeDays> movingAccumulation = new();
int rangeSize = (data.Length > 5) ? 5 : data.Length;
for (int start = 0; start < data.Length - rangeSize; start++)
{
var fiveDayTotal = growingDegreeDays with { TempRecords = data[start..(start + rangeSize)] };
movingAccumulation.Add(fiveDayTotal);
}
Console.WriteLine();
Console.WriteLine("Total degree days in the last five days");
foreach(var item in movingAccumulation)
{
Console.WriteLine(item);
}
// </RunningFiveDayTotal>
}
}The generated ToString output is useful for quick diagnostics while you iterate.
Override PrintMembers to customize output
When the default output includes too much noise, override PrintMembers in the base record:
sample · csharpdocs/csharp/fundamentals/tutorials/snippets/records/DegreeDays.cs
using System.Text;
namespace record_types;
public abstract record DegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords)
{
// <AddPrintMembers>
protected virtual bool PrintMembers(StringBuilder stringBuilder)
{
stringBuilder.Append($"BaseTemperature = {BaseTemperature}");
return true;
}
// </AddPrintMembers>
}
public record HeatingDegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords)
: DegreeDays(BaseTemperature, TempRecords)
{
public double DegreeDays => TempRecords.Where(s => s.Mean < BaseTemperature).Sum(s => BaseTemperature - s.Mean);
}
public sealed record CoolingDegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords)
: DegreeDays(BaseTemperature, TempRecords)
{
public double DegreeDays => TempRecords.Where(s => s.Mean > BaseTemperature).Sum(s => s.Mean - BaseTemperature);
}The override keeps the output focused on the information you need.
Use with expressions for nondestructive mutation
Use with to create modified copies without mutating the original record:
sample · csharpdocs/csharp/fundamentals/tutorials/snippets/records/Program.cs
namespace record_types;
class Program
{
// <DeclareData>
private static DailyTemperature[] data = [
new DailyTemperature(HighTemp: 57, LowTemp: 30),
new DailyTemperature(60, 35),
new DailyTemperature(63, 33),
new DailyTemperature(68, 29),
new DailyTemperature(72, 47),
new DailyTemperature(75, 55),
new DailyTemperature(77, 55),
new DailyTemperature(72, 58),
new DailyTemperature(70, 47),
new DailyTemperature(77, 59),
new DailyTemperature(85, 65),
new DailyTemperature(87, 65),
new DailyTemperature(85, 72),
new DailyTemperature(83, 68),
new DailyTemperature(77, 65),
new DailyTemperature(72, 58),
new DailyTemperature(77, 55),
new DailyTemperature(76, 53),
new DailyTemperature(80, 60),
new DailyTemperature(85, 66)
];
// </DeclareData>
static void Main(string[] args)
{
// <HeatingAndCooling>
var heatingDegreeDays = new HeatingDegreeDays(65, data);
Console.WriteLine(heatingDegreeDays);
var coolingDegreeDays = new CoolingDegreeDays(65, data);
Console.WriteLine(coolingDegreeDays);
// </HeatingAndCooling>
// <GrowingDegreeDays>
// Growing degree days measure warming to determine plant growing rates
var growingDegreeDays = coolingDegreeDays with { BaseTemperature = 41 };
Console.WriteLine(growingDegreeDays);
// </GrowingDegreeDays>
// <RunningFiveDayTotal>
// showing moving accumulation of 5 days using range syntax
List<CoolingDegreeDays> movingAccumulation = new();
int rangeSize = (data.Length > 5) ? 5 : data.Length;
for (int start = 0; start < data.Length - rangeSize; start++)
{
var fiveDayTotal = growingDegreeDays with { TempRecords = data[start..(start + rangeSize)] };
movingAccumulation.Add(fiveDayTotal);
}
Console.WriteLine();
Console.WriteLine("Total degree days in the last five days");
foreach(var item in movingAccumulation)
{
Console.WriteLine(item);
}
// </RunningFiveDayTotal>
}
}Extend that idea to compute rolling totals from slices of your input data:
sample · csharpdocs/csharp/fundamentals/tutorials/snippets/records/Program.cs
namespace record_types;
class Program
{
// <DeclareData>
private static DailyTemperature[] data = [
new DailyTemperature(HighTemp: 57, LowTemp: 30),
new DailyTemperature(60, 35),
new DailyTemperature(63, 33),
new DailyTemperature(68, 29),
new DailyTemperature(72, 47),
new DailyTemperature(75, 55),
new DailyTemperature(77, 55),
new DailyTemperature(72, 58),
new DailyTemperature(70, 47),
new DailyTemperature(77, 59),
new DailyTemperature(85, 65),
new DailyTemperature(87, 65),
new DailyTemperature(85, 72),
new DailyTemperature(83, 68),
new DailyTemperature(77, 65),
new DailyTemperature(72, 58),
new DailyTemperature(77, 55),
new DailyTemperature(76, 53),
new DailyTemperature(80, 60),
new DailyTemperature(85, 66)
];
// </DeclareData>
static void Main(string[] args)
{
// <HeatingAndCooling>
var heatingDegreeDays = new HeatingDegreeDays(65, data);
Console.WriteLine(heatingDegreeDays);
var coolingDegreeDays = new CoolingDegreeDays(65, data);
Console.WriteLine(coolingDegreeDays);
// </HeatingAndCooling>
// <GrowingDegreeDays>
// Growing degree days measure warming to determine plant growing rates
var growingDegreeDays = coolingDegreeDays with { BaseTemperature = 41 };
Console.WriteLine(growingDegreeDays);
// </GrowingDegreeDays>
// <RunningFiveDayTotal>
// showing moving accumulation of 5 days using range syntax
List<CoolingDegreeDays> movingAccumulation = new();
int rangeSize = (data.Length > 5) ? 5 : data.Length;
for (int start = 0; start < data.Length - rangeSize; start++)
{
var fiveDayTotal = growingDegreeDays with { TempRecords = data[start..(start + rangeSize)] };
movingAccumulation.Add(fiveDayTotal);
}
Console.WriteLine();
Console.WriteLine("Total degree days in the last five days");
foreach(var item in movingAccumulation)
{
Console.WriteLine(item);
}
// </RunningFiveDayTotal>
}
}This approach is useful when you need transformations while you preserve original values.
Next steps
- Review C# record types for deeper guidance.
- Continue with Object-oriented C# for broader design patterns.
- Explore Converting types to combine records with safe conversion patterns.