I love using .NET attributes. 

When I am teaching a class on web services, I start by showing what an attribute is, how you can create custom attributes, and how to use GetCustomAttributes to detect your attribute.  I then cruft up a sample CsvSerializer that serializes a class to a CSV file based on the metadata attributes.  This helps show what the XmlSerializer is accomplishing behind the scenes, and selfishly gives me an opportunity to code using a part of the framework that I find fascinating. 

I have been digging into ASMX lately a little more than I had previously, part of which requires that I spend more time looking at the System.Xml.Serialization attributes that affect XML serialization closer. I have also been trying to dig into reflection a little more than I had previously.  All of this requires that you understand not only what an attribute is and does, but requires that you think about the various attributes that can be applied in a given context to achieve your goal. 

Steve Steiner has a great post on using metadata attributes to change the stepping, breakpoint, and callstack behavior of the Visual Studio managed debugger.  He also references a post from Mike Stall that shows how to use Just My Code debugging

Using attributes in .NET is one of the most interesting aspects of managed code programming, and it is usually the point of highest interest when I talk to Java developers about .NET programming.  You can look up all of the .NET Framework's attributes by looking at the descendants of System.Attribute.  Looking at that page, we can see that there are a number of interesting debugger-related attributes in the System.Diagnostics namespace.

System.Diagnostics.ConditionalAttribute Indicates to compilers that a method is callable if a specified preprocessing identifier is applied to the method.
System.Diagnostics.DebuggableAttribute Modifies code generation for runtime just-in-time (JIT) debugging. This class cannot be inherited.
System.Diagnostics.DebuggerBrowsableAttribute Determines if and how a member is displayed in the debugger variable windows. This class cannot be inherited.
System.Diagnostics.DebuggerDisplayAttribute Determines how a class or field is displayed in the debugger variable windows.
System.Diagnostics.DebuggerHiddenAttribute Specifies the DebuggerHiddenAttribute. This class cannot be inherited.
System.Diagnostics.DebuggerNonUserCodeAttribute Identifies a type or member that is not part of the user code for an application.
System.Diagnostics.DebuggerStepperBoundaryAttribute Indicates the code following the attribute is to be executed in run, not step, mode.
System.Diagnostics.DebuggerStepThroughAttribute Specifies the DebuggerStepThroughAttribute. This class cannot be inherited.
System.Diagnostics.DebuggerTypeProxyAttribute Specifies the display proxy for a type.
System.Diagnostics.DebuggerVisualizerAttribute Specifies that the type has a visualizer. This class cannot be inherited.
System.Diagnostics.SwitchAttribute Identifies a switch used in an assembly, class, or member.
System.Diagnostics.SwitchLevelAttribute Identifies the level type for a switch.

While looking through the list of attributes, I came across a capability that I didn't know was in the .NET Framework... the System.Web.Services.Protocols.MatchAttribute type allows pattern matching, which can be useful to create web services that parse the contents of a web page. 

The use of metadata to affect your code's behavior is definitely one of the key aspects to be a more effective .NET programmer, and I echo Sam's statement: .NET programmers should spend the time to learn  about .NET fundamentals.  Chapters 1-3 of Richter's Applied .NET Framework Programming should be required reading... along with chapters 16, 18, 19, and 20.