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?
Please note that this is based on Clean Architecture principles. For more information, please visit this link.
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