Поиск  
Always will be ready notify the world about expectations as easy as possible: job change page
Aug 15, 2021

The art of .NET custom exceptions

Источник:
Просмотров:
4716

.NET has a large number of built in exceptions. However, there maybe times when none of the built exceptions seem adequate for your particular scenario and you will need to create your own custom (AKA “user defined”) exception.

This post focuses on a discussion of how to create custom exceptions in .NET. The different types of built in .NET exceptions, exception handling, throwing, and more general exception related best practices will not be covered.

All code examples are in C#.

Basic Custom Exception Example

The following example illustrates the most basic template that every custom exception should follow:

using System;
using System.Runtime.Serialization;

// ...

[Serializable]
public class EntityNotFoundException : Exception
{
    private const string DefaultMessage = "Entity does not exist.";

    public EntityNotFoundException() : base(DefaultMessage)
    {
    }

    public EntityNotFoundException(string message) : base(message)
    {
    }

    public EntityNotFoundException(string message, Exception innerException) : base(message, innerException)
    {
    }

    protected EntityNotFoundException(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
    }
}

Example notes:

  • The class inherits from Exception. Custom exceptions should not inherit from ApplicationException. In the early days of .NET Microsoft actually recommended inheriting from ApplicationException but has since changed that recommendation.
  • The class is decorated with the Serializable attribute. Custom exceptions are not serializable by default. But making a custom exception serializable allows it to be properly marshalled across app domains and threads. It should be noted that simply decorating your custom exception with Serializable is not enough to serialize any extra properties you might have defined. More on this in the “Extended Custom Exception Example” section below.
  • The class should always (unless there is a very good reason) have the four Microsoft recommended constructors: public (), public (string message), public (string message, Exception innerException), protected (SerializationInfo info, StreamingContext context).
  • The custom exception class itself is not marked as implementing ISerializable. Doing so would be redundant as we are inheriting from Exception which itself implements ISerializable.
  • We provide a default message that makes sense for the custom exception when the parameterless constructor is called.
  • The custom exception’s name ends with “Exception”. All exceptions in .NET should have the name suffix of “Exception”.
  • Any XML documentation comments have not been added for the sake of brevity. However, adding them to the public interface of the exception can be a good idea to aid in it’s use.

Extended Custom Exception Example

In the following example we build upon our basic example from the previous section and add the concept of adding our own property. In this case an integer property EntityId:

using System;
using System.Runtime.Serialization;

// ...

[Serializable]
public class EntityNotFoundException : Exception
{
    private const string DefaultMessage = "Entity does not exist.";

    public int EntityId { get; }

    public EntityNotFoundException() : base(DefaultMessage)
    {
    }

    public EntityNotFoundException(string message) : base(message)
    {
    }

    public EntityNotFoundException(string message, Exception innerException) : base(message, innerException)
    {
    }

    public EntityNotFoundException(int entityId) : this($"Entity does not exist with ID: '{entityId}'.")
    {
        EntityId = entityId;
    }

    protected EntityNotFoundException(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
        EntityId = (int)info.GetValue(nameof(EntityId), typeof(int));
    }

    public override void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        base.GetObjectData(info, context);

        info.AddValue(nameof(EntityId), EntityId, typeof(int));
    }
}

Example notes:

  • Additional new properties, in this case EntityId, should not have non-private setters. This is because .NET exceptions should be immutable (once created their state should not change). Instead all values should be passed through and set in the constructor.
  • A new method has been added: GetObjectData. This method is part of the ISerializable interface which Exception implements. When overridden it allows us to set extra information that we want to save during serialization. In this case the integer value in our property EntityId. When overriding this method make sure to also call the base implementation.
  • Our protected constructor which was previously empty now has implementation to help set our new EntityId property when our exception is being deserialized.

Unit Testing Custom Exceptions

We can unit test our extended custom exception example with the following tests:

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using NUnit.Framework;

// ...

[TestFixture]
public class EntityNotFoundExceptionTests
{
    private const string Message = "some message";
    private const int EntityId = 1;

    [Test]
    public void WhenNoArgs_ThenSetMessageToDefault()
    {
        var sut = new EntityNotFoundException();

        Assert.That(sut.Message, Is.EqualTo("Entity does not exist."));
    }

    [Test]
    public void WhenMessageSpecified_ThenSetMessage()
    {
        var sut = new EntityNotFoundException(Message);

        Assert.That(sut.Message, Is.EqualTo(Message));
    }

    [Test]
    public void WhenMessageAndInnerExSpecified_ThenSetMessageAndInnerEx()
    {
        var innerException = new Exception();

        var sut = new EntityNotFoundException(Message, innerException);

        Assert.That(sut.Message, Is.EqualTo(Message));
        Assert.That(sut.InnerException, Is.SameAs(innerException));
    }

    [Test]
    public void WhenIdSpecified_ThenSetProperty()
    {
        var sut = new EntityNotFoundException(EntityId);

        Assert.That(sut.Message, Is.EqualTo($"Entity does not exist with ID: '{EntityId}'."));
        Assert.That(sut.EntityId, Is.EqualTo(EntityId));
    }

    [Test]
    public void WhenSerialized_ThenDeserializeCorrectly()
    {
        var sut = new EntityNotFoundException(EntityId);

        var result = Serializer.SerializeAndDeserialize(sut);

        Assert.That(result.EntityId, Is.EqualTo(sut.EntityId));
        Assert.That(result.ToString(), Is.EqualTo(sut.ToString()));
    }
}

// ...

internal static class Serializer
{
    public static TException SerializeAndDeserialize<TException>(TException exception)
    {
        var formatter = new BinaryFormatter();
        
        using (var stream = new MemoryStream())
        {
            formatter.Serialize(stream, exception);

            stream.Seek(0, 0);

            return (TException)formatter.Deserialize(stream);
        }
    }
}

The tests cover five scenarios: the four different constructors and what properties they set. The fifth test checks the serialization/deserialization of the exception and it’s EntityId property value.

Final Thoughts

So when should we create our own custom exception instead of using a built in .NET exception? If a .NET built in exception fits your requirement then you should certainly use it. However, if you do go down the route of creating your own custom exception here are some things to bear in mind:

  • Don’t create custom exceptions for every type of situation. Be wary if a custom exception is too granular. Instead generalize to one custom exception and be more specific in the exception’s message when the exception is thrown.
  • Don’t create complex hierarchies of custom exceptions for your application. Most of the time it will make the most sense that your custom exception inherits directly from Exception.
  • Do create a custom exception to signify a problem in your particular application domain. For example creating a custom exception that represents that a problem has occurred in a NuGet package library you have created.
  • When creating custom exceptions do think about the exception handling that a consumer of your code might have to deal with. For example if you create a number of different custom exceptions is the consumer of your code going to have a number of different catch blocks? It might also be worth considering how you are going to communicate that your exception exists and under what scenarios it will be thrown to the consumer of your code.

As of writing .NET is nearly 20 years old and there are many posts on the internet about creating custom exceptions. However, virtually all only cover small parts of the topic or unfortunately give bad advice. Hopefully this post has provided a more complete picture on how to create custom exceptions.

Похожее
Jul 21
Jul 14, 2023 Some time ago, many professionals forecasted that .NET Core would be the upcoming successful thing, which would give an opportunity to developers for a large number of ideas/options in application development. Wherein, developers with good skills have...
Apr 24, 2022
Author: Lucas Diogo
A practice approach to creating stable software. Don’t make your software unstable like a house of cards, solidify it. There are five principles to follow when we write code with object-oriented programming to make it more readable and maintainable if...
Aug 14
Author: Yasith Wimukthi
In today’s fast-paced development environment, Docker has become a vital tool for developers, simplifying the process of creating, deploying, and running applications in containers. However, to truly harness the power of Docker, it’s crucial to follow best practices that ensure...
Oct 26, 2023
Author: Alex Maher
Entity Framework Core Features in 2023 EF Core 2023 has rolled out some pretty cool stuff, and I’m excited to share it with you. So, grab a cup of coffee, and let’s get started! 1. Cosmos DB Provider Improvements Azure...
Написать сообщение
Тип
Почта
Имя
*Сообщение