Connection Resiliency and Command Interception with the Entity Framework in an ASP.NET MVC Application
by Tom Dykstra
Download Completed Project or Download PDF
The Contoso University sample web application demonstrates how to create ASP.NET MVC 5 applications using the Entity Framework 6 Code First and Visual Studio 2013. For information about the tutorial series, see the first tutorial in the series.
So far the application has been running locally in IIS Express on your development computer. To make a real application available for other people to use over the Internet, you have to deploy it to a web hosting provider, and you have to deploy the database to a database server.
In this tutorial you’ll learn how to use two features of Entity Framework 6 that are especially valuable when you are deploying to the cloud environment: connection resiliency (automatic retries for transient errors) and command interception (catch all SQL queries sent to the database in order to log or change them).
This connection resiliency and command interception tutorial is optional. If you skip this tutorial, a few minor adjustments will have to be made in subsequent tutorials.
Enable connection resiliency
When you deploy the application to Windows Azure, you’ll deploy the database to Windows Azure SQL Database, a cloud database service. Transient connection errors are typically more frequent when you connect to a cloud database service than when your web server and your database server are directly connected together in the same data center. Even if a cloud web server and a cloud database service are hosted in the same data center, there are more network connections between them that can have problems, such as load balancers.
Also a cloud service is typically shared by other users, which means its responsiveness can be affected by them. And your access to the database might be subject to throttling. Throttling means the database service throws exceptions when you try to access it more frequently than is allowed in your Service Level Agreement (SLA).
Many or most connection problems when you’re accessing a cloud service are transient, that is, they resolve themselves in a short period of time. So when you try a database operation and get a type of error that is typically transient, you could try the operation again after a short wait, and the operation might be successful. You can provide a much better experience for your users if you handle transient errors by automatically trying again, making most of them invisible to the customer. The connection resiliency feature in Entity Framework 6 automates that process of retrying failed SQL queries.
The connection resiliency feature must be configured appropriately for a particular database service:
- It has to know which exceptions are likely to be transient. You want to retry errors caused by a temporary loss in network connectivity, not errors caused by program bugs, for example.
- It has to wait an appropriate amount of time between retries of a failed operation. You can wait longer between retries for a batch process than you can for an online web page where a user is waiting for a response.
- It has to retry an appropriate number of times before it gives up. You might want to retry more times in a batch process that you would in an online application.
You can configure these settings manually for any database environment supported by an Entity Framework provider, but default values that typically work well for an online application that uses Windows Azure SQL Database have already been configured for you, and those are the settings you’ll implement for the Contoso University application.
All you have to do to enable connection resiliency is create a class in your assembly that derives from the DbConfiguration class, and in that class set the SQL Database execution strategy, which in EF is another term for retry policy.
- In the DAL folder, add a class file named SchoolConfiguration.cs.
Replace the template code with the following code:
[!code-csharpMain]
1: using System.Data.Entity;
2: using System.Data.Entity.SqlServer;
3:
4: namespace ContosoUniversity.DAL
5: {
6: public class SchoolConfiguration : DbConfiguration
7: {
8: public SchoolConfiguration()
9: {
10: SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy());
11: }
12: }
13: }
DbConfiguration
. You can use theDbConfiguration
class to do configuration tasks in code that you would otherwise do in the Web.config file. For more information, see EntityFramework Code-Based Configuration.In StudentController.cs, add a
[!code-csharpMain]using
statement forSystem.Data.Entity.Infrastructure
.1: using System.Data.Entity.Infrastructure;
Change all of the
catch
blocks that catchDataException
exceptions so that they catchRetryLimitExceededException
exceptions instead. For example:[!code-csharpMain]
1: catch (RetryLimitExceededException /* dex */)
2: {
3: //Log the error (uncomment dex variable name and add a line here to write a log.
4: ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
5: }
You were using
DataException
to try to identify errors that might be transient in order to give a friendly “try again” message. But now that you’ve turned on a retry policy, the only errors likely to be transient will already have been tried and failed several times and the actual exception returned will be wrapped in theRetryLimitExceededException
exception.
For more information, see Entity Framework Connection Resiliency / Retry Logic.
Enable Command Interception
Now that you’ve turned on a retry policy, how do you test to verify that it is working as expected? It’s not so easy to force a transient error to happen, especially when you’re running locally, and it would be especially difficult to integrate actual transient errors into an automated unit test. To test the connection resiliency feature, you need a way to intercept queries that Entity Framework sends to SQL Server and replace the SQL Server response with an exception type that is typically transient.
You can also use query interception in order to implement a best practice for cloud applications: log the latency and success or failure of all calls to external services such as database services. EF6 provides a dedicated logging API that can make it easier to do logging, but in this section of the tutorial you’ll learn how to use the Entity Framework’s interception feature directly, both for logging and for simulating transient errors.
Create a logging interface and class
A best practice for logging is to do it by using an interface rather than hard-coding calls to System.Diagnostics.Trace or a logging class. That makes it easier to change your logging mechanism later if you ever need to do that. So in this section you’ll create the logging interface and a class to implement it./p>
- Create a folder in the project and name it Logging.
In the Logging folder, create a class file named ILogger.cs, and replace the template code with the following code:
[!code-csharpMain]
1: using System;
2:
3: namespace ContosoUniversity.Logging
4: {
5: public interface ILogger
6: {
7: void Information(string message);
8: void Information(string fmt, params object[] vars);
9: void Information(Exception exception, string fmt, params object[] vars);
10:
11: void Warning(string message);
12: void Warning(string fmt, params object[] vars);
13: void Warning(Exception exception, string fmt, params object[] vars);
14:
15: void Error(string message);
16: void Error(string fmt, params object[] vars);
17: void Error(Exception exception, string fmt, params object[] vars);
18:
19: void TraceApi(string componentName, string method, TimeSpan timespan);
20: void TraceApi(string componentName, string method, TimeSpan timespan, string properties);
21: void TraceApi(string componentName, string method, TimeSpan timespan, string fmt, params object[] vars);
22: }
23: }
The interface provides three tracing levels to indicate the relative importance of logs, and one designed to provide latency information for external service calls such as database queries. The logging methods have overloads that let you pass in an exception. This is so that exception information including stack trace and inner exceptions is reliably logged by the class that implements the interface, instead of relying on that being done in each logging method call throughout the application.
The TraceApi methods enable you to track the latency of each call to an external service such as SQL Database.In the Logging folder, create a class file named Logger.cs, and replace the template code with the following code:
[!code-csharpMain]
1: using System;
2: using System.Diagnostics;
3: using System.Text;
4:
5: namespace ContosoUniversity.Logging
6: {
7: public class Logger : ILogger
8: {
9: public void Information(string message)
10: {
11: Trace.TraceInformation(message);
12: }
13:
14: public void Information(string fmt, params object[] vars)
15: {
16: Trace.TraceInformation(fmt, vars);
17: }
18:
19: public void Information(Exception exception, string fmt, params object[] vars)
20: {
21: Trace.TraceInformation(FormatExceptionMessage(exception, fmt, vars));
22: }
23:
24: public void Warning(string message)
25: {
26: Trace.TraceWarning(message);
27: }
28:
29: public void Warning(string fmt, params object[] vars)
30: {
31: Trace.TraceWarning(fmt, vars);
32: }
33:
34: public void Warning(Exception exception, string fmt, params object[] vars)
35: {
36: Trace.TraceWarning(FormatExceptionMessage(exception, fmt, vars));
37: }
38:
39: public void Error(string message)
40: {
41: Trace.TraceError(message);
42: }
43:
44: public void Error(string fmt, params object[] vars)
45: {
46: Trace.TraceError(fmt, vars);
47: }
48:
49: public void Error(Exception exception, string fmt, params object[] vars)
50: {
51: Trace.TraceError(FormatExceptionMessage(exception, fmt, vars));
52: }
53:
54: public void TraceApi(string componentName, string method, TimeSpan timespan)
55: {
56: TraceApi(componentName, method, timespan, "");
57: }
58:
59: public void TraceApi(string componentName, string method, TimeSpan timespan, string fmt, params object[] vars)
60: {
61: TraceApi(componentName, method, timespan, string.Format(fmt, vars));
62: }
63: public void TraceApi(string componentName, string method, TimeSpan timespan, string properties)
64: {
65: string message = String.Concat("Component:", componentName, ";Method:", method, ";Timespan:", timespan.ToString(), ";Properties:", properties);
66: Trace.TraceInformation(message);
67: }
68:
69: private static string FormatExceptionMessage(Exception exception, string fmt, object[] vars)
70: {
71: // Simple exception formatting: for a more comprehensive version see
72: // http://code.msdn.microsoft.com/windowsazure/Fix-It-app-for-Building-cdd80df4
73: var sb = new StringBuilder();
74: sb.Append(string.Format(fmt, vars));
75: sb.Append(" Exception: ");
76: sb.Append(exception.ToString());
77: return sb.ToString();
78: }
79: }
80: }
The implementation uses System.Diagnostics to do the tracing. This is a built-in feature of .NET which makes it easy to generate and use tracing information. There are many “listeners” you can use with System.Diagnostics tracing, to write logs to files, for example, or to write them to blob storage in Azure. See some of the options, and links to other resources for more information, in Troubleshooting Azure Web Sites in Visual Studio. For this tutorial you’ll only look at logs in the Visual Studio Output window.
In a production application you might want to consider tracing packages other than System.Diagnostics, and the ILogger interface makes it relatively easy to switch to a different tracing mechanism if you decide to do that.
Create interceptor classes
Next you’ll create the classes that the Entity Framework will call into every time it is going to send a query to the database, one to simulate transient errors and one to do logging. These interceptor classes must derive from the DbCommandInterceptor
class. In them you write method overrides that are automatically called when query is about to be executed. In these methods you can examine or log the query that is being sent to the database, and you can change the query before it’s sent to the database or return something to Entity Framework yourself without even passing the query to the database.
To create the interceptor class that will log every SQL query that is sent to the database, create a class file named SchoolInterceptorLogging.cs in the DAL folder, and replace the template code with the following code:
[!code-csharpMain]
1: using System;
2: using System.Data.Common;
3: using System.Data.Entity;
4: using System.Data.Entity.Infrastructure.Interception;
5: using System.Data.Entity.SqlServer;
6: using System.Data.SqlClient;
7: using System.Diagnostics;
8: using System.Reflection;
9: using System.Linq;
10: using ContosoUniversity.Logging;
11:
12: namespace ContosoUniversity.DAL
13: {
14: public class SchoolInterceptorLogging : DbCommandInterceptor
15: {
16: private ILogger _logger = new Logger();
17: private readonly Stopwatch _stopwatch = new Stopwatch();
18:
19: public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
20: {
21: base.ScalarExecuting(command, interceptionContext);
22: _stopwatch.Restart();
23: }
24:
25: public override void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
26: {
27: _stopwatch.Stop();
28: if (interceptionContext.Exception != null)
29: {
30: _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText);
31: }
32: else
33: {
34: _logger.TraceApi("SQL Database", "SchoolInterceptor.ScalarExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText);
35: }
36: base.ScalarExecuted(command, interceptionContext);
37: }
38:
39: public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
40: {
41: base.NonQueryExecuting(command, interceptionContext);
42: _stopwatch.Restart();
43: }
44:
45: public override void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
46: {
47: _stopwatch.Stop();
48: if (interceptionContext.Exception != null)
49: {
50: _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText);
51: }
52: else
53: {
54: _logger.TraceApi("SQL Database", "SchoolInterceptor.NonQueryExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText);
55: }
56: base.NonQueryExecuted(command, interceptionContext);
57: }
58:
59: public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
60: {
61: base.ReaderExecuting(command, interceptionContext);
62: _stopwatch.Restart();
63: }
64: public override void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
65: {
66: _stopwatch.Stop();
67: if (interceptionContext.Exception != null)
68: {
69: _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText);
70: }
71: else
72: {
73: _logger.TraceApi("SQL Database", "SchoolInterceptor.ReaderExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText);
74: }
75: base.ReaderExecuted(command, interceptionContext);
76: }
77: }
78: }
To create the interceptor class that will generate dummy transient errors when you enter “Throw” in the Search box, create a class file named SchoolInterceptorTransientErrors.cs in the DAL folder, and replace the template code with the following code:
[!code-csharpMain]
1: using System;
2: using System.Data.Common;
3: using System.Data.Entity;
4: using System.Data.Entity.Infrastructure.Interception;
5: using System.Data.Entity.SqlServer;
6: using System.Data.SqlClient;
7: using System.Diagnostics;
8: using System.Reflection;
9: using System.Linq;
10: using ContosoUniversity.Logging;
11:
12: namespace ContosoUniversity.DAL
13: {
14: public class SchoolInterceptorTransientErrors : DbCommandInterceptor
15: {
16: private int _counter = 0;
17: private ILogger _logger = new Logger();
18:
19: public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
20: {
21: bool throwTransientErrors = false;
22: if (command.Parameters.Count > 0 && command.Parameters[0].Value.ToString() == "%Throw%")
23: {
24: throwTransientErrors = true;
25: command.Parameters[0].Value = "%an%";
26: command.Parameters[1].Value = "%an%";
27: }
28:
29: if (throwTransientErrors && _counter < 4)
30: {
31: _logger.Information("Returning transient error for command: {0}", command.CommandText);
32: _counter++;
33: interceptionContext.Exception = CreateDummySqlException();
34: }
35: }
36:
37: private SqlException CreateDummySqlException()
38: {
39: // The instance of SQL Server you attempted to connect to does not support encryption
40: var sqlErrorNumber = 20;
41:
42: var sqlErrorCtor = typeof(SqlError).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Where(c => c.GetParameters().Count() == 7).Single();
43: var sqlError = sqlErrorCtor.Invoke(new object[] { sqlErrorNumber, (byte)0, (byte)0, "", "", "", 1 });
44:
45: var errorCollection = Activator.CreateInstance(typeof(SqlErrorCollection), true);
46: var addMethod = typeof(SqlErrorCollection).GetMethod("Add", BindingFlags.Instance | BindingFlags.NonPublic);
47: addMethod.Invoke(errorCollection, new[] { sqlError });
48:
49: var sqlExceptionCtor = typeof(SqlException).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Where(c => c.GetParameters().Count() == 4).Single();
50: var sqlException = (SqlException)sqlExceptionCtor.Invoke(new object[] { "Dummy", errorCollection, null, Guid.NewGuid() });
51:
52: return sqlException;
53: }
54: }
55: }
This code only overrides the
ReaderExecuting
method, which is called for queries that can return multiple rows of data. If you wanted to check connection resiliency for other types of queries, you could also override theNonQueryExecuting
andScalarExecuting
methods, as the logging interceptor does.When you run the Student page and enter “Throw” as the search string, this code creates a dummy SQL Database exception for error number 20, a type known to be typically transient. Other error numbers currently recognized as transient are 64, 233, 10053, 10054, 10060, 10928, 10929, 40197, 40501, and 40613, but these are subject to change in new versions of SQL Database.
The code returns the exception to Entity Framework instead of running the query and passing back query results. The transient exception is returned four times, and then the code reverts to the normal procedure of passing the query to the database.
Because everything is logged, you’ll be able to see that Entity Framework tries to execute the query four times before finally succeeding, and the only difference in the application is that it takes longer to render a page with query results.
The number of times the Entity Framework will retry is configurable; the code specifies four times because that’s the default value for the SQL Database execution policy. If you change the execution policy, you’d also change the code here that specifies how many times transient errors are generated. You could also change the code to generate more exceptions so that Entity Framework will throw the
RetryLimitExceededException
exception.The value you enter in the Search box will be in
This is just a convenient way to test connection resiliency based on changing some input to the application UI. You can also write code that generates transient errors for all queries or updates, as explained later in the comments about the DbInterception.Add method.command.Parameters[0]
andcommand.Parameters[1]
(one is used for the first name and one for the last name). When the value “%Throw%” is found, “Throw” is replaced in those parameters by “an” so that some students will be found and returned.In Global.asax, add the following
[!code-csharpMain]using
statements:1: using ContosoUniversity.DAL;
2: using System.Data.Entity.Infrastructure.Interception;
Add the highlighted lines to the
Application_Start
method:[!code-csharpMain]
1: protected void Application_Start()
2: {
3: AreaRegistration.RegisterAllAreas();
4: FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
5: RouteConfig.RegisterRoutes(RouteTable.Routes);
6: BundleConfig.RegisterBundles(BundleTable.Bundles);
7: DbInterception.Add(new SchoolInterceptorTransientErrors());
8: DbInterception.Add(new SchoolInterceptorLogging());
9: }
These lines of code are what causes your interceptor code to be run when Entity Framework sends queries to the database. Notice that because you created separate interceptor classes for transient error simulation and logging, you can independently enable and disable them.
You can add interceptors using the
DbInterception.Add
method anywhere in your code; it doesn’t have to be in theApplication_Start
method. Another option is to put this code in the DbConfiguration class that you created earlier to configure the execution policy.[!code-csharpMain]
1: public class SchoolConfiguration : DbConfiguration
2: {
3: public SchoolConfiguration()
4: {
5: SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy());
6: DbInterception.Add(new SchoolInterceptorTransientErrors());
7: DbInterception.Add(new SchoolInterceptorLogging());
8: }
9: }
Wherever you put this code, be careful not to execute
DbInterception.Add
for the same interceptor more than once, or you’ll get additional interceptor instances. For example, if you add the logging interceptor twice, you’ll see two logs for every SQL query.Interceptors are executed in the order of registration (the order in which the
DbInterception.Add
method is called). The order might matter depending on what you’re doing in the interceptor. For example, an interceptor might change the SQL command that it gets in theCommandText
property. If it does change the SQL command, the next interceptor will get the changed SQL command, not the original SQL command.You’ve written the transient error simulation code in a way that lets you cause transient errors by entering a different value in the UI. As an alternative, you could write the interceptor code to always generate the sequence of transient exceptions without checking for a particular parameter value. You could then add the interceptor only when you want to generate transient errors. If you do this, however, don’t add the interceptor until after database initialization has completed. In other words, do at least one database operation such as a query on one of your entity sets before you start generating transient errors. The Entity Framework executes several queries during database initialization, and they aren’t executed in a transaction, so errors during initialization could cause the context to get into an inconsistent state.
Test logging and connection resiliency
- Press F5 to run the application in debug mode, and then click the Students tab.
Look at the Visual Studio Output window to see the tracing output. You might have to scroll up past some JavaScript errors to get to the logs written by your logger.
Notice that you can see the actual SQL queries sent to the database. You see some initial queries and commands that Entity Framework does to get started, checking the database version and migration history table (you’ll learn about migrations in the next tutorial). And you see a query for paging, to find out how many students there are, and finally you see the query that gets the student data.
In the Students page, enter “Throw” as the search string, and click Search.
You’ll notice that the browser seems to hang for several seconds while Entity Framework is retrying the query several times. The first retry happens very quickly, then the wait before increases before each additional retry. This process of waiting longer before each retry is called exponential backoff.
When the page displays, showing students who have “an” in their names, look at the output window, and you’ll see that the same query was attempted five times, the first four times returning transient exceptions. For each transient error you’ll see the log that you write when generating the transient error in the
SchoolInterceptorTransientErrors
class (“Returning transient error for command…”) and you’ll see the log written whenSchoolInterceptorLogging
gets the exception.Since you entered a search string, the query that returns student data is parameterized:
[!code-sqlMain]
1: SELECT TOP (3)
2: [Project1].[ID] AS [ID],
3: [Project1].[LastName] AS [LastName],
4: [Project1].[FirstMidName] AS [FirstMidName],
5: [Project1].[EnrollmentDate] AS [EnrollmentDate]
6: FROM ( SELECT [Project1].[ID] AS [ID], [Project1].[LastName] AS [LastName], [Project1].[FirstMidName] AS [FirstMidName], [Project1].[EnrollmentDate] AS [EnrollmentDate], row_number() OVER (ORDER BY [Project1].[LastName] ASC) AS [row_number]
7: FROM ( SELECT
8: [Extent1].[ID] AS [ID],
9: [Extent1].[LastName] AS [LastName],
10: [Extent1].[FirstMidName] AS [FirstMidName],
11: [Extent1].[EnrollmentDate] AS [EnrollmentDate]
12: FROM [dbo].[Student] AS [Extent1]
13: WHERE ([Extent1].[LastName] LIKE @p__linq__0 ESCAPE N'~') OR ([Extent1].[FirstMidName] LIKE @p__linq__1 ESCAPE N'~')
14: ) AS [Project1]
15: ) AS [Project1]
16: WHERE [Project1].[row_number] > 0
17: ORDER BY [Project1].[LastName] ASC:
You’re not logging the value of the parameters, but you could do that. If you want to see the parameter values, you can write logging code to get parameter values from the
Note that you can’t repeat this test unless you stop the application and restart it. If you wanted to be able to test connection resiliency multiple times in a single run of the application, you could write code to reset the error counter inParameters
property of theDbCommand
object that you get in the interceptor methods.SchoolInterceptorTransientErrors
.To see the difference the execution strategy (retry policy) makes, comment out the
SetExecutionStrategy
line in SchoolConfiguration.cs, run the Students page in debug mode again, and search for “Throw” again.This time the debugger stops on the first generated exception immediately when it tries to execute the query the first time.
Uncomment the SetExecutionStrategy line in SchoolConfiguration.cs.
Summary
In this tutorial you’ve seen how to enable connection resiliency and log SQL commands that Entity Framework composes and sends to the database. In the next tutorial you’ll deploy the application to the Internet, using Code First Migrations to deploy the database.
Please leave feedback on how you liked this tutorial and what we could improve. You can also request new topics at Show Me How With Code.
Links to other Entity Framework resources can be found in ASP.NET Data Access - Recommended Resources.
|