Global Exception Handling and Logging in aspnet core webapi

Create a Model for your Error details as below:

public class ErrorDetails
{
	public int StatusCode { get; set; }
	public string Message { get; set; }


	public override string ToString()
	{
		return JsonConvert.SerializeObject(this); //JsonConvert is part of Newtonsoft.Json package.
	}
}

Create the Exception Factory which will handle Exceptions globally in your Api:

public static class ExceptionFactory
{ 
    public static void ConfigureExceptionHandler(this IApplicationBuilder app, int StatusCode = 0, string message = "")
    {
        app.UseExceptionHandler(appError =>
        {
            appError.Run(async context =>
            {
                context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                context.Response.ContentType = "application/json";
                
                var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
                if (contextFeature != null)
                {
                    LogTraceFactory.LogError($"Something went wrong: {contextFeature.Error}");

                    await context.Response.WriteAsync(new ErrorDetails()
                    {
                        StatusCode = context.Response.StatusCode,
                        Message = "Internal Server Error."
                    }.ToString());
                }
            });
        });
    }
}

Register Exception handling in your Api:

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}
	app.ConfigureExceptionHandler();
	.....
}

Also, you can return the error details in your Action Controllers as below:

[HttpGet]
[Route("product/getproductdetails")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public IActionResult GetproductDetails([FromQuery]int productId)
{
	LeadsProduct lead = null;
	if (productId == 0)
	{
		LogTraceFactory.LogError($"Incorrect parameters, productId: {productId}");
		return BadRequest(new ErrorDetails { StatusCode = Convert.ToInt32(HttpStatusCode.BadRequest), Message = $"Missing parameters, productId: {productId} for product Leads." });
	}

	lead = leadService.FetchProductDetails(productId);

	if (lead == null)
	{
		LogTraceFactory.LogError($"product Leads Not Found for productId: {productId}");
		return NotFound(new ErrorDetails { StatusCode = Convert.ToInt32(HttpStatusCode.NotFound), Message = $"product Leads not found for productId {productId}." });
	}

	return Ok(lead);
}

The example above uses the nlog package in the .net core Web Api. You can create LogTraceFactory class as below:

public static class LogTraceFactory
{
	private static ILogger logger = LogManager.GetCurrentClassLogger();

	public static void LogDebug(string message)
	{
		logger.Debug(message);
	}

	public static void LogError(string message)
	{
		logger.Error(message);
	}

	public static void LogInfo(string message)
	{
		logger.Info(message);
	}

	public static void LogWarn(string message)
	{
		logger.Warn(message);
	}
}

Configure nlog package as below in the nlog.config file:

<?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"
      autoReload="true"
      internalLogLevel="Trace"
      internalLogFile="C:\internal_logs\internallog.txt">

  <targets>
    <target name="logfile" xsi:type="File"
            fileName="C:\ProjectLogs\${shortdate}_logfile.txt"
            layout="${longdate} ${level:uppercase=true} ${message}"/>
  </targets>

  <rules>
    <logger name="*" minlevel="Debug" writeTo="logfile" />
  </rules>
</nlog>

You can modify the above configuration as required. As part of the clean architecture, it is better to setup the ExceptionFactory and LogTraceFactory in the Common Layer of your Solution.


Omg! Now you can earn a six figure income. Click here to know how.

How to serialize data using translator C# .net core webapi

Suppose you’re trying to fetch user data from your database using ado.net in you .net core webapi. You have a SQLHelper class that calls a Stored Procedure and returns data that requires to be converted to a DTO object with pre-defined properties in C#.

The SQLHelper class will have the following method to call your Stored Procedure:

public static TData ExtecuteProcedureReturnData<TData>(string connString, string procName,
	Func<SqlDataReader, TData> translator,
	params SqlParameter[] parameters)
{
	using (var sqlConnection = new SqlConnection(connString))
	{
		using (var sqlCommand = sqlConnection.CreateCommand())
		{
			sqlCommand.CommandType = System.Data.CommandType.StoredProcedure;
			sqlCommand.CommandText = procName;
			if (parameters != null)
			{
				sqlCommand.Parameters.AddRange(parameters);
			}
			sqlConnection.Open();
			using (var reader = sqlCommand.ExecuteReader())
			{
				TData elements;
				try
				{
					elements = translator(reader);
				}
				finally
				{
					while (reader.NextResult())
					{ }
				}
				return elements;
			}
		}
	}
}

What is a Translator?

A translator is a class like a DTO in C# which will serialize your data returned from the Stored Procedure into it’s properties.
This will be returned as a json object by your WebApi to your Client front-end.

You can create a Translators folder in your .net core WebApi Project to have all such classes in one place.

An example Translator is as shown below:

public static class UserTranslator
{
	public static User TranslateAsUser(this SqlDataReader reader)
	{
		if (!reader.HasRows)
			return null;
		reader.Read();

		var item = new User();

		if (reader.IsColumnExists("Username"))
			item.Username = SqlHelper.GetNullableString(reader, "Username");

		if (reader.IsColumnExists("FullName"))
			item.FullName = SqlHelper.GetNullableString(reader, "FullName");
			
		if (reader.IsColumnExists("RoleName"))
                item.RoleName = SqlHelper.GetNullableString(reader, "RoleName");

		if (reader.IsColumnExists("Email"))
			item.Email = SqlHelper.GetNullableString(reader, "Email");

		return item;
	}
}

In the above example, you data will have the following columns as Username, FullName and Email. It only returns one row and not a list.

For returning a list:

public static List<User> TranslateAsUsersList(this SqlDataReader reader)
{
	var list = new List<User>();
	while (reader.Read())
	{
		list.Add(TranslateAsUser(reader, true));
	}
	return list;
}

Make sure your reader.Read() method is not called twice.

The DTO for user is as follows:

public class User
{
	public string Username { get; set; }
	public string FullName { get; set; }
	public string RoleName { get; set; }
	public string Email { get; set; }
}

Now, you need to call your Stored Procedure from your Repository:

public User getUserDetails(string UserName)
{
	string connString = CommonUtil.ConnectionString;
	SqlParameter[] param =
	{
		new SqlParameter("@Username", UserName)
	};

	User user = SqlHelper.ExtecuteProcedureReturnData<User>(
		connString,
		"GetUserDetailsFromDB",
		r => r.TranslateAsUser(), //call TranslateAsUsersList if List of Users is required and return List<User>
		param
		);

	return user;
}

Assuming, you’re using the Repository pattern in your WebApi Data Layer. Else, you can call the above method however your Project structure works.

I’ve written another post on multiple ways to fetch data for calling StoredProcedure in your WebApi for your SQLHelper class.

How to read connection strings stored in appsettings file C#

This post is based on a setup of an asp.net core application. Configuration is read in the Startup class upon the Application startup. The Configure method in this class calls the ApiBootstrapper to check whether the connection string for Dev or Production is required.
This can be further used to call the Stored Procedures or query tables using ADO.Net.

Appsettings.json file is the asp.net core config file. This file contains the Connection Strings is as shown below:

{
	"configSetting": {
		"ConnectionStrings": {
			"ProdConnection": "Data Source=ServerName;Initial Catalog=DBProd;UID=username;PWD=password;",
			"DevConnection": "Data Source=ServerName;Initial Catalog=DBDev;UID=username;PWD=password;"
		},
		"Parameters": {
			"IsProduction": true
			"IsDev": false
		}
	}
}
public class Startup
{
	public Startup(IConfiguration configuration)
	{
		Configuration = configuration;
	}
	
	public IConfiguration Configuration { get; }
	
	public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
	{
		////
	
	
		ApiBootstrapper.Initialize(Configuration);
	}
	
}

Below is the code for ApiBootstrapper class:

public class ApiBootstrapper
{
	
	public static void Initialize(IConfiguration configuration)
	{
		CommonUtil.IsProduction = configuration.GetSection(ConfigKeys.configSetting.ToString()).GetSection(ConfigKeys.Log.ToString()).Value.ToString();
		if (Convert.ToBoolean(CommonUtil.IsProduction))
		{
			CommonUtil.ConnectionString = configuration.GetSection("configSetting").GetSection("ConnectionStrings").GetSection("ProdConnection").Value.ToString();
		}
	}
}

The above example shows how we can store Connection Strings for different environments like Dev and Prod and read it based on Config file settings.

Managing Custom Errors with Asp.net

You never want your users to see that yellow screen which shows up when a run-time or design-time error occurs in Asp.Net. However, a developer might want to see the error which may help in finding out the issue.

We have the following Custom error modes in Asp.net that can be set in web.config file:

  • Off: shows the actual error on the screen for all users.
  • On: shows only the custom error page and not the error details to all users.
  • RemoteOnly: shows the error details only to the local users where the Application is running. But does not show it to the outside users.

We recently faced a scenario where one of our Asp.Net Application was returning 3xx series status code from IIS Server for non-existent pages. This was flagged as a possible Security flaw by the team.
e.g. https://abc.com/xyz.aspx

So, if the page xyz.aspx does not exist, the Server will return 404 status code by default.

The following CustomErrors setting by default will give 404 status code:

<customErrors mode="Off" defaultRedirect="Error.htm"/>

We have used CustomErrors in our Web.config file which by the default behaviour of Asp.Net will make the IIS send the following response…
• With status code 302: Found, which effectively means a redirect
• Having a Location response header where the resource should be requested (in this case, the generic error page).
In the end, because the generic error page is static and does not change, when that is requested over same session IIS may return the response 304: Not modified.

Asp.Net CustomErrors setting in Web.Config file:

<customErrors mode="On" defaultRedirect="Error.htm"/>

The below setting produces the same result:

<customErrors mode="On" defaultRedirect="Error.htm">
    <error statusCode="404" redirect="FileNotFound.htm" />
</customErrors>

Similarly, you can manage other status codes.

The default behaviour of Asp.Net returning 3xx series status codes is by design for redirect done by Custom Errors and could be a false Security alert.

Prevent form submission with Javascript button click

Suppose you have a html form and you need to prevent the submission of a form based on the input provided in a textbox.
The html input type should be “button” in this case.

<input type="button" value="Submit" onclick="checkInput();">

Below is the Javascript code that gets called on the button click:

function checkInput() {
	var form = document.getElementById('form1');
	var str = document.getElementById("txtBox").value; 
	if (str == "") {
		var r = confirm("Do you want to add the detail in the input box?");
		if (r == true) {
			document.getElementById("txtBox").focus();
		} else {
			form.submit();
		}
	}
	else {
		form.submit();
	}
}

The above code will submit the form if field is not blank. If the field is blank, focus gets set to the textbox field named “txtBox” when clicking on OK button. Clicking on Cancel will again submit the form.

Debug classic asp application hosted on IIS with Visual Studio

Some non .Net Applications like the ones written in classic ASP are required to be debugged in Visual Studio. Since these are not hosted on IIS Express, but on IIS, you need to identify the worker process running your machine or the Server and attach the w3wp.exe with the Debug tool in Visual Studio.

Enable Debugging under IIS classic ASP section as shown below:

Under the Debug menu in Visual Studio, select “Attach to Process”:

There may be multiple worker processes running on the machine depending on how many applications are running under IIS. Match the right one with the correct ProcessID.

Add the debug points in your Asp file and hit the required Page in the browser.

Change Javascript attribute for asp.net textbox using c#

Suppose we have the following asp.net textbox in a UserControl with the onfocusout javascript method that passes validation as 50000 characters.

<telerik:RadTextBox TextMode="MultiLine" ID="txtComments" onfocusout="return CheckLength(this, 50000);" Height="100px"  runat="server" CssClass="TextMulti" Width="99%" EnableSingleInputRendering="False">
</telerik:RadTextBox>

For a particular scenario, you may need to change the number of characters to say 6000.

This can be done dynamincally in the UserControl C# code as below:

txtComments.Attributes["onfocusout"] = "return CheckLength(this, 6000);";