Cleaning Up ASP.NET Data Display Using Component Model Attributes

In my last article, I talked about how you can use ASP.NET Editor Templates and Display Templates with the Html helper methods to keep your display logic understandable, maintainable, and organized. If you ran the sample application (or looked at some of the screenshots in the article) you might have noticed that the formatting of the field names was kind of messed up. For example, "LastName" instead of "Last Name" and "FirstName" instead of "First Name."

[Click on image for larger view.]

The reason that that is happening is because I'm using an ASP.NET helper method called DisplayNameFor(). Here's the code that creates that table header row that you see in the screenshot above.

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Id)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.LastName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.FirstName)
            </th>
            <th></th>
        </tr>
    </thead>
     ... 
</table>

Those calls to DisplayNameFor() use some compiler and/or reflection magic to grab the name of the property and turn it into a label string.

DisplayNameFor(): Sure, It's Cool ... but the Value Is Wrong
So DisplayNameFor() turns the property name into a label string. I suppose it's nice because it's compile-time checkable and it uses a compiled value rather than a hardcoded string for the label ... but the label is wrong. If you're using this for a not-that-important application, having a wrong label value might be fine ... but what if you want to use this in a production application?

What if you want the label to be "Last Name" instead of "LastName"? That helper method is just taking the property name and converting it to a string and C# doesn't allow you to have spaces in a property name so renaming the property to be "Last Name" isn't going to work.

public class Person
{
    public string Id { get; set; } = Guid.NewGuid().ToString();

    public string FirstName { get; set; } = string.Empty;
    
    public string LastName { get; set; } = string.Empty;
    
    public List<Address> Addresses { get; set; } = new();
}

Component Model Data Annotations to the Rescue!
If you want to start controlling what DisplayNameFor() outputs, there are some attributes that you'll want to check out. This brings us to the System.ComponentModel.DataAnnotations namespace.

If you want to customize the display name of a property, you can decorate it with the [Display()] attribute. The following code sample is the Person class modified to use the Display attribute.

using System.ComponentModel.DataAnnotations;

namespace Benday.AspNetTemplateDemo.Api;

public class Person
{
    public string Id { get; set; } = Guid.NewGuid().ToString();

    [Display(Name = "First Name")]
    public string FirstName { get; set; } = string.Empty;

    [Display(Name = "Last Name")]
    public string LastName { get; set; } = string.Empty;
    
    public List<Address> Addresses { get; set; } = new();
}

Now when I run the application, any place in the code that uses DisplayNameFor() now picks up the [Display] attribute value instead of simply using the raw property name.

[Click on image for larger view.]

Using Attributes to Format Data Values
So far in this article, I've talked about just DisplayNameFor() label values. But there are about a half-zillion other attributes in System.ComponentModel.DataAnnotations that can be used for other kinds of data formatting.

The two formatting things that I run into a lot are 1) formatting dates and 2) formatting decimals.

To demonstrate these, I'm going to add a handful of additional properties to the Person class:

  • BirthDate
  • HeightInInches
  • HeightInCentimeters
public class Person
{
    public string Id { get; set; } = Guid.NewGuid().ToString();

    [Display(Name = "First Name")]
    public string FirstName { get; set; } = string.Empty;

    [Display(Name = "Last Name")]
    public string LastName { get; set; } = string.Empty;
    
    public List<Address> Addresses { get; set; } = new();

    [Display(Name = "Birth Date")]
    public DateTime BirthDate { get; set; } = DateTime.UtcNow;

    [Display(Name = "Height (in))")]
    public float HeightInInches { get; set; } = 69.549281f;

    [Display(Name = "Height (cm)")]
    public float HeightInCentimeters
    {
        get
        {
            return HeightInInches * 2.54f;
        }
    }
}

I've added these to the Edit Person page in /Views/Person/Edit.cshtml using the <input asp-for=" ... " syntax. BTW, in case you're wondering, that's pretty much the equivalent of doing @Html.EditorFor().

<div class="form-group mb-3">
    <label asp-for="BirthDate" class="control-label"></label>
    <input type="text" asp-for="BirthDate" class="form-control" />
    <span asp-validation-for="BirthDate" class="text-danger"></span>
</div>
<div class="form-group mb-3">
    <label asp-for="HeightInInches" class="control-label"></label>
    <input asp-for="HeightInInches" class="form-control" />
    <span asp-validation-for="HeightInInches" class="text-danger"></span>
</div>
<div class="form-group mb-3">
    <label asp-for="HeightInCentimeters" class="control-label"></label>
    <input asp-for="HeightInCentimeters" class="form-control" />
    <span asp-validation-for="HeightInCentimeters" class="text-danger"></span>
</div>

Now when I go to the Edit Person page, I see those fields. But the values are kind of ugly. For BirthDate, I don't need all the time information and for the height fields, there are just way too many digits to the right of the decimal point.

[Click on image for larger view.]

To clean this up and adjust the formatting for the DateTime and float fields on Person, you need to add [DisplayFormat] attributes to those properties. The DisplayFormat attribute takes a data format string just like what you'd use if you were calling the ToString() method on an object.

If you want to format a DateTime to display just the date, the format string is {0:d}. To display a float with two decimal places, the format string is {0:F2}. Applying those changes to the Person class gives the following code.

public class Person
{
    public string Id { get; set; } = Guid.NewGuid().ToString();

    [Display(Name = "First Name")]
    public string FirstName { get; set; } = string.Empty;

    [Display(Name = "Last Name")]
    public string LastName { get; set; } = string.Empty;

    public List<Address> Addresses { get; set; } = new();

    [Display(Name = "Birth Date")]
    [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:d}")]
    public DateTime BirthDate { get; set; } = DateTime.UtcNow;

    [Display(Name = "Height (in))")]
    [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:F2}")]
    public float HeightInInches { get; set; } = 69.549281f;

    [Display(Name = "Height (cm)")]
    [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:F2}")]
    public float HeightInCentimeters
    {
        get
        {
            return HeightInInches * 2.54f;
        }
    }
}

When you run the application, now you get input boxes that display the data using those format strings.

[Click on image for larger view.]

Summary
Pretty cool, huh? There are so many things in the System.ComponentModel.DataAnnotations namespace to look in to and it's another one of those 'hidden in plain sight' features of ASP.NET Core.

If you want to check out the source code, it's available here

About the Author

Benjamin Day is a consultant, trainer and author specializing in software development, project management and leadership.

Posted by Benjamin Day on 04/21/20250 comments


Burnout in Software Development

Change-bringer and 21-time Microsoft MVP Robert Bogue studies employee burnout, but he doesn't believe in work-life balance. He believes in work-life harmony. It's an interesting difference.

Long hours, shifting priorities, and the relentless pressure to deliver clean code under tight deadlines -- these are just a few of the stressors that define life in modern software development. While the work can be deeply fulfilling, it can also take a mental and emotional toll, leading many developers down the path of burnout. It's a phenomenon most in the industry have either experienced firsthand or witnessed in colleagues: the creeping exhaustion, rising cynicism, and sense of ineffectiveness that can sap motivation and stifle innovation.

At the Visual Studio Live! developer education conference taking place at Microsoft headquarters in Redmond in August, the veteran consultant and developer advocate returns to the community to tackle this issue head-on with his session, Burnout in Software Development. Drawing on more than two decades of experience and research -- including his co-authored book "Extinguish Burnout: A Practical Guide to Prevention and Recovery" -- Bogue offers both personal perspective and practical tools to help developers and organizations spot the warning signs, intervene early, and create healthier, more sustainable work environments.

The session is designed for attendees at an introductory to intermediate level and will explore the root causes of burnout in technical roles, outline effective strategies for recovery, and provide insights into building cultures that promote efficacy and resilience. Whether you're an individual contributor feeling stretched thin or a team leader looking to better support your developers, this session promises actionable advice grounded in both science and lived experience.

In the following Q&A, Bogue discusses what inspired his return to this important topic, how developers can recognize burnout in themselves and others, and why organizations need to rethink their approach to culture, productivity, and learning.

VSLive! What inspired you to present a session on this topic?
Bogue: I first encountered burnout with my development team in 2003. I wrote about it, then largely forgot about it. In 2018 we were struggling with one of our children and burnout impacted both my wife and me. The result of that was more research and work which culminated in the book "Extinguish Burnout: A Practical Guide to Prevention and Recovery," that we wrote for the Society for Human Resource Management (SHRM).

"I felt like I needed to return to my roots and share how burnout works and what can be done about it with my fellow developers"

Robert Bogue, President, Thor Projects LLC

While we've been doing these presentations to all types and sizes of organizations, I felt like I needed to return to my roots and share how burnout works and what can be done about it with my fellow developers. It's such an issue for us as we often don't get to see the results of our hard work.

Inside the Session

What: Burnout in Software Development

When: Aug. 5, 2025, 1:30 p.m. - 2:45 p.m.

Who: Robert Bogue, President, Thor Projects LLC

Why: Learn ways to both stay productive and feel effective doing the work you enjoy.

Find out more about VSLive! @ MS HQ taking place Aug. 4-8.

What are the early signs of burnout that developers should be aware of to take proactive measures?
Burnout is defined by exhaustion, cynicism, and lack of efficacy. If these things are becoming central to who you are and how you show up, there's a problem or potential problem. For me, I first experienced it as a lack of care. I just didn't care about my work, my craft, as a developer any longer. I felt like I was losing a part of who I was -- and I didn't know where I had lost it.

Can you share effective strategies for maintaining a healthy work-life balance in the software development industry?
I don't believe in work-life balance. I believe in work-life harmony. The difference is that balance is something that you can't achieve in a permanent sense. There will always be the big project. There will always be the things at home that disrupt your ability to work. When I'm looking for harmony, I'm trying to make things fit together and realizing that sometimes one part is in the front and other times there are other parts of my life that need to take precedence. I think that one of the great things about our work is that we get to spend so much time in flow. That can be powerful and addicting. We need to find ways to value and invest in other parts of our life as well.

What role does company culture play in preventing or contributing to developer burnout?
Most corporate cultures belong in a petri dish. I've got 30+ years of consulting experience and it's rare to find a universal positive culture. For organizations that want the benefits of burnout proofing, they'll want to help people feel more effective. In the session, I'll give direct advice to leaders about how to recognize results, ensure support, prioritize self-care, and how to carefully manage demands. To the extent that organizations don't encourage people to feel effective, they're encouraging burnout.

How can team leaders identify and address burnout within their development teams?
Identification is most visible with cynicism. They just don't believe they can make a change anymore. Addressing burnout is often as simple as focusing on how to help developers be -- and feel -- more productive.

How can organizations support continuous learning without overwhelming their developers?
Learning is so important as a developer. We must find ways to learn what we need to learn to be productive. One of the things that I love about the VSLive! Events is the way that we can all come together and learn new things that are both practical and relevant to what we're doing. Organizations have defaulted to older mechanisms of training that don't have the richness of conversation. I think that learning how to identify which types of learning are simply skill acquisition and which ones are craft enhancement is key to organizations enabling developers to have the right experiences and achieve the right results.

What resources can attendees use to learn more about this topic and prepare for your session?
Attendees are encouraged to look at the over 60 articles and other resources we have on the https://ExtinguishBurnout.com site. It's a good primer for what they can do to start preventing and recovering from burnout today. And, they're always welcome to reach out via one of my web sites or via LinkedIn.

Save $400 when you register for VSLive! @ Microsoft HQ Conference ( Aug. 4-8) by the Super Early Bird savings deadline of June 6.

Posted by David Ramel on 04/15/20250 comments


ASP.NET View Re-use with Editor & Display Templates

Whenever I sit down to write one of these articles, I always struggle with whether to write about things that are super-advanced vs. super-basic. My goal though is to try to write about things that are somewhat obscure but handy in the hopes that I can smooth out the learning curve for someone -- especially people who are just getting going with .NET Core. ASP.NET Core has a zillion great features, but lots of them are hidden. One of my favorites (and the topic of this month's article) is ASP.NET Core Editor Templates and Display Templates.

When I'm writing an ASP.NET Core application and I need a web UI, my default is ASP.NET Core MVC. This means you'll be working with Controller classes and Views that are implemented with cshtml files.

Organization of an ASP.NET Core MVC Application
Let's say that you need to write a simple app to manage basic information about a people and their addresses. The Model classes for this application would probably look like the Person and Address classes in the screenshot below.

[Click on image for larger view.]

If this were a real application, there would probably be code for working with databases or Cosmos DB or services, but let's keep it simple and skip about those details. Ultimately, you'll get to an ASP.NET web application that looks something like the screenshot below. You'll have a folder called Controllers that will contain your ASP.NET Controller classes. That folder will have a class called PersonController that manages the list, add, edit, and delete operations for the Person records. The controllers folder is pretty simple to understand as long as you follow the naming convention of "Class Name" plus the word "Controller" -- PersonController, OrderController, InvoiceController, etc.

A little further down in that project, there's going to be a Views folder and this folder is where the complexity and hidden features start to show up -- or, if you're just learning, completely fail to show up. Typically, for each Controller type that you have, you'll create a subfolder under views with the same name. In this case, there a model class named Person and a controller named PersonController so therefore we have a folder called "Views/Person."

[Click on image for larger view.]

Inside the "Views/Person" folder, you'll create a cshtml (razor) file for each way that you want to display the data. Just to keep it confusing, "Index.cshtml" is usually for lists of data. In the screenshot, Index.cshtml is for displaying lists of Person records and Edit.cshtml is for editing a specific Person record.

When you run the application and open up the editor for a Person, you'll see something like this (see screenshot) in your browser.

[Click on image for larger view.]

If you open the Edit.cshtml, you'll see code that looks like this.

<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group mb-3">
                <label asp-for="Id" class="control-label"></label>
                <input asp-for="Id" class="form-control" />
                <span asp-validation-for="Id" class="text-danger"></span>
            </div>
            <div class="form-group mb-3">
                <label asp-for="FirstName" class="control-label"></label>
                <input asp-for="FirstName" class="form-control" />
                <span asp-validation-for="FirstName" class="text-danger"></span>
            </div>
            <div class="form-group mb-3">
                <label asp-for="LastName" class="control-label"></label>
                <input asp-for="LastName" class="form-control" />
                <span asp-validation-for="LastName" class="text-danger"></span>
            </div>

            <div class="form-group mb-3">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>
[Click on image for larger view.]

Throughout the code, you'll see lots of things that say "asp-for" and "asp-validation-for." These are some helpers from ASP.NET and Razor that help you to create logic to display and edit properties on your Model classes.

Clearly, Razor knows a lot about your models. But wait! There's more!

Re-usable, Composable Pieces of 'View'
So far, this is only showing and editing stuff about the Person details. What about the address details? And what if you wanted to re-use View functionality? Or what if you wanted to display a bunch of stuff but didn't want to cram it all into a single CSHTML file? That's where templates come in.

Taking a look at the screenshot below, with a minor addition, we're able to tell ASP.NET and the Razor engine, "display the list of addresses." We can do this using the Html Helper's EditorFor method, @Html.EditorFor(model => model.Addresses, "AddressList").

[Click on image for larger view.]

This helper call says "take the list of Addresses from the Person model instance and display them using the 'AddressList' template."

These templates live in a folders that you have to know to create and come in two flavors: Display and Editor. Display is intended to give you a read-only rendering of the target and the Editors are intended to give you an editable version. In the screenshot below, I've got editor templates for Address and AddressList.

[Click on image for larger view.]

These folders live in either Views/Shared/EditorTemplates or Views/Shared/DisplayTemplates.

The screenshot below is for displaying a list of Address objects. This is what would be triggered by the @Html.EditorFor(model => model.Addresses, "AddressList") call in Edit.cshtml for the Person class. Using the @model directive, this cshtml template receives the list of Address classes that it's supposed to display. Once it has that list of addresses, it goes about displaying them.

What's helpful is that these editor templates can call other editor templates. In the case of the screenshot below, the AddressList template, uses @Html.EditorFor(model => item) to call out to a different editor template for displaying the details of a single Address.

[Click on image for larger view.]

When this runs and the user opens the Edit view for a Person, now we get the editor for a Person plus all the Addresses.

[Click on image for larger view.]

And the upside is that these are nicely separated into different files so that we don't have collisions in version control and we can theoretically use these in multiple places without having to duplicate code.

Summary
Editor and Display templates are easy to use but difficult to discover. You have to know that they're there as a feature. But once you know, it's simply a matter of creating the Views/Shared/EditorTemplates or Views/Shared/DisplayTemplates folders and then creating the cshtml files.

If you want to see the full source code for this, it's published on Github here.

About the Author

Benjamin Day is a consultant, trainer and author specializing in software development, project management and leadership.

Posted by Benjamin Day on 03/31/20250 comments


Under the Hood: How Do C# Top-Level Statements Actually Work?

I taught a C# programming class for a company just outside of Boston. The company was making a move towards .NET Core and C#, but a good chunk of their coders were Python experts not C# experts. They all knew how to program but moving over to C# and the world of Visual Studio was causing them headaches. So they brought me in to help them get up to speed.

These students peppered me with non-stop questions about C# and .NET Core and ASP.NET Core. Since they already knew how to write code, a lot of these questions were really insightful and eye-opening and made me realize how much stuff we know about what we do that we just never think about.

One of these questions was about Program.cs and C# and the difference between C# written with top-level statements and without top-level statements. The question was basically: "so how does that actually work?" Now that might sound like a real "noob" question -- and it is. But then when I went to start explaining it, I really understood how much magical stuff happens that is hidden from us.

Anyway, this article is inspired by that question.

'Hello,World' with and without C# Top-Level Statements
First, it's probably worth mentioning what C# top-level statements are. They're a feature that was added to C# 9.0 back in November of 2020. My (possibly wrong) understanding of why they were added to the language is so that there was less of a hurdle for people to get started coding with C#.

Let's look at "Hello, World" done in the old way before top-level statements.

using System;

namespace Benday.WithoutTopLevelStatements
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
        }
    }
}

If you're a veteran C# programmer, you'll look at this, know exactly what's going on, and your eye will quickly land on the call to Console.WriteLine(). "Yep. Got it. This is hello world."

But try to look at this with fresh eyes. For such a simple piece of software, there's actually kind of a lot of stuff in here. And if you're reading from top to bottom, you need to get through four different C# statements before you get to the part that actually prints "Hello, world" on the screen.

  1. using System;
  2. namespace
  3. class Program
  4. static void Main(string[] args)

If you're not familiar with C#, that's four instances of "uhhh what does that mean" before you get to the Console.WriteLine().

That's a pretty good barrier to entry for people looking to get started with C#.

Now let's take a look at 'Hello, World' with top-level statements:

Console.WriteLine("Hello, World!");

That's it. One line. Straight to the point.

"But How Does It Work?"
So I showed that student the two different versions. He paused for about 5 seconds ... and then hit me with three more questions.

  1. "Ok. Sure. It gets rid of a bunch of stuff. But how does it actually work?"
  2. "Which version is better?"
  3. "Are there performance differences?"

My reply was something like "Ooof. Dude, you're making me work for my fee!"

First the "how does it work" question. The answer is that the C# compiler makes a bunch of choices for you and adds in the missing stuff.

Next, "which version is better?" It all comes down to readability and maintainability. For small little apps that you don't expect to need to maintain over the long term, the top-level statements version is just fine -- you write your code and you move on. If you expect your application to get complex, then the version without top-level statements has some stuff that can help you stay organized.

Finally, "are there performance differences?" This one I didn't know off the top of my head. I suspected that there would be absolutely no difference but I didn't know for sure.

.NET Intermediate Language (IL) and ildasm
After class, I started thinking about whether there actually would be a difference between the two versions of the code. I also started wondering if I really knew what I was talking about when I said that there would be practically no difference between the two versions. I assumed they'd be just about the same but I didn't know that they'd be the same.

You might have heard the term managed code before in relation to C# and .NET. When you compile your C# code, what gets created is something called .NET Intermediate Language or "IL." (Pronounced, 'eye-ell'.) That IL gets written into your DLLs and EXEs and when you run your .NET applications, that IL is read and run by the Common Language Runtime (CLR).

If we're being 100% honest and transparent, when we compile our C# code, it's actually NOT compiling. It's turning it into this intermediate language which is basically a script. Which then begs the question "is C# actually an interpreted language?" I mean, you can't run IL directly, right? Correct, you can't run IL directly, but C# still isn't interpreted. What happens when you run your .NET applications is that the CLR takes the IL, compiles it into machine code, and then executes the machine code.

That's actually quite a bit of the "secret sauce" that allows .NET Core to be cross-platform. The .NET Runtime (which contains the CLR) has platform-specific versions and it's able to take the IL in your DLLs and run them on Windows, Linux, or Mac.

If you want to view the IL from your DLLs, there's a nifty little tool called ildasm2. To install it, go to your command prompt and type dotnet tool install dotnet-ildasm2 -g. Once you've done that, you get a command called ildasm (not ildasm2, btw) that will let you extract the IL from a DLL.

Comparing the IL for 'Hello, World'
So to get to the bottom of my student's "how does it work" question, I went home and created two version of 'Hello, World' with and without top-level statements. Then I used ildasm to extract the IL from the DLL.

By the way, if you're interested, I posted all of this to https://github.com/benday-inc/demo-ildasm-top-level-statements.

Here's the IL for the non-top-level statement version:

.class private auto ansi beforefieldinit Benday.WithoutTopLevelStatements.Program extends [System.Runtime]System.Object
{
  .method private hidebysig static default void Main(string[] args) cil managed
  {
    .custom instance void class [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(byte) = ( 01 00 01 00 00 ) // .....
    // Method begins at Relative Virtual Address (RVA) 0x2050
    .entrypoint
    // Code size 13 (0xD)
    .maxstack 8
    IL_0000: nop
    IL_0001: ldstr "Hello, World!"
    IL_0006: call void class [System.Console]System.Console::WriteLine(string)
    IL_000b: nop
    IL_000c: ret
  } // End of method System.Void Benday.WithoutTopLevelStatements.Program::Main(System.String[])
  .method public hidebysig specialname rtspecialname instance default void .ctor() cil managed
  {
    // Method begins at Relative Virtual Address (RVA) 0x205E
    // Code size 8 (0x8)
    .maxstack 8
    IL_0000: ldarg.0
    IL_0001: call instance void class [System.Runtime]System.Object::.ctor()
    IL_0006: nop
    IL_0007: ret
  } // End of method System.Void Benday.WithoutTopLevelStatements.Program::.ctor()
} // End of class Benday.WithoutTopLevelStatements.Program

And here's the IL for the version that uses top-level statements:

.class private auto ansi beforefieldinit Program extends [System.Runtime]System.Object
{
  .custom instance void class [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // ....
  .method private hidebysig static default void <Main>$(string[] args) cil managed
  {
    // Method begins at Relative Virtual Address (RVA) 0x2050
    .entrypoint
    // Code size 12 (0xC)
    .maxstack 8
    IL_0000: ldstr "Hello, World!"
    IL_0005: call void class [System.Console]System.Console::WriteLine(string)
    IL_000a: nop
    IL_000b: ret
  } // End of method System.Void Program::<Main>$(System.String[])
  .method public hidebysig specialname rtspecialname instance default void .ctor() cil managed
  {
    // Method begins at Relative Virtual Address (RVA) 0x205D
    // Code size 8 (0x8)
    .maxstack 8
    IL_0000: ldarg.0
    IL_0001: call instance void class [System.Runtime]System.Object::.ctor()
    IL_0006: nop
    IL_0007: ret
  } // End of method System.Void Program::.ctor()
} // End of class Program

Here's a screenshot of these two IL files side-by-side. The one on the left is the "without" top-level statements version and the one on the right is the "with" top-level statements version. The "without" version is 25 lines long and the "with" version is 24 lines long. Structurally, they're almost exactly the same.

[Click on image for larger view.]

There's only one real difference. The "without" version starts with a nop call. I'm not going to pretend that I 100% understand what's happening in the IL but nop means "no operation". Basically, the equivalent of saying "just hang out for a second and do nothing". After that, nop call, everything else is the same except for some line numbers.

Without Top-Level Statements With Top-Level Statements
IL_0000: nop IL_0000: ldstr "Hello, World!"
IL_0001: ldstr "Hello, World!" IL_0005: call void class [System.Console]System.Console::WriteLine(string)
IL_0006: call void class [System.Console]System.Console::WriteLine(string) IL_000a: nop
IL_000b: nop IL_000b: ret
IL_000c: ret

Summary
So all of that came from basically one student question in a C# course. It was a heck of a good question, actually. In short, top-level statements in C# generate almost exactly the same "executable" output and you can verify it by looking at the generated Intermediate Language (IL) using ildasm. All that stuff that isn't there in the top-level statement version of Hello, World, just gets added in by the compiler.

I hope you found this interesting. I realize that this is some pretty esoteric stuff. But I've gotta say that I love knowing how this stuff actually works.

BTW, if you want to look at the code and the IL output, you can get it here: https://github.com/benday-inc/demo-ildasm-top-level-statements.

About the Author

Benjamin Day is a consultant, trainer and author specializing in software development, project management and leadership.

Posted by Benjamin Day on 02/21/20250 comments


CallerMemberName: Your New Best Friend for Logging in C#

As I mentioned in my last post, I recently re-wrote the website and blog engine for my website. As part of that project and a different project of mine, I've been writing a lot of library code for CosmosDb and ASP.NET Core lately. One of the big things that I wanted was an easy way to log CosmosDb query details and ASP.NET Core MVC page request details.

I don't know about you but I've always found getting my logging code right to be kind of a tedious and messy process with a lot of hard-coded string values. Typically, those hard-coded strings are the "source" values for the log messages -- which class and which method is making the log call.

public class MyClass
{
  public void DoSomething() 
  {
    logger.WriteToLog("MyClass.DoSomething()", "did something");   
  }
}

You can make these calls a little more streamlined by using nameof but that only gets you so far. I wanted something a little more "automagical."

CallerMemberName Attribute
That "automagical" thing that I discovered is the CallerMemberName attribute. The method below is a utility method in CosmosRepository that creates a description for a query that runs against CosmosDb. Notice that the methodName parameter has a default value of empty string -- so if you call this method, you don't have to pass any value for methodName. But that parameter also has the CallerMemberName attribute attached to it, so when this method gets called, the compiler automatically drops in the name of the caller.

[Click on image for larger view.]

So lets say that you have the following code:

public class MyClass : CosmosRepository
{
  public void DoSomething() 
  {
    // calls GetQueryDescription()
 		logger.WriteToLog("MyClass.DoSomething()", GetQueryDescription());   
  }
}

When the logger.WriteToLog() line gets executed, it passes the value returned from GetQueryDescription() as the log message! I think that's unbelievably cool!

A Little Side-Note About CallerMemberName
As a disclaimer, I've been writing using C# for a long, long time. Decades. And when I discovered this attribute, I figured it was something new that had only recently been introduced.

Nope. Not new at all.

CallerMemberName was introduced as part of .NET Framework 4.5 in 2012. When .NET Core 1.0 dropped in 2016, it was there on the first day. I literally had never heard of this before and it blew my mind that I'd never known about it.

Getting Fancier (and Lazy-er) with Extension Methods
So using that attribute saved me from having to supply one parameter value. When I started adding logging to my website's content management system (CMS), I wanted to log things like page view, blog post views, and errors. Those calls were super repetitive and I wanted to see if I could get even more streamlined (read: lazy).

All of my controller classes extend from a common abstract base class named CmsControllerBase. One of the key features of CmsControllerBase is hanging on to a repository class for writing HTTP traffic information to CosmosDb, and that repository is exposed as protected ITrafficLogRepository TrafficLogRepository.

[Click on image for larger view.]

The upside of this is that I can create a C# extension method for the logging calls and those logging calls will have access to the controller. The extension method below is for logging HTTP 404 Not Found results. It takes the calling controller as an argument via this CmsControllerBase controller. It also takes the calling method name that uses CallerMemberName.

[Click on image for larger view.]

Then in my controller code, that means that logging an HTTP Not Found is just a matter of one empty method call (see below).

[Click on image for larger view.]

This wasn't the only extension method for logging. I also created utility extension methods for recording successful web traffic (HTTP 200), bad requests (HTTP 400), and errors (HTTP 500).

[Click on image for larger view.]

Once I had these, adding the logging calls was a simple matter of pasting in a bunch of single-line method calls without having to fiddle with any argument values.

Summary
If you're looking to simplify your logging calls, the CallerMemberName attribute is a powerful tool that can save you a lot of time and effort. By automatically inserting the name of the calling method, it eliminates the need for hard-coded strings and reduces the risk of errors. This attribute is especially useful for creating more maintainable and readable code, making your logging process more efficient and less prone to mistakes. Give it a try and see how it can streamline your logging practices.

About the Author

Benjamin Day is a consultant, trainer and author specializing in software development, project management and leadership.

Posted by Benjamin Day on 01/21/20250 comments


Building an AI-Powered .NET Aspire Application

AI's transformation of software development sees more and more groundbreaking tools and frameworks empowering developers to build more intelligent and efficient applications. Among these advancements is the ability to integrate large language models (LLMs), vectorized data, and robust cloud-based services to create AI copilots that can transform how users interact with software.

For VSLive! blog readers, of course, that means using Microsoft tooling like Semantic Kernel with Azure OpenAI Service and Azure Cosmos DB -- with the help of .NET Aspire.

And that's what will be used in an upcoming session titled "Building AI Copilots: Integrate Semantic Kernel, Azure OpenAI, and Azure Cosmos DB with .NET Aspire," presented by Justine Cocchi, a Senior Program Manager at Microsoft, during the Visual Studio Live! developer conference in Las Vegas March 10-14. This introductory-to-intermediate session offers attendees a hands-on guide to building an AI-powered application step-by-step. From generating embeddings on user input to leveraging Azure Cosmos DB's cutting-edge DiskANN vector indexing and search capabilities, the session is designed to demystify the process of building Generative AI applications using a Retrieval-Augmented Generation (RAG) pattern.

"We'll walk through how .NET Aspire provides consistent client initialization patterns for Azure Cosmos DB and Azure OpenAI as well as how the Aspire dashboard simplifies debugging in development."

Justine Cocchi, Senior Program Manager, Microsoft

Cocchi's session aims to equip developers with actionable insights to seamlessly integrate the tools mentioned above into .NET Aspire applications. Microsoft has been heavily pushing .NET Aspire lately, a cloud-ready stack for building observable, production-ready, distributed, cloud-native applications with .NET, helping devs with orchestration, components, tooling, service discovery, deployment and more.

.NET Aspire
Source: Microsoft

Attendees are promised to gain a comprehensive understanding of the foundational concepts, explore practical techniques for enhancing scalability and performance, and leave with a solid framework to create their own AI copilots.

Ahead of the event, we caught up with Cocchi to learn more about the technology, the session and how attendees can prepare for it.

Inside the Session

What: Building AI Copilots: Integrate Semantic Kernel, Azure OpenAI, and Azure Cosmos DB with .NET Aspire

When: March 13, 8 a.m. - 9:15 a.m.

Who: Justine Cocchi, Senior Program Manager, Microsoft

Why: Learn how to generate embeddings on user input, search vectorized custom data, generate responses from an LLM, manage chat history, and build a semantic cache to enhance performance.

Find out more about VS Live! taking place March 10-14 at Paris Las Vegas Hotel & Casino

VSLive! What inspired you to present a session on this topic?
Cocchi: Generative AI applications have been trending for some time now, and I wanted to walk attendees through some best practices. From choosing a database that can seamlessly scale as your app grows like Azure Cosmos DB, using frameworks that help you manage infrastructure and code like .NET Aspire and Semantic Kernel, to following patterns that allow for contextually accurate results like the RAG pattern -- there are many factors that make a successful AI application.

How does .NET Aspire interoperate with the core components and functionalities of Semantic Kernel, Azure OpenAI, and Azure Cosmos DB to facilitate the development of AI copilots?
.NET Aspire is helpful while developing and deploying any production-grade .NET application, and AI copilots are no different.

We'll walk through how .NET Aspire provides consistent client initialization patterns for Azure Cosmos DB and Azure OpenAI as well as how the Aspire dashboard simplifies debugging in development.

How do Semantic Kernel and .NET Aspire specifically work together?
Semantic Kernel and .NET Aspire are both orchestration frameworks that can help you manage your application. While Semantic Kernel helps orchestrate the AI components in your code, .NET Aspire is useful for managing the service dependencies and various projects in your solution. These two components work together to ensure you can focus energy building your business logic instead of worrying about how to set up your project.

What common challenges might developers face when integrating these technologies?
One of the challenges is keeping up with quickly changing libraries and AI LLMs. Relying on frameworks like Semantic Kernel to help you orchestrate the AI components in your application instead of writing all the functionality in native SDKs can help reduce the burden of underlying API changes. Knowing what tool to use when is critical to maintaining performance while reducing developer toil.

How can developers optimize the performance and scalability of AI copilots built with this technology stack, particularly concerning data processing and response times?
There are a few ways to ensure performance remains high as your application scales. It's critical to pick a database that can keep response times low as more data is added. We'll talk about DiskANN vector indexing in Azure Cosmos DB, which is built for performant vector searches over large data volumes. Slow response times can also come from the LLM generating chat completions, especially when large amounts of context are passed in. We'll dig into managing the amount of chat context sent with each new request and patterns for semantic caching to reduce overall response times on repeated questions.

What resources would you recommend for developers to get up to speed in this space and prepare for your session?
There are many great resources to learn about building copilots with Azure Cosmos DB, Azure OpenAI, Semantic Kernel and .NET Aspire. The session is based on the demo application in this repository. You can also refer to the AI Samples Gallery and the Azure Cosmos DB vector search documentation.

Save $400 when you register for VSLive! Las Vegas Developer Training Conference (March 10-14) by the Super Early Bird savings deadline of Jan. 17.

Posted by David Ramel on 01/13/20250 comments


Understanding Layouts in ASP.NET Core: Using Sections to Build Flexible Content

I recently decided that my website needed some love and maintenance. I'd been hosting it using WordPress for a zillion years and I'd started getting fed up with how much it was costing me to run it. So I did what every busy developer does -- I decided to write my own content management system / blog engine using Cosmos DB and ASP.NET Core MVC. It came out pretty darned good and my ASP.NET MVC skills got a workout and a refresher.

One big thing I learned when I was writing all the search engine optimization (SEO) code was about sections and the @RenderSection helper method.

Why Would I Care about @RenderSection?
In order to get your site to be SEO compliant and to help your site and content to show up in search engines, there are a whole bunch of values that you'll want to put into the HTML of your webpages. Lots of these values are part of the Open Graph Protocol and are the machine-readable metadata that a search engine will use to "understand" your content. These values should be part of the <head> tag of your HTML page and show up as <meta> tags.

Some of the values that I'm using on my website are:

  • og:site_name -- name of the website
  • og:title -- the title of the page or blog post
  • og:description -- description of the website or an excerpt of a blog post
  • og:type -- type of content (website, article, other)
  • og:image -- image for the page or blog post

The image below is a screenshot of the meta tags that I'm setting on my home page. As you can see, there's kind of a lot of them and they all show up in the <head> tag.

[Click on image for larger view.]

If this were a static site, I'd just put the values that I need in the various hard-coded html files for my site. But this is a dynamic page and the meta values are going to change a lot from page to page and blog post to blog post.

The part that was tripping me up was that the values needed to be in the <head> tag.

A Little Bit About the Structure of an ASP.NET Core MVC Application
In order to point out why getting values into the <head> tag was a problem, let me just pause for a moment to review the structure of an ASP.NET Core MVC View. The views are all the *.cshtml files that live in your web project. These cshtml files use a templating syntax called Razor that allows you to mingle C# code and HTML markup that gets turned in to rendered HTML that shows up in your browser.

So far so good.

The cshtml/razor/MVC syntax creates a hierarchy of these views that all work together to allow you to reuse logic and HTML so that you avoid tons of duplicated code. Here's an example. Let's say that I want to display a blog post. The display logic for a blog post lives in Entry.cshtml and when it gets run, the MVC app knows that Entry is only part of the display logic and that it also has to render _Layout.cshtml, too.

[Click on image for larger view.]

The _Layout.cshtml file contains all the basic structure of a web page like the tags, the <html> tags, the <body>, and also the <head> tag and its contents. It also has a call to @RenderBody and this is where the contents of Entry.cshtml gets dropped in in order to make a fully rendered webpage.

[Click on image for larger view.]

My Problem: How Do I Get Dynamic Content from a View into _Layout.cshtml?
Back to my problem. I need to be able to generate those "og:" <meta> values and make them show up in the <head> tag. The Entry.cshtml view will have access to the information that should be in the those values but the problem is that the actual tag lives in a totally different file. So how to get those <meta> values but the problem is that the actual <head> tag lives in a totally different file. So how to get those <meta> tag values from Entry.cshtml to the <head> tag in _Layout.cshtml?

The Answer: Sections and @RenderSection
Razor and cshtml to the rescue! It turns out that you can define something called a section that allows you to pass content from the child view to the layout view. All you need to do is to put a @RenderSection directive wherever you want that content to show up. In this case (see below), I put a @RenderSection called HeadContent inside of the <head> tag of _Layout.cshtml. This basically just says to the templating engine, "if you've got content for this section, drop it here." I'm also marking it as required = false meaning that if there's no content supplied, to just leave this section as blank.

[Click on image for larger view.]

Over in my Entry.cshtml view, you can use the @section directive to set all the values that should show up in the section. So as you can see below, Entry.cshtml has access to a BlogEntry class that contains all the data for a blog post via the @model directive. Once I've got that data, it no big deal to just drop in the values.

[Click on image for larger view.]

Then at runtime, all the generated <meta> data from the entry, shows up in the <head> tag. All I needed to do was make sure that the name I supplied on my @section matches the name supplied in the @RenderSection.

  • @section HeadContent
  • @RenderSection("HeadContent", required: false)

Summary
In this post, we explored how to leverage @RenderSection in ASP.NET Core MVC to solve layout-related challenges and create dynamic, flexible web pages. Whether you're building a custom blog engine or enhancing your web application's structure, sections provide a clean, modular approach to content management. By integrating sections effectively, you can simplify your layout design and unlock more maintainable solutions for rendering dynamic content.

About the Author

Benjamin Day is a consultant, trainer and author specializing in software development, project management and leadership.

Posted by Benjamin Day on 12/17/20240 comments


What's New in Modern .NET Library APIs

.NET 9 was introduced in November with claims of speed, efficiency, and cross-platform capabilities to provide Microsoft-centric developers with even more flexibility and power to write modern applications.

That's done with the help of new APIs that have been introduced by the Microsoft dev team in recent .NET versions, with .NET 9 providing new APIs for .NET Aspire, ASP.NET Core and other properties that touch upon functionality regarding OpenAI, garbage collection, authentication and authorization and much more.

.net 9 Highlights
[Click on image for larger view.] .NET 9 Highlights (source: .NET Conf screenshot).

To help developers get a handle on the new .NET 9 functionality, specifically what APIs have been added to recent .NET versions, expert Microsoft MVP Jason Bock will review new features and APIs at the Visual Studio Live! developer conference coming to Las Vegas in March.

Aptly titled "What's New in Modern .NET Library APIs," this is an intermediate session lasting 75 minutes in which attendees are promised to learn:

  • What functionality .NET provides
  • New features in the latest version of .NET
  • Insights in staying on top of new runtime versions

We caught up with Bock, a staff software engineer at Rocket Mortgage, to learn more about his upcoming session.

VSLive! What inspired you to present a session on this topic?
Bock: I've always been interested in watching the evolution of .NET.

"The API surface is .NET is vast, providing functionality for a wide variety of concerns, from as small as generating a unique identifier to creating a web application. In each version, there's always something new to dive into and see what it does."

Jason Bock, Staff Software Engineer, Rocket Mortgage

The API surface is .NET is vast, providing functionality for a wide variety of concerns, from as small as generating a unique identifier to creating a web application. In each version, there's always something new to dive into and see what it does. Even if it may not of interest me now, it might be something I need in the future. I want to learn about these features and share with others what I've found.

Inside the Session

What: What's New in Modern .NET Library APIs

When: March 13, 2025, 11 a.m. -- 12:15 p.m.

Who: Jason Bock, Staff Software Engineer, Rocket Morrtgage

Why: Review new features and APIs that you can use wherever .NET goes.

Find out more about VS Live! taking place March 10-14 at Paris Las Vegas Hotel & Casino

What is just one of the most notable API additions in .NET 9 that serve to enhance developer productivity?
While the focus is usually on "big name" items like .NET MAUI and .NET Aspire, I personally am interested in those general-purpose APIs that I can use no matter what application I'm building or framework I'm using. For example, there's a new type called CompositeFormat that is useful when you want to use a string with format "holes" in them that will get filled in later, like in a logging call. CompositeFormat can make this scenario a bit more performant. Task.WhenEach() is a new API that will let you enumerate a set of tasks based on when the tasks complete. Finally, the Base64Url class -- a type used for Base64 encoding -- no longer requires a separate package to be installed from NuGet. It's "in the box" with .NET 9.

What are a couple of top considerations developers should keep in mind when integrating these new APIs into existing .NET applications?
.NET has become an extremely stable platform from an upgrade perspective. Gone are the days where moving from one version of the .NET Framework to a new one may require a significant investment of time addressing breaking changes and versioning issues. If you're moving from a previous version of .NET to .NET 9, the transition should be very smooth. That doesn't mean it'll always issue-free, but keeping on pace with new .NET versions is far less of a burden than it used to be.

How do the new APIs in .NET 9 align with current trends in cloud-native development and microservices architecture?
There are definitely changes related to AI. There are more packages being released by Microsoft that make the usage of LLMs and other AI-related technologies easier to consume. .NET Aspire is a new API that is all about simplifying and empowering developers working on cloud-based applications. And underneath it all, there are constant performance improvements in each version of .NET to improve the performance of modern applications targeting .NET.

What resources or best practices would you recommend for developers to get up to speed with these new APIs and prepare for your session?
Microsoft typically provides a "what's new" article summarizing all of the changes that are done with a major release. .NET 9 is no different -- you can find that resource here. If you're interesting in diving deeper, there are Markdown diff files that show exactly what has changed from version to version. Those release notes exist here.

Note: Those wishing to attend the conference can save hundreds of dollars by registering early, according to the event's pricing page. "Save $500 when you Register by the Cyber Savings deadline of Dec. 13," said the organizer of the event.

Posted by David Ramel on 12/12/20240 comments


Keeping the Compiler Happy: Null Member Variable Compile Errors in C#

Wait! This won't be boring! Yes, I know -- this probably isn't the most exciting topic ever, but I ran into some problems this week and I actually learned some cool stuff about nullability in C# and the compiler.

The issue I was running into was member variable initialization inside of a class. WAIT! Please don't quit on this article! I swear it gets better! I started off with a simple class, but it had complex initialization logic that involved multiple method calls and a bunch of calculations. Simple enough but I couldn't get it to compile because I kept getting nullability errors from the compiler.

Stick with me and I'll talk you through the fixes I used to keep the compiler happy.

Simple Class. Complex Logic.
The sample code I'm going to use is going to be much simpler, but it'll demonstrate the problems. Let's start with a C# class for a Person that has two string properties: FirstName and LastName. Looking at the code below, right from the very beginning I've got two compilation errors on those properties. Since strings are null by default, this is failing compilation because I'm not initializing the property values.

div>
Figure 1: Person class with two compile errors
[Click on image for larger view.]
Figure 1: Person Class with Two Compile Errors

Now it would be simple enough in this case to just set the values either using a constructor or by using a property initializer. Just tag "= string.Empty;" onto the end of each of those properties and the compiler warning goes away.

Figure 2: Fixing the compile problem using property initializers
[Click on image for larger view.]
Figure 2: Fixing the Compile Problem Using Property Initializers

This works in a simple example like this, but in my production application code, the logic wasn't so straightforward -- and I needed to use the initialization logic in multiple places.

Use an Initialization Method
Since I needed code reuse and wanted avoid duplicate code, my ideal solution is to use an Initialize() method that I call from the constructor.

Figure 3 - Initialize() method but compilation still fails
[Click on image for larger view.]
Figure 3: Initialize() Method but Compilation Still Fails

The problem is that once I do this, I'm getting a compile error.

Non-nullable property 'FirstName' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. CS8618

This is super annoying because I know as the programmer that the Initialize() method actually is fixing the initialization problem and therefore the compile error is kind of wrong. It's not totally wrong ... but it's pretty much wrong. The problem is that I don't have a way to prove it to the compiler.

The C# 'required' Keyword: Make it Someone Else's Problem
The compile error is helpfully giving us a couple of options. Either make the property nullable or add the required keyword. Both of these "solutions" solve the problem without solving the problem -- arguably they just move the problem somewhere else and make it someone else's problem.

If we make the property nullable, we get properties that look like this:

public string? FirstName { get; set; }

That "?" at the end of the string type means that anyone who uses this property now has to worry that the value could be null. That spreads a lot of null-checking logic throughout the app and that stinks. Kind of misses the point of having compiler null checks, too.

The other suggestion is to use the required keyword. This was something that I didn't know about. It was added as part of C# 11 back in November of 2022 so it's still a somewhat new feature in the language.

If you add the required keyword to the properties, it means that the caller to this class must supply a value as part of the call. Put another way, anyone who new's up an instance of Person would need to supply the values for FirstName and LastName.

Figure 4: Adding the 'required' keyword to the FirstName and LastName properties
[Click on image for larger view.]
Figure 4: Adding the 'required' Keyword to the FirstName and LastName Properties

This solves the compile problem in Person.cs but -- like I said -- it kind of moves the problem somewhere else. Let's say I have a class called PersonService that creates an instance of Person. When PersonService creates that instance of Person (see Figure 5), it needs to pass values for FirstName and LastName. Once again, in a simple scenario like this, it isn't all that onerous -- but if the logic for initialization is complex, this is a recipe for sprawling code duplication.

Figure 5: The 'required' keyword means you must supply values when you construct the class
[Click on image for larger view.]
Figure 5: The 'required' Keyword Means You Must Supply Values when You Construct the Class

This moves the problem out of the Person class and potentially spreads the problem around in the rest of the code. This violates not only best practices of object-orientation but also guidelines for maintainability.

Not my favorite solution.

Show the Compiler Who's Boss: The [NotNullWhen] Attribute
Let's go back to that Initialize() method from a little while back. In my actual production application, I had complex initialization logic that I needed to use in a couple of different places and that made this Initialize() method my preferred option. But it also caused problems because the compiler didn't officially know that I was actually indeed initializing the FirstName and LastName property values.

Figure 6: The Compiler doesn't know that Initialize() actually initializes
[Click on image for larger view.]
Figure 6: The Compiler Doesn't Know that Initialize() Actually Initializes

The solution here is to add an attribute called [MemberNotNull]. It's part of the System.Diagnostics.CodeAnalysis namespace and what it does is tell the compiler what we're achieving in the Initialize() method -- in this case, actually setting FirstName and LastName to non-null values.

In the code sample below (Figure 7), I apply the [MemberNotNull] attribute and pass it the names of the FirstName and LastName properties. Once I do that, I don't have any compiler errors complaining about null values and initialization and I also get to reuse my Initialize() method code. Just by adding this attribute, it saves me from having to pile all my initialization logic into my constructor just to keep the compiler happy.

Figure 7: The [MemberNotNull] Attribute Fixes the Compile Problem
[Click on image for larger view.]
Figure 7: The [MemberNotNull] Attribute Fixes the Compile Problems

By the way, there are also some other nullability hint attributes that you might want to check out: [NotNull], [NotNullWhen], and [MemberNotNullWhen].

Summary
Hopefully, you agree that this wasn't that boring and hopefully you learned something. If you care about your compile time null checks (and you should), you sometimes have to get creative about your initialization logic. You can either pile all your init code into the constructor so that the C# compiler understands the logic by default, you can add the required keyword to mandate that callers pass you the required values, or you can tell the compiler to chill out by using [MemberNotNull].

About the Author
Benjamin Day is a consultant, trainer and author specializing in software development, project management and leadership.

Posted by Benjamin Day on 11/18/20240 comments


Debugging Complex Classes with the [DebuggerDisplay] Attribute

Here's a scenario I ran into just last week. I was working on some code to edit PowerPoint PPTX files using C# and the OpenXML libraries. If you've ever tried using them before, you know that the OpenXML libraries are impossibly complex and nearly impossible to understand. And as much as I might have wanted to keep my code simple, I was losing and the complexity was growing.

The bugs I was getting were pretty complex, too. I'd put a bunch breakpoints in my code and then I'd be using the debug visualization tools in Visual Studio to try to figure out what was going wrong. But the problem was the my bugs could be just about anywhere in a giant tree of objects. It was really hard to figure out what was going on just by looking at the debug windows. For example, just check out that Locals debugging window in the screenshot below. It's nested object after nested object after nested object.

[Click on image for larger view.]

I needed to tame that debugging sprawl.

The [DebuggerDisplay] Attribute
I'm not sure how I found out about this, but there's this cool feature in Visual Studio and .NET that lets you customize how your objects appear in the debugger. It's the DebuggerDisplay attribute in System.Diagnostics. I'd never used it before because -- well -- I just assumed that it must be difficult to use.

But it turns out that it's super easy to implement. If you know how to use interpolated strings, you pretty much know how to implement the debugger display attribute.

Here's a quick review of interpolated strings. Let's say that you have three values that you want to combine into a single string: current date, a random number, and the OS version string. You could combine them using the "+" operator -- that's the old-school, original way.

Or you could combine them using string interpolation. Just put a "$" in front of the string and then wrap the variable names you want to use inside of curly braces. Done.

[Click on image for larger view.]

Tame the Debugging Mess
Rather than try to demo this with OpenXml APIs, I'm going to introduce a more understandable example. Let's say that I have a Person class that has a property for Name and a property for PhoneNumber. Most of the time you'd probably just have a Person class with a string property for FirstName, a string property for LastName, and a string property for PhoneNumber. But since I need some complexity, I'm splitting these into separate classes so that I get an object model:

[Click on image for larger view.]

To keep adding complexity, let's say that I'm writing my own PersonCollection class and I'm trying to write a unit test to make sure that when I "foreach" over the PersonCollection, that the objects are always returned in alphabetical order by name.

The test is failing and I'm not sure why, so I put a breakpoint on the code and go to take a look in the debugger:
[Click on image for larger view.]

When I look at the "actual" collection variable in the debugger, I see a list of 5 objects but I can't see the values:

[Click on image for larger view.]

In order to figure out what the values are, I need to start expanding properties in the watch window. That gives me something that looks like this:

[Click on image for larger view.]

So, I can see that the values are clearly not sorted by last name but the next time that I run this code, it's going to show me the original view and I'm going to have to click a bunch of times to get to the data.

Add the DebuggerDisplay Attribute
What would be a lot nicer would be a simple view of my Person properties right in the debugger when I'm looking at that collection. This is exactly what the DebuggerDisplay attribute does.

At the top of the Person class, I'll just add the DebuggerDisplay attribute and then provide it with an interpolated string describing how I'd like to visualize the data in the debugger. In this case, I want to see the LastName value, a comma, FirstName, and then the phone number.

[Click on image for larger view.]

The next time that I run this code and hit that breakpoint, I see much more descriptive data in the Values column:

[Click on image for larger view.]

And now I can focus on fixing my bug rather than crawling through a tree of values in the watch windows.

Conclusion
Debugging complex trees of objects can be difficult even with great debugging tools. In order to make it easier on yourself, consider adding the DebuggerDisplay attribute to those difficult-to-debug classes. DebuggerDisplay allows you to provide a view of your object in the Visual Studio debugger windows that have exactly the data you need to see in order to fix your problems.

If you'd like the source code for this demo, you can get it on GitHub.

About the Author
Benjamin Day is a consultant, trainer and author specializing in software development, project management and leadership.

Posted by Benjamin Day on 10/17/20240 comments


Keep Up-to-Date with Visual Studio Live!

Email address*Country*