Global Error Handling In .Net 6.0 With ElasticSearch
Hi, today we will talk about catching an error Globally in two ways on .Net Core 6.0. Later we will log this error into Elasticsearch and return Custom Error Model to the client.
You can get very kind of errors from all over the solution. So sometimes you may want to make extra operations for some specific errors. Or may you want to make global operations for all errors.
First Let’s Send Custom Forbidden Exception From the Controller:
We will send a “403 Forbidden” error message from the controller for testing the global error exception catcher class.
- “ex.Data.Add()” method gets Dictionary parameter. So we add statusCode as a key and statusMessage as a value dictionary parameter into the Custome Exception’s Data and send it.
UserController/GetTopOneUser():
public void GetTopOneUser()
{
//throw custom exception
int statusCode = 403;
string statusMessage = "You are not authorized for this page";
var ex = new Exception(string.Format("{0} - {1}", statusMessage, statusCode));
ex.Data.Add(statusCode, statusMessage); // store 403and "Forbidden Parameters"
throw ex;
}
Habit is the nursery of errors. — Victor Hugo
1-) First Method Api Extension:
Firstly we will create the “IApplicationBuilder” Extention class. And Catch exceptions globally. And finally, we will attach the Response to the current Context.
1.We created ConfigureExceptionHandler extension class for IApplicationBuilder. We will catch all App Exceptions on “UseExceptionHandler()” method.
2. We will set up the ElasticSearch configuration. We will talk about it later. We will check if is there any error in the context’s Features. If there is an error in it, we will get an error message and error code form in it and create a new Dictionary for our custom exception.
3. We will create Custome ErrorLogModel for CreatingIndex in ElasticSearch.
public class ErrorLogModel
{
public int UserID { get; set; }
public string Message { get; set; }
public string Controller { get; set; }
public string Action { get; set; }
public string Method { get; set; }
public string Service { get; set; }
public DateTime PostDate { get; set; }
public int ErrorCode { get; set; }
}
We will get Action and Controller name from the “context.Request.Path” for logging. We will set the current time and get UserID from Request Header. And finally, we will save the “logModel” Index to the ElasticSearch (error_log Index).
4. We will check if is there any error in the Error’s Data. If it is yes, we will loop every error and add it to the Exception Dictionary. And finally, we will write “Error Message” to the context Response.
The greatest mistake is to imagine that we never err.
— Thomas Carlyle
Infrastructor/ExceptionMiddlewareExtensions.cs:
appsettings.json/ElasticConnectionSettings:
We’ll go over the ElasticSearch CoreLibrary roughly. Because ElastichSearch is a totally different subject. As seen below, elastic connection and credentials are encrypted for security reasons. And all three index names are defined in the config file.
ElasticSearch/IElasticSearchService.cs: These are Elastic Operations. Create<T> Dynamic Index and Search from specific 3 Index(Error, Audit, and Login). In this article, we will talk about only Creating or Inserting Index to ElasticSearch.
ElasticSearch/ElasticSearchService.cs:
- ElasticClientProvider is setting ElasticConfig and creating a singleton Elastic-Client class. We will talk about it later.
- CheckExistsAndInsertLog() method. In this example, we will create the “error_log” index, if not exists. NumberOfReplica and NumberofShards are set for improving performance and swapping between master with the slave machine for disaster scenarios.
- “Aliases(a => a.Alias(indexName)))”: Setting aliases is very important for ellasticsearch. When making changes to the index in the future, it ensures that the previously saved documents can be easily created without losing them.
- “IndexResponse responseIndex = _client.Index<T>(logModel, idx => idx.Index(indexName))”: After Index created or if it is already exist, we will saved error logModel into the error_log index. Actually, this method is “Generic<T>” so you can save every model to any index on elasticsearch.
ElasticSearch/ElasticClientProvider:
We will create ElasticClient with config information. All configuration texts are encrypted for security reasons. So we used an external encryption class to Decrypt all config information.
Security/Encryption.cs: This is our encryption class. We will use “TripleDES” for encoding and decoding. For security reasons, you have to encrypt all important text before saving it to config. After all, when we read config information from the appsettings.json, we have to decrypt all text.
Startup.cs: Now we have to call our “ConfigureExceptionHandler()” method on Startup.cs “app” is inherited from the IApplicationBuilder interface. And we wrote an extension on the IApplicationBuilder interface. So now we are monitoring every error on the app. And if it happens, we will log into the ElasticSearch. And we can use the Kibana tool for Monitoring to ErrorLogs order by date or we can filter with any specific property.
.
.
app.ConfigureExceptionHandler();
.
.
Never interrupt your enemy when he is making a mistake. — Napoleon
2-) Second Method Creating Middleware Class:
In this second method, we will implement two methods in this middleware class. “Invoke()” and “HandelExceptionAsync()” methods. After all, we will inject the “GlobalErrorHandlingMiddleware” class by using “UseMiddleware()” to the app in .Net 6.0
1.We will get the RequestDelegate on the Constructor of GlobalErrorHandlingMiddleware class with the “RequestDelegate next” parameter and set private “_next” variable.
2.We will get the current HttpContext with the “Invoke()” method as a parameter.
DeepNote: “RequestDelegate” is a task that represents the completion of request processing.
In the “try {} catch{}” block, we will try the finish current HtppContext Request, if we get an error we will call the asynchronous “HandleExceptionAsync()” method.
3.On HandelExceptionAsync() method we will get error message, stackTrace. And if we want, we can take action and controller name from the “context.Request.Path.Value” as before example. We will inject our custome error response to the current context. And return it.
We will inject this middleware class into the app as below in Startup.cs:
Startup.cs:
.
.
app.UseMiddleware(typeof(GlobalErrorHandlingMiddleware));
.
.
Infrastructor/GlobalErrorHandlingMiddleware.cs:
This is the result screen:
Conclusion:
Our main purpose is managing to the solution of errors in one place. So later you can change, extend or remove some error operations easily. You can test and monitor generally and efficiently. For error logging these days, you can use Elasticsearch but maybe later you can decide to Index your error document in mongoDB, cosmosDB, or anywhere. You can make extra operations for specific error codes globally. For example, if you get “500 Error” you can send an email to the admin.
We do not disturb the general flow. If we met any error, we only attached our error log document to the “context.Response”. We didn’t talk about multi-language support in this article. And we wrote everything in English. That is entirely the subject of another article.
See you until the next article.
“If you have read so far, first of all, thank you for your patience and support. I welcome all of you to my blog for more!”
Source: