连接弹性和命令拦截的 ASP.NET MVC 应用程序中的实体框架

本文涉及的产品
云数据库 RDS SQL Server,基础系列 2核4GB
日志服务 SLS,月写入数据量 50GB 1个月
简介: 最近悟出来一个道理,在这儿分享给大家:学历代表你的过去,能力代表你的现在,学习代表你的将来。   十年河东十年河西,莫欺少年穷   学无止境,精益求精   上篇博客我们学习了EF 之 MVC 排序,查询,分页 Sorting, Filtering, and Paging For MVC ...

   最近悟出来一个道理,在这儿分享给大家:学历代表你的过去,能力代表你的现在,学习代表你的将来。

   十年河东十年河西,莫欺少年穷

   学无止境,精益求精

   上篇博客我们学习了EF 之 MVC 排序,查询,分页 Sorting, Filtering, and Paging For MVC About EF,本节继续学习

   标题中的:连接弹性(微软解释:瞬态错误自动重试连接)和命令拦截(捕捉所有 SQL 查询发送到数据库,以便登录或改变它们)

   上网查了大量的资料,网友们基本都是直接翻译原文:Connection Resiliency and Command Interception with the Entity Framework in an ASP.NET MVC Application

    在解释连接弹性之前,我们来看一段代码:

        /// <summary>
        /// 释放数据库资源 断开连接
        /// </summary>
        /// <param name="disposing"></param>
        protected override void Dispose(bool disposing)
        {
            db.Dispose();
            base.Dispose(disposing);
        }

   上述代码意思是SQL操作执行后,及时断开数据库连接,释放数据库资源

   SQL操作的过程:SQL操作-->执行时发生异常-->执行Dispose-->断开连接,释放资源。在本次操作中,程序和数据库连接了一次,因为发生异常,及时释放了数据库资源,这样的执行过程看似没问题,但是用户体验不太好。

   如果SQL本身没有什么问题,由于断开了数据库连接,用户得不到数据结果,岂不是用户体验差吗?

   我们再来看看微软的解读:连接弹性(微软解释:瞬态错误自动重试连接的次数)

   微软的意思是,在执行一个SQL的过程中,如果第一次执行错误,还可以通过代码控制来实现重连,进行第二次数据库连接,同理,如果第二次数据连接依然发生异常,还会执行第三次数据库连接等等,而在数据库访问策略中,这样的重试连接默认是四次。

   回到刚才的话题:如果SQL语句本身没有什么问题,SQL第一次执行失败,那么第二次就可能成功,这样就提高了用户体验。

   在此:举一些例子,例如SQL执行过程中突然断网,访问的资源临时被占用等导致的执行失败都是可以尝试重连的。

   OK,关于连接弹性的说明就到这儿,下面我们探讨下命令拦截,首先看微软的解释<捕捉所有 SQL 查询发送到数据库,以便登录或改变它们>

   看完微软的解释,相信你和我一样也是丈二的和尚,摸不着头脑。而本人的理解是这样的,当然,我的理解也可能不对,希望大家在评论区指出,谢谢。

   我的理解如下:

   EF代码很少使用SQL语句,在我们写EF时,基本都用Linq To Sql代替了,而我们访问数据库的最基本单元就是SQL语句,那么你书写的linq To Sql 会转化成什么样的SQL语句呢?如果我们能看到这些SQL语句,我们就可以根据这些SQL语句做一些改变,从而提高程序的效率。

   例如:下面的EF代码语句:

        private StudentContext db = new StudentContext();
        /// <summary>
        /// 简单分页演示
        /// </summary>
        /// <param name="page">页码</param>
        /// <returns></returns>
        public ActionResult Index2(int page = 1)//查询所有学生数据
        {
            return View(db.Students.OrderBy(item=>item.Id).ToPagedList(page,9));
        }

   上述代码是个简单的分页程序,如果你看不懂,请参照我的上篇博客:EF 之 MVC 排序,查询,分页 Sorting, Filtering, and Paging For MVC About EF

   那么上述代码在执行的过程中会生成什么样的SQL语句呢?

   

   在程序运行的输出窗口中,我们可以看到如上输出,其输出的完整SQL如下:

SELECT TOP (9) 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Name] AS [Name], 
    [Extent1].[Sex] AS [Sex], 
    [Extent1].[StudentNum] AS [StudentNum]
    FROM ( SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Sex] AS [Sex], [Extent1].[StudentNum] AS [StudentNum], row_number() OVER (ORDER BY [Extent1].[Id] ASC) AS [row_number]
        FROM [dbo].[Student] AS [Extent1]
    )  AS [Extent1]
    WHERE [Extent1].[row_number] > 0
    ORDER BY [Extent1].[Id] ASC

   那么,我们怎样才能捕捉到这些SQL语句呢?

   在MVC EF 默认的输出窗口中,这些SQL语句是不会输出的,我们需要增加一个‘捕捉器’来捕捉这些SQL语句。

   综上所言,我们就基本了解了连接弹性和命令拦截的概念和基本意思。注:如有个人理解不对的地方,谨防误人子弟,希望大家在评论区指出,小弟拜谢

   那么,我们需要写什么代码来达到连接弹性和命令拦截的功效呢?

   如下<大家也可参考:Connection Resiliency and Command Interception with the Entity Framework in an ASP.NET MVC Application>

   首先:如何启用弹性连接

   在我们的EF项目中创建一个名称为:Configuration 的文件夹,在文件夹中首先添加一个数据库重连类:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.SqlServer;
using System.Linq;
using System.Web;

namespace EF_Test.Configuration
{ public class StudentConfiguration : DbConfiguration { /// <summary> /// 需要引入命名空间:using System.Collections.Generic;和using System.Data.Entity.SqlServer; /// </summary> public StudentConfiguration() { //设置 SQL 数据库执行策略 默认重连四次 SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy()); } } }

   上文中提到,如果不是SQL本身的异常,我们重新连接数据库,可能会得到我们想要的结果。例如查询数据时,突然断网,第一次查询失败,在数据库重连后,第二次查询成功,系统将查询结果反馈给客户,提高了客户体验。

   但是,如果您写的SQL本身就是错误的,那无论重连几次数据都将是无用之功,这时,我们可以通过如下代码来捕获SQL执行异常:

   在控制器代码中引用:using System.Data.Entity.Infrastructure;

            try
            {
                //有异常的SQL操作,SQL语句本身异常
            }
            catch (RetryLimitExceededException /* dex */)
            {
                //Log the error (uncomment dex variable name and add a line here to write a log.
                ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
            }

   至此:数据库弹性连接的启用就完成了,下面我们继续命令拦截:

   如何启用命令拦截:

   首先在项目中创建文件夹:ILogger

   1、创建日志接口和类:在日志记录文件夹中,创建一个名为ILogger.cs的类文件︰

    public interface ILogger
    {
        void Information(string message);
        void Information(string fmt, params object[] vars);
        void Information(Exception exception, string fmt, params object[] vars);

        void Warning(string message);
        void Warning(string fmt, params object[] vars);
        void Warning(Exception exception, string fmt, params object[] vars);

        void Error(string message);
        void Error(string fmt, params object[] vars);
        void Error(Exception exception, string fmt, params object[] vars);

        void TraceApi(string componentName, string method, TimeSpan timespan);
        void TraceApi(string componentName, string method, TimeSpan timespan, string properties);
        void TraceApi(string componentName, string method, TimeSpan timespan, string fmt, params object[] vars);

    }

   2、在日志记录文件夹中,创建一个名为Logger.cs的类文件︰

    public class Logger : ILogger
    {

        public void Information(string message)
        {
            Trace.TraceInformation(message);
        }

        public void Information(string fmt, params object[] vars)
        {
            Trace.TraceInformation(fmt, vars);
        }

        public void Information(Exception exception, string fmt, params object[] vars)
        {
            Trace.TraceInformation(FormatExceptionMessage(exception, fmt, vars));
        }

        public void Warning(string message)
        {
            Trace.TraceWarning(message);
        }

        public void Warning(string fmt, params object[] vars)
        {
            Trace.TraceWarning(fmt, vars);
        }

        public void Warning(Exception exception, string fmt, params object[] vars)
        {
            Trace.TraceWarning(FormatExceptionMessage(exception, fmt, vars));
        }

        public void Error(string message)
        {
            Trace.TraceError(message);
        }

        public void Error(string fmt, params object[] vars)
        {
            Trace.TraceError(fmt, vars);
        }

        public void Error(Exception exception, string fmt, params object[] vars)
        {
            Trace.TraceError(FormatExceptionMessage(exception, fmt, vars));
        }

        public void TraceApi(string componentName, string method, TimeSpan timespan)
        {
            TraceApi(componentName, method, timespan, "");
        }

        public void TraceApi(string componentName, string method, TimeSpan timespan, string fmt, params object[] vars)
        {
            TraceApi(componentName, method, timespan, string.Format(fmt, vars));
        }
        public void TraceApi(string componentName, string method, TimeSpan timespan, string properties)
        {
            string message = String.Concat("Component:", componentName, ";Method:", method, ";Timespan:", timespan.ToString(), ";Properties:", properties);
            Trace.TraceInformation(message);
        }

        private static string FormatExceptionMessage(Exception exception, string fmt, object[] vars)
        {
            // Simple exception formatting: for a more comprehensive version see 
            // http://code.msdn.microsoft.com/windowsazure/Fix-It-app-for-Building-cdd80df4
            var sb = new StringBuilder();
            sb.Append(string.Format(fmt, vars));
            sb.Append(" Exception: ");
            sb.Append(exception.ToString());
            return sb.ToString();
        }
    }

   3、在日志文件夹中创建拦截器类

using System;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Infrastructure.Interception;
using System.Data.Entity.SqlServer;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Reflection;
using System.Linq;

namespace EF_Test.ILogger
{
    public class StudentInterceptorLogging : DbCommandInterceptor
    {
        private ILogger _logger = new Logger();
        private readonly Stopwatch _stopwatch = new Stopwatch();

        public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            base.ScalarExecuting(command, interceptionContext);
            _stopwatch.Restart();
        }

        public override void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            _stopwatch.Stop();
            if (interceptionContext.Exception != null)
            {
                _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText);
            }
            else
            {
                _logger.TraceApi("SQL Database", "SchoolInterceptor.ScalarExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText);
            }
            base.ScalarExecuted(command, interceptionContext);
        }

        public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
            base.NonQueryExecuting(command, interceptionContext);
            _stopwatch.Restart();
        }

        public override void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
            _stopwatch.Stop();
            if (interceptionContext.Exception != null)
            {
                _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText);
            }
            else
            {
                _logger.TraceApi("SQL Database", "SchoolInterceptor.NonQueryExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText);
            }
            base.NonQueryExecuted(command, interceptionContext);
        }

        public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            base.ReaderExecuting(command, interceptionContext);
            _stopwatch.Restart();
        }
        public override void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            _stopwatch.Stop();
            if (interceptionContext.Exception != null)
            {
                _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText);
            }
            else
            {
                _logger.TraceApi("SQL Database", "SchoolInterceptor.ReaderExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText);
            }
            base.ReaderExecuted(command, interceptionContext);
        }
    }
}

   4、创建记录SQL错误的拦截器类

using System;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Infrastructure.Interception;
using System.Data.Entity.SqlServer;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Reflection;
using System.Linq;

namespace EF_Test.ILogger
{
    public class StudentInterceptorTransientErrors : DbCommandInterceptor
    {
        private int _counter = 0;
        private ILogger _logger = new Logger();

        public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            bool throwTransientErrors = false;
            if (command.Parameters.Count > 0 && command.Parameters[0].Value.ToString() == "%Throw%")
            {
                throwTransientErrors = true;
                command.Parameters[0].Value = "%an%";
                command.Parameters[1].Value = "%an%";
            }

            if (throwTransientErrors && _counter < 4)
            {
                _logger.Information("Returning transient error for command: {0}", command.CommandText);
                _counter++;
                interceptionContext.Exception = CreateDummySqlException();
            }
        }

        private SqlException CreateDummySqlException()
        {
            // The instance of SQL Server you attempted to connect to does not support encryption
            var sqlErrorNumber = 20;

            var sqlErrorCtor = typeof(SqlError).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Where(c => c.GetParameters().Count() == 7).Single();
            var sqlError = sqlErrorCtor.Invoke(new object[] { sqlErrorNumber, (byte)0, (byte)0, "", "", "", 1 });

            var errorCollection = Activator.CreateInstance(typeof(SqlErrorCollection), true);
            var addMethod = typeof(SqlErrorCollection).GetMethod("Add", BindingFlags.Instance | BindingFlags.NonPublic);
            addMethod.Invoke(errorCollection, new[] { sqlError });

            var sqlExceptionCtor = typeof(SqlException).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Where(c => c.GetParameters().Count() == 4).Single();
            var sqlException = (SqlException)sqlExceptionCtor.Invoke(new object[] { "Dummy", errorCollection, null, Guid.NewGuid() });

            return sqlException;
        }
    }
}

   至此,整个拦截器就建立完毕。

   如果正确的使拦截器发挥作用呢?我们还需在全局应用文件中添加如下代码:

   代码如下:

        protected void Application_Start()
        {
           // Database.SetInitializer<StudentContext>(new DropCreateDatabaseIfModelChanges<StudentContext>());  

            AreaRegistration.RegisterAllAreas();
            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
            //
            DbInterception.Add(new StudentInterceptorTransientErrors());
            DbInterception.Add(new StudentInterceptorLogging());
        }

   当然,我们如果不想写在全局应用文件中,我们可以在数据库重连策略类中添加,如下:

        public StudentConfiguration()
        {
            //设置 SQL 数据库执行策略 默认重连四次
            SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy());
            //注册拦截器  using System.Data.Entity.Infrastructure.Interception;
            DbInterception.Add(new StudentInterceptorTransientErrors());
            DbInterception.Add(new StudentInterceptorLogging());
        }

 

   下面是我的文件代码目录结构:

   

   运行程序,测试下我们的拦截器及输出的SQL语句:

   

   程序效果图为:

   上述SQL语句其实就是一个简单的分页SQL语句。

   我们输入学号进行查询,看看会输出什么样的SQL语句:

   输出的SQL语句为:

   我们把SQL语句放入数据库中执行,如下:

   至此:本节内容也就讲完了,谢谢!

    @陈卧龙的博客

相关实践学习
通过日志服务实现云资源OSS的安全审计
本实验介绍如何通过日志服务实现云资源OSS的安全审计。
相关文章
|
7月前
|
JSON 数据格式
【Azure Fabric Service】演示使用PowerShell命令部署SF应用程序(.NET)
本文详细介绍了在中国区微软云Azure上使用Service Fabrics服务时,通过PowerShell命令发布.NET应用的全过程。由于Visual Studio 2022无法直接发布应用,需借助PowerShell脚本完成部署。文章分三步讲解:首先在Visual Studio 2022中打包应用部署包,其次连接SF集群并上传部署包,最后注册应用类型、创建实例并启动服务。过程中涉及关键参数如服务器证书指纹和服务端证书指纹的获取,并附带图文说明,便于操作。参考官方文档,帮助用户成功部署并运行服务。
225 73
|
SQL 开发框架 .NET
ASP.NET连接SQL数据库:详细步骤与最佳实践指南ali01n.xinmi1009fan.com
随着Web开发技术的不断进步,ASP.NET已成为一种非常流行的Web应用程序开发框架。在ASP.NET项目中,我们经常需要与数据库进行交互,特别是SQL数据库。本文将详细介绍如何在ASP.NET项目中连接SQL数据库,并提供最佳实践指南以确保开发过程的稳定性和效率。一、准备工作在开始之前,请确保您
706 3
|
10月前
|
监控 前端开发 API
一款基于 .NET MVC 框架开发、功能全面的MES系统
一款基于 .NET MVC 框架开发、功能全面的MES系统
249 5
|
11月前
|
数据库 C# 开发者
ADO.NET连接到南大通用GBase 8s数据库
ADO.NET连接到南大通用GBase 8s数据库
|
开发框架 .NET API
Windows Forms应用程序中集成一个ASP.NET API服务
Windows Forms应用程序中集成一个ASP.NET API服务
225 9
|
12月前
|
SQL XML 关系型数据库
入门指南:利用NHibernate简化.NET应用程序的数据访问
【10月更文挑战第13天】NHibernate是一个面向.NET的开源对象关系映射(ORM)工具,它提供了从数据库表到应用程序中的对象之间的映射。通过使用NHibernate,开发者可以专注于业务逻辑和领域模型的设计,而无需直接编写复杂的SQL语句来处理数据持久化问题。NHibernate支持多种数据库,并且具有高度的灵活性和可扩展性。
259 2
|
11月前
|
数据库连接 数据库 C#
Windows下C# 通过ADO.NET方式连接南大通用GBase 8s数据库(上)
Windows下C# 通过ADO.NET方式连接南大通用GBase 8s数据库(上)
|
11月前
|
数据库连接 数据库 C#
Windows下C# 通过ADO.NET方式连接南大通用GBase 8s数据库(下)
本文接续前文,深入讲解了在Windows环境下使用C#和ADO.NET操作南大通用GBase 8s数据库的方法。通过Visual Studio 2022创建项目,添加GBase 8s的DLL引用,并提供了详细的C#代码示例,涵盖数据库连接、表的创建与修改、数据的增删查改等操作,旨在帮助开发者提高数据库管理效率。
|
开发框架 前端开发 JavaScript
ASP.NET MVC 教程
ASP.NET 是一个使用 HTML、CSS、JavaScript 和服务器脚本创建网页和网站的开发框架。
211 7
|
12月前
|
开发框架 JavaScript 前端开发
一个适用于 ASP.NET Core 的轻量级插件框架
一个适用于 ASP.NET Core 的轻量级插件框架
177 0