Search code examples
c#asp.net-coreexceptionentityclean-architecture

What exception should I throw when expecting a null value?


Is there a standard Exception to invoke when excepting null instead?

Commonly, exceptions are thrown for null references and arguments such as NullReferenceException and ArgumentNullException exceptions.

  • The NullReferenceException is thrown when there is an attempt to dereference a null object reference.

  • The ArgumentNullException is thrown when a null reference is passed to a method that does not accept it as a valid argument.

But what about in the event we expect the result to be null? For example, I am trying to validate if an entity exists in the database, if it does then an exception should be thrown and then I can log the error. The purpose of this validation is simply to ensure a duplicate entity is not created based on Clean Architecture in ASP.NET Core.

The code is as follows:

private async Task ValidateEntityIfExist(EntityModel entityModel, CancellationToken cancellationToken)
{
    Entity entity = await this._repository.GetEntityByIdAsync(entityModel.Id, cancellationToken);
    if (entity != null)
    {
        throw new NotNullReferenceException($"{entityModel} with this id already exists.");
    }
}

I have tried to create a custom NotNullReferenceException class that inherits from Exception class which I use for logging the event:

The code is as follows:

public class NotNullReferenceException : Exception
{
    internal NotNullReferenceException(string message)
        : base(message)
    {
    }
    internal NotNullReferenceException(string message, Exception exception)
        : base(message, exception)
    {
    }
}

Is there a standard Exception to invoke when excepting null instead? If not, what would be the best solution or workaround to such a scenario?


Update

Please note that this is based on Clean Architecture principles. For more information, please visit this link.


Solution

  • In Clean Architecture, exceptions can be handled in a centralized spot to return consistent responses.

    Once the business-level entities are defined, common exceptions and business-specific exceptions should be defined so that they can be shared among projects like API, Function App, Console App, etc..

    Once the exceptions are defined, we need a centralized spot to handle all the pre-defined exceptions as well as the unknown exceptions (most likely a 500 error), log the errors, and then return a consistent API response so that any client that calls the API knows what to expect.

    For example, in your case, we define an EntityAlreadyExistsException at the Core Layer as follows:

    using System;
    using System.Collections.Generic;
    using System.Text;
    
    namespace MyApp.Core.Exceptions
    {
        public class EntityAlreadyExistsException : Exception
        {
            public EntityAlreadyExistsException()
            { }
    
            public EntityAlreadyExistsException(string message) : base(message)
            { }
    
            public EntityAlreadyExistsException(string message, Exception inner) : base(message, inner)
            { }
        }
    }
    

    In order to catch all the exceptions, we can define a filter that runs asynchronously after an action has thrown an exception. For example:

    using MyApp.Core.Exceptions;
    using MyApp.WebAPI.Infrastructure.ApiExceptions;
    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.Filters;
    using Serilog;
    using System;
    using System.Collections.Generic;
    
    namespace MyApp.WebAPI.Infrastructure.Filters
    {
        public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
        {
            private readonly IDictionary<Type, Action<ExceptionContext>> _exceptionHandlers;
    
            public ApiExceptionFilterAttribute()
            {
                // Register known exception types and handlers.
                _exceptionHandlers = new Dictionary<Type, Action<ExceptionContext>>
                {
                    { typeof(EntityAlreadyExistsException), HandleAlreadyExistsException }
                };
            }
    
            public override void OnException(ExceptionContext context)
            {
                HandleException(context);
                base.OnException(context);
            }
    
            private void HandleException(ExceptionContext context)
            {
                Log.Error(context.Exception, "Handling exception:");
                Type type = context.Exception.GetType();
                if (_exceptionHandlers.ContainsKey(type))
                {
                    _exceptionHandlers[type].Invoke(context);
                    return;
                }
                if (!context.ModelState.IsValid)
                {
                    HandleInvalidModelStateException(context);
                    return;
                }
                HandleUnknownException(context);
            }
    
            private void HandleAlreadyExistsException(ExceptionContext context)
            {
                EntityAlreadyExistsException exception = context.Exception as EntityAlreadyExistsException;
                ProblemDetails details = new ProblemDetails()
                {
                    Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
                    Title = "The specified resource already exists.",
                    Detail = exception.Message
                };
                context.Result = new NotFoundObjectResult(details);
                context.ExceptionHandled = true;
            }
        }
    }
    

    Then register the Filter:

    services.AddControllers(options =>
      // handle exceptions thrown by an action
      options.Filters.Add(new ApiExceptionFilterAttribute())
    );
    

    You can find a Template to setup centralized exception handling and logging below:

    https://github.com/ShawnShiSS/clean-architecture-azure-cosmos-db