Welcome to the nineteenth episode of my course “Becoming a software developer” in which we will gracefully handle the exceptions and extend logging services with NLog.
All of the materials including videos and sample projects can be downloaded from here.
The source code repository is being hosted on GitHub.
Scope
- Exceptions
- NLog
Abstract
Exceptions
Dealing with exceptions from the API’s point of view is not an easy task. In the end, we want to return meaningful error codes to the end customers of our service once something goes wrong. It doesn’t matter whether it’s an internal server error or some client validation issue – returning simply an HTTP Status Code that something went wrong doesn’t really help.
Let’s start from the very begging, at first, we can define the custom exception type like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public abstract class PassengerException : Exception { public string Code { get; } protected PassengerException() { } protected PassengerException(string code) { Code = code; } protected PassengerException(string message, params object[] args) : this(string.Empty, message, args) { } protected PassengerException(string code, string message, params object[] args) : this(null, code, message, args) { } protected PassengerException(Exception innerException, string message, params object[] args) : this(innerException, string.Empty, message, args) { } protected PassengerException(Exception innerException, string code, string message, params object[] args) : base(string.Format(message, args), innerException) { Code = code; } } |
Having this base class, we can go further and implement specialized types for different kinds of exceptions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public class DomainException : PassengerException { public DomainException() { } public DomainException(string code) : base(code) { } public DomainException(string message, params object[] args) : base(string.Empty, message, args) { } public DomainException(string code, string message, params object[] args) : base(null, code, message, args) { } public DomainException(Exception innerException, string message, params object[] args) : base(innerException, string.Empty, message, args) { } public DomainException(Exception innerException, string code, string message, params object[] args) : base(string.Format(message, args), innerException) { } } |
How can we actually pass further custom error codes? For sure, we could use “magic strings”, but why not move these properties to a separate class and use it in the following way:
1 2 3 4 5 6 7 |
public static class ErrorCodes { public static string InvalidEmail => "invalid_email"; public static string InvalidPassword => "invalid_password"; public static string InvalidRole => "invalid_role"; public static string InvalidUsername => "invalid_username"; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
public void SetUsername(string username) { if(!NameRegex.IsMatch(username)) { throw new DomainException(ErrorCodes.InvalidUsername, "Username is invalid."); } if (String.IsNullOrEmpty(username)) { throw new DomainException(ErrorCodes.InvalidUsername, "Username is invalid."); } Username = username.ToLowerInvariant(); UpdatedAt = DateTime.UtcNow; } public void SetEmail(string email) { if (string.IsNullOrWhiteSpace(email)) { throw new DomainException(ErrorCodes.InvalidEmail, "Email can not be empty."); } if (Email == email) { return; } Email = email.ToLowerInvariant(); UpdatedAt = DateTime.UtcNow; } |
Eventually, we can extend the custom exception handler by adding another case for handling our own exception types:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
private static Task HandleExceptionAsync(HttpContext context, Exception exception) { var errorCode = "error"; var statusCode = HttpStatusCode.BadRequest; var exceptionType = exception.GetType(); switch(exception) { case Exception e when exceptionType == typeof(UnauthorizedAccessException): statusCode = HttpStatusCode.Unauthorized; break; case ServiceException e when exceptionType == typeof(ServiceException): statusCode = HttpStatusCode.BadRequest; errorCode = e.Code; break; case Exception e when exceptionType == typeof(Exception): statusCode = HttpStatusCode.InternalServerError; break; } var response = new { code = errorCode, message = exception.Message }; var payload = JsonConvert.SerializeObject(response); context.Response.ContentType = "application/json"; context.Response.StatusCode = (int)statusCode; return context.Response.WriteAsync(payload); } |
NLog
Default logging available within ASP.NET Core framework is fine, however, quite often we would actually like to use something more sophisticated in terms of logging services. Having the ability to specify different outputs like files, console, databases, external services, defining templates of the messages being logged and so on. This is where libraries like NLog come in handy.
In order to start working with NLog, you need to install the base package in the infrastructure project as well as other 2 extensions designed for ASP.NET Core that will be used by the API. Once the packages are installed, you need to do the 3 things: configure the NLog in Startup class, create a nlog.config where you can specify all of the behaviors, outputs (targets) and much more if needed and mark this file as part of the content that should be copied to the output directory.
1 2 3 4 5 6 7 8 |
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime appLifetime) { loggerFactory.AddNLog(); app.AddNLogWeb(); env.ConfigureNLog("nlog.config"); ... } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <targets> <target name="logfile" xsi:type="File" fileName="file.txt" /> <target name="console" xsi:type="Console" /> </targets> <rules> <logger name="*" minlevel="Trace" writeTo="logfile" /> <logger name="*" minlevel="Info" writeTo="console" /> </rules> </nlog> |
1 2 3 4 5 |
<ItemGroup> <None Update="nlog.config"> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> </None> </ItemGroup> |
Please note that this is a very basic configuration and you can extend it in order to achieve a really sophisticated logging. And finally, you need to copy the configuration file and mark it as a content in the End-to-End test project in order to run the integration tests properly.
In order to use NLog, simply add the following line of code to the particular class and that’s it.
1 |
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); |
Next
In the next episode, we will handle not only the exceptions but the actual business logic operations within specialized handlers in a more fluent way.
Pingback: Dew Drop - June 1, 2017 (#2491) - Morning Dew
great
The episode of course about software developer available in this page. I have learn existing course projects in this blog posts. Very clearly to handle for each videos and subjects for the readers. Source code and sample projects are available to get good understandable and knowledge about logging service. In this part of course is getting the dealing and exceptions and how to handle the error codes, internal server errors etc. And get the possible logging and NLog related codes also. Other 2 extensions are possible to design for ASP.NET core. The packages installation and identify the behaviors of its to be followed in this post. Thanks for explanation about that
Podczas dodawania w switchu case’a dla naszego wyjątku Service odwołujemy się do Passanger.Core co chcieliśmy chyba uniknąć? W .NET Core zmienili Izolacje między warstwami i gdy nie dodamy do odpowiedniego .csproj atrybutu PrivateAssets=”All” to warstwa nadrzędna będzie widziała niższe warstwy. Natomiast gdy dodamy PrivateAssets=”All” to kompilator krzyczy, że nie ma referencji do Core’a. Więc coś jest nie tak z założeniem tej izolacji.