Clean Architecture in .Net

Clean Architecture is a software design pattern that emphasizes separation of concerns, maintainability, and testability in .NET applications. It was introduced by Robert C. Martin, also known as Uncle Bob, in his book “Clean Architecture: A Craftsman’s Guide to Software Structure and Design”.

Clean Architecture involves breaking down a .NET application into multiple layers, each with its own set of responsibilities and dependencies. These layers include:

  • Presentation layer: Exposes the Web API endpoints and handles HTTP requests and responses. Depends on the Application layer interfaces.
  • Application layer: Implements the use cases of the application, orchestrating the Domain layer and Infrastructure layer to perform the necessary actions. Depends on the Domain layer and Infrastructure layer interfaces.
  • Domain layer: Defines the business entities and logic of the application, independent of the infrastructure or presentation details. Implements the Domain layer interfaces.
  • Infrastructure layer: Implements the interfaces defined in the Application layer and Domain layer, providing the necessary services and resources to accomplish the tasks. Depends on external services and libraries.
  • Tests: Contains unit and integration tests for the application.

The key principle of Clean Architecture is the Dependency Inversion Principle (DIP), which states that high-level modules should not depend on low-level modules; both should depend on abstractions. This allows for flexibility in the design and promotes modularity and testability.

Implementing Clean Architecture in .NET involves using design patterns such as Dependency Injection (DI), Inversion of Control (IoC), and Separation of Concerns (SoC) to achieve loose coupling and high cohesion between the layers. This helps to make the application more modular and easier to maintain, test, and evolve over time.

MyApp/
├── MyApp.Api/                      # Presentation layer (Web API)
│   ├── Controllers/               # HTTP controllers
│   ├── Filters/                   # Action filters
│   ├── Program.cs                 # Web API entry point
│   └── Startup.cs                 # Web API configuration
├── MyApp.Application/              # Application layer (Use cases)
│   ├── Commands/                  # Command handlers
│   ├── Queries/                   # Query handlers
│   ├── Interfaces/                # Application interfaces (ports)
│   └── MyApp.Application.csproj   # Application layer project file
├── MyApp.Domain/                   # Domain layer (Business entities and logic)
│   ├── Entities/                  # Domain entities
│   ├── Exceptions/                # Domain exceptions
│   ├── Interfaces/                # Domain interfaces (ports)
│   ├── Services/                  # Domain services
│   └── MyApp.Domain.csproj        # Domain layer project file
├── MyApp.Infrastructure/           # Infrastructure layer (Database, I/O, external services)
│   ├── Data/                      # Data access implementation (EF Core, Dapper, etc.)
│   ├── External/                  # External services implementation (REST APIs, gRPC, etc.)
│   ├── Migrations/                # Database migrations (EF Core)
│   ├── Interfaces/                # Infrastructure interfaces (ports)
│   ├── Logging/                   # Logging implementation
│   ├── MyApp.Infrastructure.csproj# Infrastructure layer project file
│   └── SeedData/                  # Seed data for database
├── MyApp.Tests/                    # Unit and integration tests
│   ├── MyApp.UnitTests/            # Unit tests
│   └── MyApp.IntegrationTests/     # Integration tests
└── MyApp.sln                       # Solution file

This is just one example of a possible clean architecture project structure. You can adapt it to your specific needs and preferences.
Advertisement

The request was aborted: Could not create SSL/TLS secure channel

Since most Servers are moving towards TLS 1.3 and removing TLS 1.0/1.1 support, it is important to make note of certain Server configurations that might be required to make your .Net Framework Application compatible with new TLS versions like TLS 1.2.

Just upgrading the Application to latest .Net Framework like 4.8 version, which as per documentation states it automatically handles the compatibility with newer TLS versions when older TLS versions are disabled.

I have managed to resolve the issues on my server by updating the SSL Cipher Suite Order, I had mistakenly removed some of the suites that windows suggested was for TLS1.0 and 1.1 only when in actual fact they were needed for some TLS1.2 connections as well.

I resolved my issues by:

  1. Open Run Prompt and run gpedit.msc
  2. Navigate to “Administrative Templates > Network > SSL Configuration Settings”
  3. Open SSL Cipher Suite Order
  4. Select Enabled
  5. Paste the list of suites below into the text box (make sure there are no spaces)
  6. Click Apply
  7. Restart the server

SSL SUITES:

TLS_DHE_RSA_WITH_AES_256_GCM_SHA384,TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384_P256,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256_P256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256_P256,TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384_P384,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256_P256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384_P384,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA_P256,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA_P256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA_P256,TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA_P256,TLS_DHE_DSS_WITH_AES_128_CBC_SHA,TLS_DHE_DSS_WITH_AES_256_CBC_SHA

Note, these suites work for me but you may require other ones for different applications. You should be able to find a full list and more info on the suites here https://docs.microsoft.com/en-us/windows/win32/secauthn/cipher-suites-in-schannel?redirectedfrom=MSDN

You can also use a tool like IISCrypto to update the Cipher Suite order.

Retry and Circuit Breaker Policy example .Net 6 and Polly

In this example, we’ll implement the Wait and Retry and Circuit Breaker policy using .Net 6 Web API and Polly. For more details on what Circuit Breaker is, refer to the MSDN documentation.

Create a WebAPI with ValuesController in .Net 6 which will always return an Exception in the Get call.

using Microsoft.AspNetCore.Mvc;

namespace ExternalAPI.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class ValuesController : ControllerBase
    {

        private readonly ILogger<ValuesController> _logger;

        public ValuesController(ILogger<ValuesController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            throw new Exception("Error");
        }

    }
}

Let’s call this ExternalAPI and running on https://localhost:7250/.

Create another .Net 6 WebAPI called CircuitBreaker.Demo and install Polly and Newtonsoft.json Nuget packages.

Configure Polly and HttpClient in Program.cs file as shown below:

using Polly;
using Polly.Extensions.Http;

var builder = WebApplication.CreateBuilder(args);
var httpRetryPolicy = Policy.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
    
    .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(retryAttempt));

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddHttpClient("errorApi", c => { c.BaseAddress = new Uri("https://localhost:7250"); });
builder.Services.AddSingleton<IAsyncPolicy<HttpResponseMessage>>(httpRetryPolicy);


var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Now add the PollyController to call the ExternalAPI using the HttpClient:

using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Polly;

namespace CircuitBreaker.Demo.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class PollyController : ControllerBase
    {

        private readonly ILogger<PollyController> _logger;
        private readonly IHttpClientFactory _httpClientFactory;
        private readonly IAsyncPolicy<HttpResponseMessage> _policy;

        public PollyController(ILogger<PollyController> logger, IHttpClientFactory httpClientFactory, IAsyncPolicy<HttpResponseMessage> policy)
        {
            _logger = logger;
            _httpClientFactory = httpClientFactory;
            _policy = policy;
        }

        [HttpGet(Name = "GetApiResult")]
        public async Task<ActionResult<IEnumerable<string>>> Get()
        {
            var client = _httpClientFactory.CreateClient("errorApi");
            var response = await _policy.ExecuteAsync(() => client.GetAsync("values"));
            response.EnsureSuccessStatusCode();
            return JsonConvert.DeserializeObject<string[]>(await response.Content.ReadAsStringAsync());
        }

    }
}

When you run both the APIs, the call to External APIs is automatically made using Wait and Retry policy after every retryAttempt number of seconds 3 times. After that, the calls are stopped as the ExternalAPI keeps giving 500 error.

CircuitBreaker example:

Now change the Http Retry Policy line in the Program.cs file to:

var httpRetryPolicy = Policy.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
    .CircuitBreakerAsync(2, TimeSpan.FromSeconds(30));

The above line will open the circuit and will start giving Circuit Open Exception after 2 attempts and keep the circuit Open for 30 seconds, which means you cannot make further calls to the API for that duration. The Exception will now be shown as below:

Call .Net core API from Console with App Bearer token

In the following example, we’re using a .Net Core 3.1 Console App that will call API with POST request that requires Authentication with a bearer token in Authrorization Header. The token is generated by passing credentials to another API endpoint.

For more details on how to use appSettings.json file in Console App, check this post.

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;

namespace ConsoleApp1
{

    class Credentials
    {
        public string username { get; set; }
        public string password { get; set; }
    }

    class Token
    {
        public string token { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            string ResponseString = "";
            HttpWebResponse response = null;
            HttpWebResponse response2 = null;
            IConfiguration Config = new ConfigurationBuilder()
                .AddJsonFile("appSettings.json")
                .Build();

            try
            {
                var baseURL = Config.GetSection("baseURL").Value;
                var request = (HttpWebRequest)WebRequest.Create(baseURL + "/token");
                request.Accept = "application/json"; //"application/xml";
                request.Method = "POST";

		//Get credentials from config.
                var dusername = EncryptionService.Decrypt(Config.GetSection("credentials")["username"]);
                var dpassword = EncryptionService.Decrypt(Config.GetSection("credentials")["password"]);

                Credentials cred = new Credentials()
                {
                    username = dusername,
                    password = dpassword
                };

                var myContent = JsonConvert.SerializeObject(cred);

                var data = Encoding.ASCII.GetBytes(myContent);

                request.ContentType = "application/json";
                request.ContentLength = data.Length;

                using (var stream = request.GetRequestStream())
                {
                    stream.Write(data, 0, data.Length);
                }

                using (response = (HttpWebResponse)request.GetResponse())
                {
                    ResponseString = new StreamReader(response.GetResponseStream()).ReadToEnd();
                }

		//Get the token from the /token end-point and call another end-point.
                Token token = JsonConvert.DeserializeObject<Token>(ResponseString);

                var request2 = (HttpWebRequest)WebRequest.Create(baseURL + "/ProcessData");
                request2.Accept = "application/json"; //"application/xml";
                request2.Method = "POST";
				
		//Pass token in Authorization Header.
                request2.Headers["Authorization"] = "Bearer " + token.token;

                using (response2 = (HttpWebResponse)request2.GetResponse())
                {
                    ResponseString = new StreamReader(response2.GetResponseStream()).ReadToEnd();
                }

                Console.WriteLine(ResponseString);
                Environment.Exit(0);
            }
            catch (WebException ex)
            {
                if (ex.Status == WebExceptionStatus.ProtocolError)
                {
                    response = (HttpWebResponse)ex.Response;
                    ResponseString = "Some error occured: " + response.StatusCode.ToString();
                }
                else
                {
                    ResponseString = "Some error occured: " + ex.Status.ToString();
                }
            }

        }
    }

}

Use config file in .Net core Console App

For this example, I’m using a Console App created in .Net Core 3.1 using Visual Studio 2019.

Add a json file to your Project and name it appSettings.json, it could like like below:

{
  "credentials": {
    "username": "xxxx",
    "password": "xxxx"
  },
  "URL": "https://myapi.abc.com"
}

Install the following Nuget packages in your Project:

<PackageReference Include="Microsoft.Extensions.Configuration" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />

The versions could vary depending on the time you’re adding these packages.

In your Program.cs file, add the namespace:

using Microsoft.Extensions.Configuration;

In the Main method, add the following code:

class Program
{
    static void Main(string[] args)
    {
		//....
		
		IConfiguration Config = new ConfigurationBuilder()
                .AddJsonFile("appSettings.json")
                .Build();
				
		var URL = Config.GetSection("URL").Value;
		
		//Assuming you're using Encrypted values in configuration.
		var dusername = EncryptionClass.Decrypt(Config.GetSection("credentials")["username"]);
        var dpassword = EncryptionClass.Decrypt(Config.GetSection("credentials")["password"]);
		
		//.....
		
    }
}

Abstract Factory pattern C#

Let’s begin with a story of 2 brothers Bill and Steve. Both love Pizza and Ice Cream. They go to a Food Court which has Pizza outlets like Pizza Hut and Dominos Pizza and Ice Cream Parlours like Baskin Robbins and Mikey.

Taking this real world pattern into our Software design by creating classes. In this context, Food Court will be our FoodFactory abstract class that creates 2 abstract Products Pizza and Ice Cream. The Food Court has 2 sections Fun and Frolic. Bill goes for Fun and Steve goes to Frolic.

public abstract class FoodFactory
{
    public abstract Pizza BakePizza();
    public abstract IceCream MakeIceCream();
}

Create an Abstract Products folder and add the following 2 classes:

public class Pizza {

}

public class IceCream {

}

Create a Products Folder and add the following classes:

public class DominosPizza: Pizza
{
    public DominosPizza()
    {
    }
}
public class PizzaHut: Pizza
{
    public PizzaHut()
    {
    }
}
public class Mikey: IceCream
{
    public Mikey()
    {
    }
}
public class BaskinRobbins: IceCream
{
    public BaskinRobbins()
    {
    }
}

Now, suppose Fun section has Pizza Hut and Mikey and Frolic has Dominos Pizza and Baskin Robbins. Create a folder Factories and under that add the following classes:

class FunFoodFactory : FoodFactory
{
    public override Pizza BakePizza()
    {
        return new PizzaHut();
    }

    public override IceCream MakeIceCream()
    {
        return new Mikey();
    }
}
public class FrolicFoodFactory : FoodFactory
{
    public override Pizza BakePizza()
    {
        return new DominosPizza();
    }

    public override IceCream MakeIceCream()
    {
        return new BaskinRobbins();
    }
}

Now the 2 Good brothers start having fun. Create a class called GoodBrothers as below:

public class GoodBrothers
{
    private readonly Pizza _pizza;
    private readonly IceCream _icecream;

    public GoodBrothers(FoodFactory factory)
    {
        _pizza = factory.BakePizza();
        _icecream = factory.MakeIceCream();
    }

    public string WhatDidYouHaveToday()
    {
        return $"Today I ate at {_pizza.GetType().Name} and {_icecream.GetType().Name}";
    }
}

From the main class we ask them What did you have today?

class Program
{
    static void Main(string[] args)
    {
        GoodBrothers Bill = new GoodBrothers(new FunFoodFactory());
        Console.WriteLine($"Bill: {Bill.WhatDidYouHaveToday()}");

        GoodBrothers Steve = new GoodBrothers(new FrolicFoodFactory());
        Console.WriteLine($"Steve: {Steve.WhatDidYouHaveToday()}");

        Console.ReadKey();
    }
}

Output:

Bill: Today I ate at PizzaHut and Mikey
Steve: Today I ate at DominosPizza and BaskinRobbins

A similar example for Abstract Factory pattern can be for a Clothes Factory abstract class which creates abstract Products like Shirts and Trousers. A Businessman may buy from ElegantFactory and a student may buy from CasualFactory. Shirts can be of types Formal and PoloTs and Trousers can be of type Suit and Jeans.

The ElegantFactory will return types Formal Shirt and Suit Trousers. CasualFactory will return types PoloTs and Jeans. We can create a Client class like GoodBrothers that receives the ClothesFactory reference in it’s constructor to create a Shirt and Trouser for the Businessman and Student and so on.

Support pre-flight OPTIONS requests in .Net WebAPI

A way to support the ‘OPTIONS’ pre-flight request in .net WebAPI is to by-pass this request with a default response and returning the required Headers.

Add the below code in your Global.asax.cs file under the Application_BeginRequest method:

protected void Application_BeginRequest()
{
	if (Request.HttpMethod == "OPTIONS")
	{
		HttpContext.Current.Response.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
		HttpContext.Current.Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, expiry, id, token, token-type");
		HttpContext.Current.Response.AddHeader("Access-Control-Allow‌​-Credentials", "true");
		HttpContext.Current.Response.AddHeader("Access-Control-Max-Age", "1728000");
		HttpContext.Current.Response.End();
	}
}

So when you call the WebAPI with Javscript fetch/axios, the pre-flight request sent before the actual request would get the 200 status code.

The example above shows some sample custom headers and allowed methods that will be returned in Response Headers.

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.

Write C# method that returns DataSet from Stored Procedure

Suppose you are writing a Helper class in your .Net Project that uses ADO.Net in the Data Layer. And you need to call Stored Procedures a lot. Writing a generic Helper method that takes in an Array of SqlParameters can be used so that you don’t have re-write the same code of calling the Stored Procedure again and again.

Below is the code that I’ve used as a general approach to call Stored Procedure and return DataSet:

public static DataSet ExecuteProcedureReturnDataSet(string connString, string procName,
            params SqlParameter[] paramters)
{
	DataSet result = null;
	using (var sqlConnection = new SqlConnection(connString))
	{
		using (var command = sqlConnection.CreateCommand())
		{
			using (SqlDataAdapter sda = new SqlDataAdapter(command))
			{
				command.CommandType = System.Data.CommandType.StoredProcedure;
				command.CommandText = procName;
				if (paramters != null)
				{
					command.Parameters.AddRange(paramters);
				}
				result = new DataSet();
				sda.Fill(result);
			}
		}
	}
	return result;
}

Another way to call Stored Procedure would be to return a single value from the Stored Procedure like a string. You can use the below method to return only a String:

public static string ExecuteProcedureReturnString(string connString, string procName,
            params SqlParameter[] paramters)
{
	string result = "";
	using (var sqlConnection = new SqlConnection(connString))
	{
		using (var command = sqlConnection.CreateCommand())
		{
			command.CommandType = System.Data.CommandType.StoredProcedure;
			command.CommandText = procName;
			if (paramters != null)
			{
				command.Parameters.AddRange(paramters);
			}
			sqlConnection.Open();
			var ret = command.ExecuteScalar();
			if (ret != null)
				result = Convert.ToString(ret);
		}
	}
	return result;
}

Example of SqlParameter array to be passed to the above methods can be as follows:

SqlParameter[] params =
{
	new SqlParameter("@name", name),
	new SqlParameter("@year", year)
};

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.