Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
A meetup needs badge labels in name order. Each attendee requests one or more copies, and each label includes a message for the attendee's role. In this tutorial, you combine small C# techniques to produce the labels without a separate collection of formatted strings.
This tutorial is for developers who know basic C# methods, loops, and collections. You choose where short operations belong, make helper dependencies explicit, and decide when the program produces results. For the reasoning behind these choices, see Building software.
Prerequisites
- The .NET 10 SDK.
- A text editor and a terminal.
Start with one label per attendee
Create a folder named
BadgeLabelsand a file namedProgram.csinside it.Copy the complete source in the following section into
Program.cs. Use this same file for every step and experiment:#define BADGE_SORT #define BADGE_COPIES #define BADGE_FORMAT #define BADGE_COUNT // #define BADGE_DIAGNOSTICS // #define BADGE_REPEAT // #define BADGE_SNAPSHOT Attendee[] attendees = [ new("Ben", "Host", 1), new("Ada", "Guest", 2) ]; #if BADGE_SORT #if BADGE_DIAGNOSTICS Console.WriteLine("Before query definition."); #endif var orderedAttendees = attendees.OrderBy( #if BADGE_DIAGNOSTICS attendee => { Console.WriteLine($"Sort key: {attendee.Name}"); return attendee.Name; }, StringComparer.Ordinal); #else attendee => attendee.Name, StringComparer.Ordinal); #endif #if BADGE_DIAGNOSTICS Console.WriteLine("After query definition."); #endif #else var orderedAttendees = attendees; #endif #if BADGE_SNAPSHOT var attendeesToPrint = orderedAttendees.ToList(); #else var attendeesToPrint = orderedAttendees; #endif int passes = 1; #if BADGE_REPEAT passes = 2; #endif for (int pass = 0; pass < passes; pass++) { #if BADGE_COUNT int expected = attendees.Sum(attendee => attendee.Copies); int printed = 0; #endif foreach (Attendee attendee in attendeesToPrint) { #if BADGE_COPIES var labels = CreateLabels(attendee); #if BADGE_DIAGNOSTICS Console.WriteLine($"Sequence created: {attendee.Name}"); #endif foreach (string label in labels) { Console.WriteLine(label); #if BADGE_COUNT printed++; #endif } #else Console.WriteLine($"{attendee.Name} | {attendee.Role}"); #endif } #if BADGE_COUNT if (printed != expected) { throw new InvalidOperationException("Label count doesn't match the request."); } Console.WriteLine($"Printed {printed} of {expected} requested labels."); #endif } #if BADGE_COPIES static IEnumerable<string> CreateLabels(Attendee attendee) { #if BADGE_DIAGNOSTICS Console.WriteLine($"Iterator started: {attendee.Name}"); #endif ArgumentOutOfRangeException.ThrowIfNegativeOrZero(attendee.Copies); #if BADGE_FORMAT string message = attendee.Role switch { "Guest" => "Welcome!", "Host" => "Ask me for help", _ => throw new ArgumentException( $"Unsupported role: {attendee.Role}.", nameof(attendee)) }; #endif for (int copy = 1; copy <= attendee.Copies; copy++) { #if BADGE_FORMAT yield return FormatLabel( attendee.Name, attendee.Role, message, copy, attendee.Copies); #else yield return $"{attendee.Name} | {attendee.Role} [{copy}/{attendee.Copies}]"; #endif } #if BADGE_FORMAT static string FormatLabel( string name, string role, string message, int copy, int total) => $"{name} | {role} | {message} [{copy}/{total}]"; #endif } #endif record Attendee(string Name, string Role, int Copies);At the top of the file, comment out the four active
#definelines. Leave the three experiment lines commented out, too.From that folder, run the file-based app:
dotnet run --file Program.cs
The program writes:
Ben | Host
Ada | Guest
The file contains every stage. From now on, change only the #define lines at the top, not method bodies or declarations. A #if directive includes code only when its symbol is defined; an #else branch supplies the alternative. See Conditional compilation.
Uncomment one additional stage line at each step, and keep the previous stage lines enabled:
| Stage | Active stage symbols |
|---|---|
| One label per attendee | None |
| Sort attendees | BADGE_SORT |
| Produce copies | BADGE_SORT, BADGE_COPIES |
| Format messages | BADGE_SORT, BADGE_COPIES, BADGE_FORMAT |
| Verify the count | BADGE_SORT, BADGE_COPIES, BADGE_FORMAT, BADGE_COUNT |
The downloaded source enables all four stage symbols, so it runs the final program by default. The other three symbols control optional experiments and stay off during the normal progression. These symbols use a BADGE_ prefix to avoid SDK-defined symbols such as TRACE.
The following excerpts highlight code already in your file. Don't copy them over the complete program.
Attendee[] attendees =
[
new("Ben", "Host", 1),
new("Ada", "Guest", 2)
];
The Attendee declaration defines a record, a type that groups related data and supplies value-based equality. Each attendee has a Name, a Role, and a requested number of Copies. A record fits this small data model because the program passes attendee data between operations without changing it. For more about this type, see Records.
The fixed input uses an array and a collection expression. In each new(...) call, C# infers Attendee from the array element type; see Target-typed new. Top-level statements let you run the example without writing a Main method.
The foreach loop reads each attendee, and an interpolated string inserts their values into the output. The loop currently ignores Copies.
Keep this small input as you develop the program: Ben requests one copy, and Ada requests two. Only Guest and Host are supported roles, and copy counts must be positive.
Sort with a lambda
To put attendees in name order, uncomment #define BADGE_SORT. Leave the other symbols commented out. The sort branch defines this query:
var orderedAttendees = attendees.OrderBy(
#if BADGE_DIAGNOSTICS
attendee =>
{
Console.WriteLine($"Sort key: {attendee.Name}");
return attendee.Name;
}, StringComparer.Ordinal);
#else
attendee => attendee.Name, StringComparer.Ordinal);
#endif
With diagnostics off, the #else branch supplies the short lambda. The loop prints the sorted attendees:
foreach (Attendee attendee in attendeesToPrint)
{
#if BADGE_COPIES
var labels = CreateLabels(attendee);
#if BADGE_DIAGNOSTICS
Console.WriteLine($"Sequence created: {attendee.Name}");
#endif
foreach (string label in labels)
{
Console.WriteLine(label);
#if BADGE_COUNT
printed++;
#endif
}
#else
Console.WriteLine($"{attendee.Name} | {attendee.Role}");
#endif
}
Run the same command. The output now puts Ada first:
Ada | Guest
Ben | Host
attendee => attendee.Name is a lambda expression, an unnamed function supplied where code needs behavior. Here, the lambda selects the name to sort by. The lambda stays inline, directly in the OrderBy call, because it's short and its purpose is clear there. You could extract the selection into a named helper method, but a method name wouldn't explain this one-property selection better. See Lambda expressions.
OrderBy is part of Language Integrated Query (LINQ), a set of C# features and .NET APIs for queries over data. See LINQ queries.
This example uses method syntax, which calls LINQ methods directly. In contrast, query syntax uses C# clauses such as from, orderby, and select:
var orderedAttendees =
from attendee in attendees
orderby attendee.Name
select attendee;
The compiler translates query syntax into method calls. Query syntax can make a query with several clauses easier to read; method syntax keeps this single sort short and lets you pass a comparer to OrderBy. The query-syntax example uses the default string comparer, so it doesn't guarantee the same order across cultures. Keep the method-syntax version for this program. See LINQ method and query syntax.
To keep the output independent of the machine's culture, the sample uses Ordinal for name comparisons. For the short names here, that order is alphabetical. See Sort data with LINQ.
The var declaration lets the compiler infer the query variable's type. The orderedAttendees variable describes a query, not a completed sorted collection. To enumerate a sequence means to read its elements one at a time. OrderBy reads and sorts all the attendees when foreach first requests an element.
Observe the order of execution
Deferred execution means work starts when code enumerates a sequence rather than when code defines the query. See Run a query. Use diagnostic output to see the difference:
Keep only
BADGE_SORTenabled among the stage symbols.Uncomment
#define BADGE_DIAGNOSTICSat the top of the same file. The diagnostic branches surround the query definition with messages and use a lambda that prints each sort key:#if BADGE_SORT #if BADGE_DIAGNOSTICS Console.WriteLine("Before query definition."); #endif var orderedAttendees = attendees.OrderBy( #if BADGE_DIAGNOSTICS attendee => { Console.WriteLine($"Sort key: {attendee.Name}"); return attendee.Name; }, StringComparer.Ordinal); #else attendee => attendee.Name, StringComparer.Ordinal); #endif #if BADGE_DIAGNOSTICS Console.WriteLine("After query definition."); #endif #else var orderedAttendees = attendees; #endifRun
dotnet run --file Program.csagain. Before you read the output, predict whetherSort key:appears before or afterAfter query definition..
The output is:
Before query definition.
After query definition.
Sort key: Ben
Sort key: Ada
Ada | Guest
Ben | Host
No sort keys appear between the two query-definition messages. The lambda runs only when foreach requests the first result. Both key messages appear before the first label because OrderBy needs every key before it can determine which attendee comes first. The keys follow the input order, Ben then Ada; the results follow the sorted order, Ada then Ben.
The diagnostic lambda needs both a message and a returned key, so it uses a block statement body with braces and return, instead of the original single-expression body.
With diagnostics on, try the two experiment switches in the same file. No changes to the loops or query are needed:
Uncomment
#define BADGE_REPEATand run again:Before query definition. After query definition. Sort key: Ben Sort key: Ada Ada | Guest Ben | Host Sort key: Ben Sort key: Ada Ada | Guest Ben | HostThe second pass repeats the sort. The query variable doesn't cache results.
Keep
BADGE_REPEATenabled, uncomment#define BADGE_SNAPSHOT, and run again:Before query definition. After query definition. Sort key: Ben Sort key: Ada Ada | Guest Ben | Host Ada | Guest Ben | HostThe two key messages now appear only once, during
ToList, before either pass prints labels. Both passes read the stored attendees.
These switches select the sequence to enumerate and the number of passes:
#if BADGE_SNAPSHOT
var attendeesToPrint = orderedAttendees.ToList();
#else
var attendeesToPrint = orderedAttendees;
#endif
int passes = 1;
#if BADGE_REPEAT
passes = 2;
#endif
Eager execution means an operation runs immediately instead of waiting for later enumeration. ToList executes the query immediately and stores its results in a list. Both passes now read the stored attendees without running the sort again. In contrast, defining the OrderBy query still defers the sort: it reads and buffers all input only when enumeration begins. Buffering doesn't mean the query executes when you define it. Use a snapshot when you need to reuse results without recalculating them; keep the deferred query when you want each enumeration to read the current input. See Compose queries with cached results.
Before you continue, comment out BADGE_DIAGNOSTICS, BADGE_REPEAT, and BADGE_SNAPSHOT again. Keep BADGE_SORT enabled. Run the app to confirm that only the two sorted labels appear.
Produce the requested copies with an iterator
To expand each attendee into the requested number of labels, uncomment #define BADGE_COPIES alongside BADGE_SORT. The loop now selects the BADGE_COPIES branch shown earlier and calls the iterator already in the file:
#if BADGE_COPIES
static IEnumerable<string> CreateLabels(Attendee attendee)
{
#if BADGE_DIAGNOSTICS
Console.WriteLine($"Iterator started: {attendee.Name}");
#endif
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(attendee.Copies);
#if BADGE_FORMAT
string message = attendee.Role switch
{
"Guest" => "Welcome!",
"Host" => "Ask me for help",
_ => throw new ArgumentException(
$"Unsupported role: {attendee.Role}.", nameof(attendee))
};
#endif
for (int copy = 1; copy <= attendee.Copies; copy++)
{
#if BADGE_FORMAT
yield return FormatLabel(
attendee.Name, attendee.Role, message, copy, attendee.Copies);
#else
yield return $"{attendee.Name} | {attendee.Role} [{copy}/{attendee.Copies}]";
#endif
}
#if BADGE_FORMAT
static string FormatLabel(
string name, string role, string message, int copy, int total) =>
$"{name} | {role} | {message} [{copy}/{total}]";
#endif
}
#endif
Run the app again:
Ada | Guest [1/2]
Ada | Guest [2/2]
Ben | Host [1/1]
CreateLabels is an iterator, a function that produces sequence elements one at a time. Its IEnumerable<T> return type, written as IEnumerable<string>, represents a sequence of strings. Each yield return supplies one label to the inner foreach loop, which prints it before requesting the next label. See Iterators.
The for loop counts copies from 1 through attendee.Copies. One attendee produces as many strings as Copies requests. A collection would also work, but the caller only needs each label once. The iterator avoids a separate label collection and keeps the copy-number loop together. The outer and inner foreach loops separate attendee order from label production; the caller remains responsible for printing.
Calling CreateLabels(attendee) doesn't run its body. Like the LINQ query, the iterator uses deferred execution. The inner foreach starts that work, including the positive-copy check. A new enumeration runs the iterator again. Unlike OrderBy, which reads and sorts all attendees before its first result, this iterator supplies a label without creating the remaining labels.
To observe the iterator's order of execution, uncomment #define BADGE_DIAGNOSTICS again. Keep BADGE_REPEAT and BADGE_SNAPSHOT off. Diagnostics now show both the sort and the iterator:
Before query definition.
After query definition.
Sort key: Ben
Sort key: Ada
Sequence created: Ada
Iterator started: Ada
Ada | Guest [1/2]
Ada | Guest [2/2]
Sequence created: Ben
Iterator started: Ben
Ben | Host [1/1]
For each attendee, the sequence-created message appears before the iterator-started message and the labels. Creating the sequence doesn't enter the iterator body; requesting its first element does. Comment out BADGE_DIAGNOSTICS again before you continue.
The same experiment controls remain available in the later stages. BADGE_REPEAT enumerates the attendees twice and creates each attendee's label sequence again. BADGE_SNAPSHOT caches only the sorted attendees, not label strings, so it prevents another sort but doesn't prevent the iterator from running again on each pass.
The ThrowIfNegativeOrZero check reports invalid input instead of silently producing no labels. An exception reports a failure that interrupts normal execution; see Create and throw exceptions. Here, an invalid copy count stops the program instead of silently skipping that attendee.
Calculate a message and name the formatting step
Each role needs a different message. Uncomment #define BADGE_FORMAT, and leave BADGE_SORT and BADGE_COPIES enabled. The same iterator now includes the message calculation:
#if BADGE_FORMAT
string message = attendee.Role switch
{
"Guest" => "Welcome!",
"Host" => "Ask me for help",
_ => throw new ArgumentException(
$"Unsupported role: {attendee.Role}.", nameof(attendee))
};
#endif
It also selects the yield return FormatLabel(...) branch and includes this local helper:
#if BADGE_FORMAT
static string FormatLabel(
string name, string role, string message, int copy, int total) =>
$"{name} | {role} | {message} [{copy}/{total}]";
#endif
Run the app again:
Ada | Guest | Welcome! [1/2]
Ada | Guest | Welcome! [2/2]
Ben | Host | Ask me for help [1/1]
The switch expression calculates the message once per attendee, before the copy loop. An expression fits because each role selects one value to assign to message. An if/else chain or a switch statement would work, but would repeat the assignment in each branch.
Pattern matching tests an expression against a specified pattern. Here, attendee.Role is the input expression, and the constant patterns "Guest" and "Host" test its evaluated value to select the matching message. The discard pattern _ matches any other role and throws an exception, so unsupported roles don't receive a misleading default message. See Pattern matching.
The nameof operator supplies the parameter name attendee to the exception. Unlike a hard-coded string, that name stays correct if you rename the parameter with a refactoring tool.
FormatLabel is a local function, a named function declared inside another function. Because only CreateLabels needs the formatting operation, its placement inside CreateLabels limits the helper to the operation that uses it. A private member method on a class would make the helper available to other members of that class. Choose that broader scope if several operations need the same formatting operation. See Local functions.
The name FormatLabel lets you follow the copy loop without reading the formatting details. Keeping the interpolated string directly in yield return would also work, as it did in the previous step. The name becomes more useful now that a label includes a role-specific message. The helper's => expression body returns the one formatted string.
All formatting inputs arrive through parameters. A non-static local function could capture, or use, variables from its surrounding scope. The static modifier prevents that capture, so its dependencies stay explicit. See Parameters and captured state.
Because CreateLabels is still an iterator, the role check also runs only when enumeration starts. For this controlled input, those checks belong with label production. The program doesn't promise to validate all attendees before it prints any labels.
Verify the total and run the complete program
To compare actual label production with the requested count, uncomment #define BADGE_COUNT. All four stage symbols are now enabled. Leave the three experiment symbols commented out for the normal final output.
The count setup runs before the attendee loop:
#if BADGE_COUNT
int expected = attendees.Sum(attendee => attendee.Copies);
int printed = 0;
#endif
Sum uses eager execution to add the copy counts from the input array. Unlike OrderBy, it returns one number, not a sequence to enumerate later. Its lambda, attendee => attendee.Copies, selects the copy count inline. As with the name selection, a separate helper method wouldn't clarify this short expression. A loop with an accumulator would also work; Sum states directly that you want a total. See Common LINQ methods.
The printed++ increment in the label loop increases the counter only when the loop receives a label. After that loop, the if statement compares that count with the requested total and throws an exception if they differ:
#if BADGE_COUNT
if (printed != expected)
{
throw new InvalidOperationException("Label count doesn't match the request.");
}
Console.WriteLine($"Printed {printed} of {expected} requested labels.");
#endif
You've reached the default configuration of the complete program you copied at the start. No methods or declarations needed to change:
#define BADGE_SORT
#define BADGE_COPIES
#define BADGE_FORMAT
#define BADGE_COUNT
// #define BADGE_DIAGNOSTICS
// #define BADGE_REPEAT
// #define BADGE_SNAPSHOT
Run it with dotnet run --file Program.cs. The output is:
Ada | Guest | Welcome! [1/2]
Ada | Guest | Welcome! [2/2]
Ben | Host | Ask me for help [1/1]
Printed 3 of 3 requested labels.
The input array is already in memory. Sum reads it immediately, OrderBy sorts it when the attendee loop begins, and CreateLabels produces strings as the inner loop advances. The program never builds a separate collection of labels. For broader query guidance, see LINQ and collections.
To revisit the diagnostics with the final program, enable BADGE_DIAGNOSTICS alone among the experiment symbols. The output is:
Before query definition.
After query definition.
Sort key: Ben
Sort key: Ada
Sequence created: Ada
Iterator started: Ada
Ada | Guest | Welcome! [1/2]
Ada | Guest | Welcome! [2/2]
Sequence created: Ben
Iterator started: Ben
Ben | Host | Ask me for help [1/1]
Printed 3 of 3 requested labels.
If you also enable BADGE_REPEAT, each pass checks and reports its own count. With BADGE_SNAPSHOT enabled, the sort keys appear only once, but the iterator messages, labels, and count report still appear on each pass. Comment out the experiment symbols to restore the normal final output.
Summary
Each technique serves a different part of the same task:
- Lambdas and LINQ select names and copy counts inline for the sort and total.
- An iterator expresses custom one-to-many production and leaves label consumption to the caller.
- A
switchexpression calculates a role-specific value and makes unsupported roles explicit. - A local function names the formatting step, limits its scope, and states its inputs.
Choose these forms when their scope, dependencies, and order of execution fit the operation. A direct loop, a shared method, or a completed collection can fit other callers better.