/// <summary> /// Logs all name/value pairs as a single collection using concurrent MERGE statements. /// </summary> /// <param name="conn"></param> /// <param name="exCollectionID"></param> /// <param name="coll"></param> /// <returns></returns> async Task LogCollection(SqlConnectionContext conn, SHA1Hash exCollectionID, IDictionary<string, string> coll) { // The exCollectionID should be pre-calculated by `CalcCollectionID`. // Check if the exCollectionID exists already: int? collectionCount = await conn.ExecReader( @"SELECT COUNT(exCollectionID) FROM [dbo].[exCollectionKeyValue] WHERE [exCollectionID] = @exCollectionID", prms => prms.AddInParamSHA1("@exCollectionID", exCollectionID), async dr => await dr.ReadAsync() ? dr.GetInt32(0) : (int?)null ); // Don't bother logging name-value pairs if the collection already exists: if (!collectionCount.HasValue) return; if (collectionCount.Value == coll.Count) return; const int numTasksPerPair = 2; // Create an array of tasks to wait upon: var tasks = new Task[coll.Count * numTasksPerPair]; // Fill out the array of tasks with concurrent MERGE statements for each name/value pair: using (var en = coll.Keys.GetEnumerator()) for (int i = 0; en.MoveNext(); ++i) { string name = en.Current; string value = coll[name]; var exCollectionValueID = Hash.SHA1(value); // Merge the Value record: tasks[i * numTasksPerPair + 0] = conn.ExecNonQuery( @"MERGE [dbo].[exCollectionValue] WITH (HOLDLOCK) AS target USING (SELECT @exCollectionValueID) AS source (exCollectionValueID) ON (target.exCollectionValueID = source.exCollectionValueID) WHEN NOT MATCHED THEN INSERT ([exCollectionValueID], [Value]) VALUES (@exCollectionValueID, @Value );", prms => prms.AddInParamSHA1("@exCollectionValueID", exCollectionValueID) .AddInParamSize("@Value", SqlDbType.VarChar, -1, value) ); // Merge the Name-Value record: tasks[i * numTasksPerPair + 1] = conn.ExecNonQuery( @"MERGE [dbo].[exCollectionKeyValue] WITH (HOLDLOCK) AS target USING (SELECT @exCollectionID, @Name, @exCollectionValueID) AS source (exCollectionID, Name, exCollectionValueID) ON (target.exCollectionID = source.exCollectionID AND target.Name = source.Name AND target.exCollectionValueID = source.exCollectionValueID) WHEN NOT MATCHED THEN INSERT ([exCollectionID], [Name], [exCollectionValueID]) VALUES (@exCollectionID, @Name, @exCollectionValueID );", prms => prms.AddInParamSHA1("@exCollectionID", exCollectionID) .AddInParamSize("@Name", SqlDbType.VarChar, 96, name) .AddInParamSHA1("@exCollectionValueID", exCollectionValueID) ); } // Our final task's completion depends on all the tasks created thus far: await Task.WhenAll(tasks); }
async Task<HashedLogIdentifier> LogExceptionRecursively(SqlConnectionContext conn, ExceptionWithCapturedContext ctx, int? parentInstanceID = null) { // Create the exTargetSite if it does not exist: var ts = ctx.TargetSite; SHA1Hash? exTargetSiteID = null; Task tskTargetSite = null; if (ts != null) { exTargetSiteID = ts.TargetSiteID; tskTargetSite = conn.ExecNonQuery( @"MERGE [dbo].[exTargetSite] WITH (HOLDLOCK) AS target USING (SELECT @exTargetSiteID) AS source (exTargetSiteID) ON (target.exTargetSiteID = source.exTargetSiteID) WHEN NOT MATCHED THEN INSERT ([exTargetSiteID], [AssemblyName], [TypeName], [MethodName], [ILOffset], [FileName], [FileLineNumber], [FileColumnNumber]) VALUES (@exTargetSiteID, @AssemblyName, @TypeName , @MethodName , @ILOffset , @FileName , @FileLineNumber , @FileColumnNumber );", prms => prms.AddInParamSHA1("@exTargetSiteID", exTargetSiteID.GetValueOrDefault()) .AddInParamSize("@AssemblyName", SqlDbType.NVarChar, 256, ts.AssemblyName) .AddInParamSize("@TypeName", SqlDbType.NVarChar, 256, ts.TypeName) .AddInParamSize("@MethodName", SqlDbType.NVarChar, 256, ts.MethodName) .AddInParam("@ILOffset", SqlDbType.Int, ts.ILOffset) .AddInParamSize("@FileName", SqlDbType.NVarChar, 256, ts.FileName) .AddInParam("@FileLineNumber", SqlDbType.Int, ts.FileLineNumber) .AddInParam("@FileColumnNumber", SqlDbType.Int, ts.FileColumnNumber) ); } SHA1Hash? userStateCollectionID = null; if (ctx.UserState != null) { userStateCollectionID = CalcCollectionID(ctx.UserState); } // Create the exException record if it does not exist: var tskGetPolicy = conn.ExecReader( @"MERGE [dbo].[exException] WITH (HOLDLOCK) AS target USING (SELECT @exExceptionID) AS source (exExceptionID) ON (target.exExceptionID = source.exExceptionID) WHEN NOT MATCHED THEN INSERT ([exExceptionID], [AssemblyName], [TypeName], [StackTrace], [exTargetSiteID]) VALUES (@exExceptionID, @AssemblyName, @TypeName, @StackTrace, @exTargetSiteID ); SELECT excpol.[LogWebContext], excpol.[LogWebRequestHeaders] FROM [dbo].[exExceptionPolicy] excpol WITH (NOLOCK) WHERE excpol.[exExceptionID] = @exExceptionID;", prms => prms.AddInParamSHA1("@exExceptionID", ctx.ExceptionID) .AddInParamSize("@AssemblyName", SqlDbType.NVarChar, 256, ctx.AssemblyName) .AddInParamSize("@TypeName", SqlDbType.NVarChar, 256, ctx.TypeName) .AddInParamSize("@StackTrace", SqlDbType.NVarChar, -1, ctx.StackTrace) .AddInParamSHA1("@exTargetSiteID", exTargetSiteID), // Read the SELECT result set into an ExceptionPolicy, or use the default policy: async dr => !await dr.ReadAsync() ? ExceptionPolicy.Default : new ExceptionPolicy( logWebContext: dr.GetBoolean(dr.GetOrdinal("LogWebContext")), logWebRequestHeaders: dr.GetBoolean(dr.GetOrdinal("LogWebRequestHeaders")) ) ); // Create the exException record if it does not exist: var exApplicationID = CalcApplicationID(cfg); var tskApplication = conn.ExecNonQuery( @"MERGE [dbo].[exApplication] WITH (HOLDLOCK) AS target USING (SELECT @exApplicationID) AS source (exApplicationID) ON (target.exApplicationID = source.exApplicationID) WHEN NOT MATCHED THEN INSERT ([exApplicationID], [MachineName], [ApplicationName], [EnvironmentName], [ProcessPath]) VALUES (@exApplicationID, @MachineName, @ApplicationName, @EnvironmentName, @ProcessPath );", prms => prms.AddInParamSHA1("@exApplicationID", exApplicationID) .AddInParamSize("@MachineName", SqlDbType.VarChar, 64, cfg.MachineName) .AddInParamSize("@ApplicationName", SqlDbType.VarChar, 96, cfg.ApplicationName) .AddInParamSize("@EnvironmentName", SqlDbType.VarChar, 32, cfg.EnvironmentName) .AddInParamSize("@ProcessPath", SqlDbType.NVarChar, 256, cfg.ProcessPath) ); // Create the instance record: var tskInstance = conn.ExecNonQuery( @"INSERT INTO [dbo].[exInstance] ([exExceptionID], [exApplicationID], [LoggedTimeUTC], [SequenceNumber], [IsHandled], [ApplicationIdentity], [ParentInstanceID], [CorrelationID], [ManagedThreadId], [UserStateCollectionID], [Message]) VALUES (@exExceptionID, @exApplicationID, @LoggedTimeUTC, @SequenceNumber, @IsHandled, @ApplicationIdentity, @ParentInstanceID, @CorrelationID, @ManagedThreadId, @UserStateCollectionID, @Message ); SET @exInstanceID = SCOPE_IDENTITY();", prms => prms.AddOutParam("@exInstanceID", SqlDbType.Int) .AddInParamSHA1("@exExceptionID", ctx.ExceptionID) .AddInParamSHA1("@exApplicationID", exApplicationID) .AddInParam("@LoggedTimeUTC", SqlDbType.DateTime2, ctx.LoggedTimeUTC) .AddInParam("@SequenceNumber", SqlDbType.Int, ctx.SequenceNumber) .AddInParam("@IsHandled", SqlDbType.Bit, ctx.IsHandled) .AddInParamSize("@ApplicationIdentity", SqlDbType.NVarChar, 128, cfg.ApplicationIdentity) .AddInParam("@ParentInstanceID", SqlDbType.Int, parentInstanceID) .AddInParam("@CorrelationID", SqlDbType.UniqueIdentifier, ctx.CorrelationID) .AddInParam("@ManagedThreadId", SqlDbType.Int, ctx.ManagedThreadID) .AddInParamSHA1("@UserStateCollectionID", userStateCollectionID) .AddInParamSize("@Message", SqlDbType.NVarChar, 256, ctx.Exception.Message), (prms, rc) => { return (int)prms["@exInstanceID"].Value; } ); // Await the exInstance record creation: int exInstanceID = await tskInstance; // Await the exception policy result: var policy = await tskGetPolicy; // If logging the web context is enabled and we have a web context to work with, log it: Task tskLoggingWeb = null; if (policy.LogWebContext && ctx.CapturedHttpContext != null) { tskLoggingWeb = LogWebContext(conn, policy, ctx, exInstanceID); } // Log the UserState collection: if (userStateCollectionID != null) await LogCollection(conn, userStateCollectionID.Value, ctx.UserState); // Wait for any outstanding logging tasks: if (tskLoggingWeb != null) await tskLoggingWeb; if (tskTargetSite != null) await tskTargetSite; await tskApplication; // Recursively log inner exceptions: var inner = ctx.InnerException; if (inner != null) // Return the inner-most exInstanceID because you can easily drill down through the ParentInstanceID columns to reach the root level. return await LogExceptionRecursively(conn, inner, exInstanceID); return new HashedLogIdentifier(ctx.ExceptionID, exInstanceID); }