Ask Kathleen

Control Exceptions

Take control of casting exceptions, determine whether parent records have children in LINQ to SQL, and resolve cref references in XML comments.

Technologies mentioned in this article include LINQ, C#, XML, and VB.NET.

Q When a cast fails, I want an exception thrown at the point of the exception. I don't want a random invalid cast exception with no message, and I don't want a null reference exception thrown later in my code. I'm solving this using TryCast and checking for Nothing. However, I'm getting sick of writing the same lines of code over and over again:

Dim a As New ClassA
Dim b As ClassB
b = TryCast(a, ClassB)
If b Is Nothing Then
   Throw New InvalidCastException("Invalid cast for b")
End If

A I'm assuming your classes look something like this:

Class ClassA
End Class

Class ClassB
   Inherits ClassA
End Class

I also assume you've simplified the message because your example doesn't offer much beyond the exception CType would give.

To gain control over the exception, you can use an extension method to tidy up the code a bit. There are a couple ways to do this. The first approach is to create a replacement for the TryCast operator that takes the additional information. If the name of the method is TryCast, you must use an extension method to differentiate it from the operator -- which is probably desirable for clarity:

b = a.TryCast(Of ClassB)("a")

For test purposes, I'm using the variable name as the additional information, but you can use a variation of this approach to throw a custom exception or include a custom message. The generic method is strongly typed and returns the passed value on a successful cast:

<Runtime.CompilerServices.Extension()> _
Public Function [TryCast](Of TReturn As Class, _
   T)(ByVal val As T, ByVal variableName As String) _
   As TReturn
   Dim ret = TryCast(val, TReturn)
   If ret Is Nothing Then
      Dim frame = New StackTrace().GetFrame(0)
      Throw New InvalidCastException( _
         "Invalid Cast for " & variableName)
   End If
   Return ret
End Function

The problem with this approach illustrates one of the differences between methods and operators. Operators are resolved at compile time so the compiler can do additional checks. When you use the TryCast operator, the compiler checks whether a cast between the types is possible. Unfortunately, you lose this check and turn compiler errors into runtime exceptions when you use this approach -- always a bad idea. A better approach is to perform a check after the operator completes the cast. This retains the compiler behavior, and you can make this call through an extension method or standard syntax:

b = TryCast(a, ClassB).CheckCast("a")
b = Utility.CheckCast(TryCast(a, ClassB), "a")

This method takes the result of the TryCast, which is either the correct value or Nothing. If the value is Nothing, you can raise an exception under your control:

<Runtime.CompilerServices.Extension()> _
Public Function CheckCast(Of T As Class)( _
ByVal val As T, ByVal variableName As String) As T
   If val Is Nothing Then
      Throw New InvalidCastException( _
         "Invalid Cast for " & variableName)
   End If
   Return val
End Function

As before, you can adjust the action to provide the exception, message, and Exception.Data members you want, while retaining consistent behavior throughout your application. The behavior of these methods differs from the CType operator when you cast a variable that initially has a value of Nothing. CType happily performs the conversion without raising an exception, while the CheckCast method throws the exception. This is desirable if you wish to protect downstream code from null reference exceptions.

In some projects, I've used this exception interception technique in conjunction with an exception factory (see Listing 1). One of the drivers for this approach is supplying unique markers for exceptions, which frees you from line number dependencies as the source code diverges over time. For example, I once needed to report exceptions in multiple human languages. The application ran in one language, while local support was most comfortable using another language -- and the programmers who serviced the app knew neither of those languages. In that application, the exception factory was called through methods like CheckCast, as well as from global exception handlers. Each call had a unique GUID, and no two literal GUIDs in the application were identical (see "Creating GUID Literals"):

Dim metadata = Utility.CheckCast( _
   TryCast(mClassMetadata, _
   EntityMetadata), New Guid( _
   "{E4C9B084-20AC-48f7-8BF0-8227E8414560}"))

In this snippet, the CheckCast method determines the target type of the cast and selects a subcategory for the exception. Some exception types, such as InvalidOperationException, are used in a variety of scenarios. Adding a subcategory is simpler than creating a large set of exceptions.

This is especially important when enhancing the exception strategy for an existing application. Changing the type of exception thrown is risky because it can cause catch blocks higher in the stack to behave differently. The subcategory is a member of the ExceptionId enumeration:

<Runtime.CompilerServices.Extension()> _
Public Function CheckCast1( _
   Of T As Class)(ByVal val As T, ByVal _
   variableName As String) As T
   If val Is Nothing Then
      Throw ExceptionFactory. _
         CreateInvalidCastException(New Guid( _
            "{8FDEAFED-539D-4aa1-B1EC-5F72A0BCF885}"), _
            ExceptionId.TryCastFailed, 1, _
               GetType(T).Name, variableName)
   End If
   Return val
End Function

The CreateInvalidCastException method passes the type of the failed cast as part of a key value pair. The CreateException method builds the dictionary for the Data property of the exception. A System.InvalidCastException is raised, which causes the dependent logic in Catch blocks to behave identically with and without the exception factory. This is important because you will almost certainly discover additional points of vulnerability during maintenance when enhancing the standard .NET exception with additional information. In addition to letting you customize exceptions, the factory lets you set breakpoints, which is useful when debugging.

Q I have a one-to-many relationship in my database, and I need to know all of the parent records that do not have children. I accomplish this in SQL using code like this:

SELECT *FROM parentTableName
WHERE EXISTS(
   SELECT id
   FROM childTableName
   WHERE parentTableName.ID 
     = childTableName.parentID)

How can I do this in LINQ to SQL?

A LINQ includes an extension method on IEnumerable called Any that does just what you want:

var q = from c in 
   dataContext.Customers 
   where c.SalesOrderHeaders.Any()
   select c;

The Any extension method takes a filter as a parameter. An example of this functionality would be returning all the customers that had any sales orders of more than $10,000:

var q = from c in 
   dataContext.Customers 
   where c.SalesOrderHeaders.Any(
   s => s.TotalDue > 10000)
   select c;

The target value is strongly typed, and you can replace the literal with a variable whose value is determined at runtime:

var q = from c in 
   dataContext.Customers 
   where c.SalesOrderHeaders.Any(
   s => s.TotalDue > target)
   select c;

IQueryable adds the similar All method, which returns true when all items in the set match the filter. This is similar to what you encounter when working in T-SQL; you must be careful with your logic. For example, negating the Where clause and switching the comparison operator aren't equivalent, as customers might not have any orders:

var q = from c in dataContext.Customers 
   where ! c.SalesOrderHeaders.Any(
   s => s.TotalDue > target)
   select c;
   Console.WriteLine(q.Count());
var q = from c in dataContext.Customers
   where c.SalesOrderHeaders.Any(
   s => s.TotalDue <= target)
   select c;

LINQ to SQL uses information in the mapping .DBML file to create SQL calls at runtime. It's fairly intelligent in the calls it creates, even for more complex queries:

SELECT COUNT(*) AS [value]
FROM [SalesLT].[Customer] AS [t0]
WHERE NOT (EXISTS(
   SELECT NULL AS [EMPTY]
   FROM [SalesLT].[SalesOrderHeader]   
   AS [t1]
   WHERE ([t1].[TotalDue] > @p0) 
   AND ([t1].[CustomerID] = 
   [t0].[CustomerID])
   ))

Expert tuning might improve performance on a highly stressed server, but these calls will generally be at least as fast as the average programmer's handcrafted T-SQL. Pay attention to big issues like indexes, and your performance should be fine.

Q In my XML comments, I want to use a <see> tag with a cref reference to a class in another assembly, which is referenced by the project. Can I do this? I am getting the warning:

"XML comment has a tag with a ‘cref' attribute ‘IBizCollection' that could not be resolved. XML comment will be ignored."

A The compiler gets confused in some instances when resolving references. You can fix this by qualifying the reference:

''' <remarks>
''' For more info <see cref="Biz.Common.Common"/>
''' </remarks> 

About the Author

Kathleen is a consultant, author, trainer and speaker. She’s been a Microsoft MVP for 10 years and is an active member of the INETA Speaker’s Bureau where she receives high marks for her talks. She wrote "Code Generation in Microsoft .NET" (Apress) and often speaks at industry conferences and local user groups around the U.S. Kathleen is the founder and principal of GenDotNet and continues to research code generation and metadata as well as leveraging new technologies springing forth in .NET 3.5. Her passion is helping programmers be smarter in how they develop and consume the range of new technologies, but at the end of the day, she’s a coder writing applications just like you. Reach her at [email protected].

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube