다음을 통해 공유


자습서: ASP.NET MVC 앱에서 Entity Framework에서 연결 복원력 및 명령 가로채기 사용

지금까지 애플리케이션은 개발 컴퓨터의 IIS Express 로컬로 실행되었습니다. 다른 사용자가 인터넷을 통해 사용할 수 있는 실제 애플리케이션을 만들려면 웹 호스팅 공급자에 배포해야 하며 데이터베이스 서버에 데이터베이스를 배포해야 합니다.

이 자습서에서는 연결 복원력 및 명령 가로채기를 사용하는 방법을 알아봅니다. 이러한 기능은 클라우드 환경에 배포할 때 특히 유용한 Entity Framework 6의 두 가지 중요한 기능입니다. 연결 복원력(일시적인 오류에 대한 자동 다시 시도) 및 명령 가로채기(로그 또는 변경하기 위해 데이터베이스로 전송된 모든 SQL 쿼리 catch).

이 연결 복원력 및 명령 가로채기 자습서는 선택 사항입니다. 이 자습서를 건너뛰면 후속 자습서에서 몇 가지 사소한 조정이 이루어져야 합니다.

이 자습서에서는 다음을 수행합니다.

  • 연결 복원력 사용
  • 명령 가로채기 사용
  • 새 구성 테스트

사전 요구 사항

연결 복원력 사용

Windows Azure에 애플리케이션을 배포하면 클라우드 데이터베이스 서비스인 Windows Azure SQL Database에 데이터베이스를 배포합니다. 일시적인 연결 오류는 일반적으로 웹 서버와 데이터베이스 서버가 동일한 데이터 센터에서 직접 함께 연결된 경우보다 클라우드 데이터베이스 서비스에 연결할 때 더 자주 발생합니다. 클라우드 웹 서버와 클라우드 데이터베이스 서비스가 동일한 데이터 센터에서 호스트되더라도 부하 분산 장치와 같은 문제가 있을 수 있는 네트워크 연결이 더 많이 있습니다.

또한 클라우드 서비스는 일반적으로 다른 사용자가 공유하므로 응답성이 영향을 받을 수 있습니다. 또한 데이터베이스에 대한 액세스는 제한될 수 있습니다. 제한은 데이터베이스 서비스가 SLA(서비스 수준 계약)에서 허용되는 것보다 더 자주 액세스하려고 할 때 예외를 throw한다는 것을 의미합니다.

클라우드 서비스에 액세스할 때 발생하는 많은 또는 대부분의 연결 문제는 일시적입니다. 즉, 단기간에 resolve. 따라서 데이터베이스 작업을 시도하고 일반적으로 일시적인 오류 유형이 발생하면 잠시 기다린 후 작업을 다시 시도할 수 있으며 작업이 성공할 수 있습니다. 일시적 오류를 자동으로 다시 시도하여 대부분의 오류를 고객에게 보이지 않게 하여 사용자에게 훨씬 더 나은 환경을 제공할 수 있습니다. Entity Framework 6의 연결 복원력 기능은 실패한 SQL 쿼리를 다시 시도하는 프로세스를 자동화합니다.

특정 데이터베이스 서비스에 대해 연결 복원력 기능을 적절하게 구성해야 합니다.

  • 일시적일 가능성이 있는 예외를 알아야 합니다. 예를 들어 프로그램 버그로 인한 오류가 아니라 네트워크 연결의 일시적인 손실로 인한 오류를 다시 시도하려고 합니다.
  • 실패한 작업의 재시도 사이에 적절한 시간을 기다려야 합니다. 사용자가 응답을 기다리는 온라인 웹 페이지의 경우보다 일괄 처리 프로세스에 대한 재시도 사이에 더 오래 기다릴 수 있습니다.
  • 포기하기 전에 적절한 횟수를 다시 시도해야 합니다. 온라인 애플리케이션에서 수행되는 일괄 처리 프로세스에서 더 많은 시간을 다시 시도할 수 있습니다.

Entity Framework 공급자에서 지원하는 모든 데이터베이스 환경에 대해 이러한 설정을 수동으로 구성할 수 있지만 일반적으로 Windows Azure SQL Database를 사용하는 온라인 애플리케이션에 적합한 기본값은 이미 구성되어 있으며 Contoso University 애플리케이션에 대해 구현할 설정입니다.

연결 복원력을 사용하도록 설정하기 위해 해야 할 일은 DbConfiguration 클래스에서 파생되는 클래스를 어셈블리에 만들고 해당 클래스에서 SQL Database 실행 전략을 설정하는 것이며, EF에서는 재시도 정책의 또 다른 용어입니다.

  1. DAL 폴더에서 SchoolConfiguration.cs라는 클래스 파일을 추가합니다.

  2. 템플릿 코드를 다음 코드로 바꿉니다.

    using System.Data.Entity;
    using System.Data.Entity.SqlServer;
    
    namespace ContosoUniversity.DAL
    {
        public class SchoolConfiguration : DbConfiguration
        {
            public SchoolConfiguration()
            {
                SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy());
            }
        }
    }
    

    Entity Framework는 에서 DbConfiguration파생되는 클래스에서 찾은 코드를 자동으로 실행합니다. 클래스를 DbConfiguration 사용하여 코드에서 구성 작업을 수행할 수 있으며, 그렇지 않으면 Web.config 파일에서 수행할 수 있습니다. 자세한 내용은 EntityFramework Code-Based 구성을 참조하세요.

  3. StudentController.cs에서 에 대한 System.Data.Entity.Infrastructure문을 추가 using 합니다.

    using System.Data.Entity.Infrastructure;
    
  4. 대신 예외를 catch catch할 수 있도록 예외를 catch DataExceptionRetryLimitExceededException 하는 모든 블록을 변경합니다. 예:

    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.");
    }
    

    를 사용하여 DataException 친숙한 "다시 시도" 메시지를 제공하기 위해 일시적일 수 있는 오류를 식별하려고 했습니다. 그러나 다시 시도 정책을 설정했으므로 일시적일 가능성이 있는 유일한 오류는 이미 여러 번 시도되고 실패했으며 반환된 실제 예외는 예외에 RetryLimitExceededException 래핑됩니다.

자세한 내용은 Entity Framework 연결 복원력/재시도 논리를 참조하세요.

명령 가로채기 사용

다시 시도 정책을 설정했으므로 예상대로 작동하는지 확인하려면 어떻게 테스트해야 할까요? 특히 로컬에서 실행할 때 일시적인 오류가 발생하도록 강제하는 것은 쉽지 않으며, 실제 일시적 오류를 자동화된 단위 테스트에 통합하는 것은 특히 어려울 수 있습니다. 연결 복원력 기능을 테스트하려면 Entity Framework가 SQL Server 보내는 쿼리를 가로채고 SQL Server 응답을 일반적으로 일시적인 예외 형식으로 바꾸는 방법이 필요합니다.

또한 쿼리 가로채기를 사용하여 클라우드 애플리케이션에 대한 모범 사례를 구현할 수 있습니다. 데이터베이스 서비스와 같은 외부 서비스에 대한 모든 호출의 대기 시간 및 성공 또는 실패를 기록 합니다. EF6은 로깅을 더 쉽게 수행할 수 있는 전용 로깅 API 를 제공하지만 자습서의 이 섹션에서는 로깅 및 일시적인 오류 시뮬레이션을 위해 Entity Framework의 가로채기 기능을 직접 사용하는 방법을 알아봅니다.

로깅 인터페이스 및 클래스 만들기

로깅의 모범 사례는 System.Diagnostics.Trace 또는 로깅 클래스에 대한 호출을 하드 코딩하는 대신 인터페이스를 사용하여 수행하는 것입니다. 이렇게 하면 나중에 로깅 메커니즘을 더 쉽게 변경할 수 있습니다. 따라서 이 섹션에서는 로깅 인터페이스와 이를 구현하는 클래스를 만듭니다./p>

  1. 프로젝트에 폴더를 만들고 이름을 로깅으로 지정합니다.

  2. 로깅 폴더에서 ILogger.cs라는 클래스 파일을 만들고 템플릿 코드를 다음 코드로 바꿉니다.

    using System;
    
    namespace ContosoUniversity.Logging
    {
        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);
        }
    }
    

    인터페이스는 로그의 상대적 중요도를 나타내는 세 가지 추적 수준과 데이터베이스 쿼리와 같은 외부 서비스 호출에 대한 대기 시간 정보를 제공하도록 설계된 추적 수준을 제공합니다. 로깅 메서드에는 예외를 전달할 수 있는 오버로드가 있습니다. 따라서 스택 추적 및 내부 예외를 포함한 예외 정보는 애플리케이션 전체의 각 로깅 메서드 호출에서 수행되는 것에 의존하는 대신 인터페이스를 구현하는 클래스에 의해 안정적으로 기록됩니다.

    TraceApi 메서드를 사용하면 SQL Database 같은 외부 서비스에 대한 각 호출의 대기 시간을 추적할 수 있습니다.

  3. 로깅 폴더에서 Logger.cs라는 클래스 파일을 만들고 템플릿 코드를 다음 코드로 바꿉니다.

    using System;
    using System.Diagnostics;
    using System.Text;
    
    namespace ContosoUniversity.Logging
    {
        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 
                // https://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();
            }
        }
    }
    

    구현에서는 System.Diagnostics를 사용하여 추적을 수행합니다. 추적 정보를 쉽게 생성하고 사용할 수 있는 .NET의 기본 제공 기능입니다. System.Diagnostics 추적과 함께 로그를 파일에 쓰거나 Azure의 Blob Storage에 쓰는 데 사용할 수 있는 많은 "수신기"가 있습니다. 자세한 내용은 Visual Studio에서 Azure 웹 사이트 문제 해결에서 일부 옵션 및 다른 리소스에 대한 링크를 참조하세요. 이 자습서에서는 Visual Studio 출력 창의 로그만 살펴봅니다.

    프로덕션 애플리케이션에서는 System.Diagnostics 이외의 추적 패키지를 고려할 수 있으며, ILogger 인터페이스를 사용하면 다른 추적 메커니즘으로 비교적 쉽게 전환할 수 있습니다.

인터셉터 클래스 만들기

다음으로 엔터티 프레임워크가 데이터베이스에 쿼리를 보낼 때마다 호출할 클래스를 만듭니다. 하나는 일시적인 오류를 시뮬레이션하고 다른 하나는 로깅을 수행합니다. 이러한 인터셉터 클래스는 클래스에서 DbCommandInterceptor 파생되어야 합니다. 쿼리가 실행될 때 자동으로 호출되는 메서드 재정의를 작성합니다. 이러한 메서드에서는 데이터베이스로 전송되는 쿼리를 검사하거나 기록할 수 있으며, 데이터베이스로 전송되기 전에 쿼리를 변경하거나 데이터베이스에 쿼리를 전달하지 않고도 Entity Framework에 직접 반환할 수 있습니다.

  1. 데이터베이스로 전송되는 모든 SQL 쿼리를 기록하는 인터셉터 클래스를 만들려면 DAL 폴더에 SchoolInterceptorLogging.cs라는 클래스 파일을 만들고 템플릿 코드를 다음 코드로 바꿉니다.

    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;
    using ContosoUniversity.Logging;
    
    namespace ContosoUniversity.DAL
    {
        public class SchoolInterceptorLogging : 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);
            }
        }
    }
    

    성공적인 쿼리 또는 명령의 경우 이 코드는 대기 시간 정보가 포함된 정보 로그를 작성합니다. 예외의 경우 오류 로그를 만듭니다.

  2. 검색 상자에 "Throw"를 입력할 때 더미 일시적 오류를 생성하는 인터셉터 클래스를 만들려면 DAL 폴더에 SchoolInterceptorTransientErrors.cs라는 클래스 파일을 만들고 템플릿 코드를 다음 코드로 바꿉니다.

    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;
    using ContosoUniversity.Logging;
    
    namespace ContosoUniversity.DAL
    {
        public class SchoolInterceptorTransientErrors : 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;
            }
        }
    }
    

    이 코드는 여러 데이터 행을 ReaderExecuting 반환할 수 있는 쿼리에 대해 호출되는 메서드만 재정의합니다. 다른 유형의 쿼리에 대한 연결 복원력을 검사 경우 로깅 인터셉터처럼 및 ScalarExecuting 메서드를 재정 NonQueryExecuting 의할 수도 있습니다.

    학생 페이지를 실행하고 검색 문자열로 "Throw"를 입력하면 이 코드는 일반적으로 일시적인 형식인 오류 번호 20에 대한 더미 SQL Database 예외를 만듭니다. 현재 일시적인 것으로 인식되는 기타 오류 번호는 64, 233, 10053, 10054, 10060, 10928, 10929, 40197, 40501 및 40613이지만 새 버전의 SQL Database 변경될 수 있습니다.

    이 코드는 쿼리를 실행하고 쿼리 결과를 다시 전달하는 대신 Entity Framework에 예외를 반환합니다. 일시적인 예외가 네 번 반환된 다음, 코드는 쿼리를 데이터베이스에 전달하는 일반적인 프로시저로 되돌아갑니다.

    모든 항목이 기록되므로 Entity Framework가 마지막으로 성공하기 전에 쿼리를 네 번 실행하려고 시도하는 것을 볼 수 있으며, 애플리케이션의 유일한 차이점은 쿼리 결과가 있는 페이지를 렌더링하는 데 시간이 더 오래 걸린다는 것입니다.

    Entity Framework가 다시 시도할 횟수를 구성할 수 있습니다. 코드는 SQL Database 실행 정책의 기본값이므로 네 번 지정합니다. 실행 정책을 변경하는 경우 일시적인 오류가 생성되는 횟수를 지정하는 코드도 여기에 변경합니다. Entity Framework에서 예외를 throw하도록 코드를 변경하여 더 많은 예외를 RetryLimitExceededException 생성할 수도 있습니다.

    검색 상자에 입력하는 값은 및 에 command.Parameters[0] 있습니다 command.Parameters[1] (하나는 이름에 사용되고 다른 하나는 성에 사용됨). 값 "%Throw%"가 발견되면 해당 매개 변수에서 "Throw"가 "an"으로 대체되어 일부 학생이 찾아서 반환됩니다.

    이는 애플리케이션 UI에 대한 일부 입력을 변경하여 연결 복원력을 테스트하는 편리한 방법입니다. DbInterception.Add 메서드에 대한 주석의 뒷부분에서 설명한 대로 모든 쿼리 또는 업데이트에 대한 일시적인 오류를 생성하는 코드를 작성할 수도 있습니다.

  3. Global.asax에서 다음 using 문을 추가합니다.

    using ContosoUniversity.DAL;
    using System.Data.Entity.Infrastructure.Interception;
    
  4. 강조 표시된 줄을 메서드에 Application_Start 추가합니다.

    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        DbInterception.Add(new SchoolInterceptorTransientErrors());
        DbInterception.Add(new SchoolInterceptorLogging());
    }
    

    이러한 코드 줄은 Entity Framework가 데이터베이스에 쿼리를 보낼 때 인터셉터 코드가 실행되도록 하는 것입니다. 일시적인 오류 시뮬레이션 및 로깅을 위해 별도의 인터셉터 클래스를 만들었으므로 독립적으로 사용하도록 설정하고 사용하지 않도록 설정할 수 있습니다.

    코드의 어디에서나 메서드를 DbInterception.Add 사용하여 인터셉터를 추가할 수 있습니다. 메서드에 Application_Start 있을 필요는 없습니다. 또 다른 옵션은 실행 정책을 구성하기 위해 이전에 만든 DbConfiguration 클래스에 이 코드를 배치하는 것입니다.

    public class SchoolConfiguration : DbConfiguration
    {
        public SchoolConfiguration()
        {
            SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy());
            DbInterception.Add(new SchoolInterceptorTransientErrors());
            DbInterception.Add(new SchoolInterceptorLogging());
        }
    }
    

    이 코드를 넣을 때마다 동일한 인터셉터에 대해 두 번 이상 실행 DbInterception.Add 하지 않도록 주의하거나 추가 인터셉터 인스턴스를 가져옵니다. 예를 들어 로깅 인터셉터를 두 번 추가하면 모든 SQL 쿼리에 대해 두 개의 로그가 표시됩니다.

    인터셉터는 등록 순서(메서드가 호출되는 DbInterception.Add 순서)로 실행됩니다. 순서는 인터셉터에서 수행하는 일에 따라 중요할 수 있습니다. 예를 들어 인터셉터 속성에 가져오는 CommandText SQL 명령을 변경할 수 있습니다. SQL 명령을 변경하면 다음 인터셉터에서 원래 SQL 명령이 아닌 변경된 SQL 명령을 가져옵니다.

    UI에 다른 값을 입력하여 일시적인 오류를 발생시킬 수 있는 방식으로 일시적인 오류 시뮬레이션 코드를 작성했습니다. 또는 특정 매개 변수 값을 확인하지 않고 항상 일시적인 예외 시퀀스를 생성하는 인터셉터 코드를 작성할 수 있습니다. 그런 다음 일시적인 오류를 생성하려는 경우에만 인터셉터를 추가할 수 있습니다. 그러나 이 작업을 수행하는 경우 데이터베이스 초기화가 완료될 때까지 인터셉터를 추가하지 마세요. 즉, 일시적인 오류를 생성하기 전에 엔터티 집합 중 하나에 대한 쿼리와 같은 데이터베이스 작업을 하나 이상 수행합니다. Entity Framework는 데이터베이스 초기화 중에 여러 쿼리를 실행하며 트랜잭션에서 실행되지 않으므로 초기화 중 오류가 발생하면 컨텍스트가 일관되지 않은 상태가 될 수 있습니다.

새 구성 테스트

  1. F5 키를 눌러 디버그 모드에서 애플리케이션을 실행한 다음 학생 탭을 클릭합니다.

  2. Visual Studio 출력 창을 확인하여 추적 출력을 확인합니다. 로거가 작성한 로그에 도달하려면 일부 JavaScript 오류를 지나 위로 스크롤해야 할 수 있습니다.

    데이터베이스로 전송된 실제 SQL 쿼리를 볼 수 있습니다. Entity Framework가 시작하기 위해 수행하는 몇 가지 초기 쿼리 및 명령이 표시되며 데이터베이스 버전 및 마이그레이션 기록 테이블을 확인합니다(다음 자습서에서는 마이그레이션에 대해 알아봅니다). 페이징에 대한 쿼리가 표시되고, 학생 수를 확인할 수 있으며, 마지막으로 학생 데이터를 가져오는 쿼리가 표시됩니다.

    일반 쿼리에 대한 로깅

  3. 학생 페이지에서 검색 문자열로 "Throw"를 입력하고 검색을 클릭합니다.

    검색 문자열 throw

    Entity Framework가 쿼리를 여러 번 다시 시도하는 동안 브라우저가 몇 초 동안 중단되는 것처럼 보입니다. 첫 번째 재시도는 매우 빠르게 수행된 다음, 각 추가 재시도 전에 대기가 증가합니다. 각 재시도를 기하급수적 백오프라고 부르기 전에 더 오래 기다리는 이 프로세스입니다.

    페이지가 표시되면 이름에 "an"이 있는 학생을 표시하고 출력 창을 보면 동일한 쿼리가 5번 시도되었으며 처음 4번은 일시적인 예외를 반환하는 것을 볼 수 있습니다. 각 일시적인 오류에 대해 클래스에서 SchoolInterceptorTransientErrors 일시적 오류를 생성할 때 작성하는 로그("명령에 대한 일시적인 오류 반환...")가 표시되고 예외가 발생하면 기록된 SchoolInterceptorLogging 로그가 표시됩니다.

    재시도를 보여 주는 로깅 출력

    검색 문자열을 입력했기 때문에 학생 데이터를 반환하는 쿼리는 매개 변수화됩니다.

    SELECT TOP (3) 
        [Project1].[ID] AS [ID], 
        [Project1].[LastName] AS [LastName], 
        [Project1].[FirstMidName] AS [FirstMidName], 
        [Project1].[EnrollmentDate] AS [EnrollmentDate]
        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]
            FROM ( SELECT 
                [Extent1].[ID] AS [ID], 
                [Extent1].[LastName] AS [LastName], 
                [Extent1].[FirstMidName] AS [FirstMidName], 
                [Extent1].[EnrollmentDate] AS [EnrollmentDate]
                FROM [dbo].[Student] AS [Extent1]
                WHERE ([Extent1].[LastName] LIKE @p__linq__0 ESCAPE N'~') OR ([Extent1].[FirstMidName] LIKE @p__linq__1 ESCAPE N'~')
            )  AS [Project1]
        )  AS [Project1]
        WHERE [Project1].[row_number] > 0
        ORDER BY [Project1].[LastName] ASC:
    

    매개 변수 값을 로깅하지는 않지만 그렇게 할 수 있습니다. 매개 변수 값을 보려면 로깅 코드를 작성하여 인터셉터 메서드에서 가져오는 개체의 DbCommand 속성에서 Parameters 매개 변수 값을 가져올 수 있습니다.

    애플리케이션을 중지하고 다시 시작하지 않으면 이 테스트를 반복할 수 없습니다. 애플리케이션의 단일 실행에서 연결 복원력을 여러 번 테스트하려면 에서 SchoolInterceptorTransientErrors오류 카운터를 다시 설정하는 코드를 작성할 수 있습니다.

  4. 실행 전략(재시도 정책)의 차이를 확인하려면 SchoolConfiguration.cs에서 줄을 주석으로 처리 SetExecutionStrategy 하고, 디버그 모드에서 학생 페이지를 다시 실행하고, "Throw"를 다시 검색합니다.

    이번에는 처음 쿼리를 실행하려고 할 때 디버거가 처음 생성된 예외에서 즉시 중지됩니다.

    더미 예외

  5. SchoolConfiguration.cs에서 SetExecutionStrategy 줄의 주석 처리를 제거합니다.

코드 가져오기

완료된 프로젝트 다운로드

추가 리소스

다른 Entity Framework 리소스에 대한 링크는 ASP.NET 데이터 액세스 - 권장 리소스에서 찾을 수 있습니다.

다음 단계

이 자습서에서는 다음을 수행합니다.

  • 활성화된 연결 복원력
  • 사용 명령 가로채기
  • 새 구성 테스트

다음 문서로 이동하여 Code First 마이그레이션 및 Azure 배포에 대해 알아봅니다.