Code · 84 lines · 3290 bytes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84using Microsoft.Data.SqlClient;

namespace VanDerHeijden.Logging.Sql;

/// <summary>
/// Writes log entries to a SQL Server table in bulk using SqlBulkCopy.
/// Expected table schema:
///   CREATE TABLE Logs (
///     Id        BIGINT IDENTITY PRIMARY KEY,
///     Timestamp DATETIME2       NOT NULL,
///     Level     NVARCHAR(20)    NOT NULL,
///     Category  NVARCHAR(256)   NOT NULL,
///     Message   NVARCHAR(MAX)   NOT NULL,
///     Exception NVARCHAR(MAX)   NULL,
///     Path      NVARCHAR(1024)  NULL,
///     Method    NVARCHAR(10)    NULL,
///     ClientIp  NVARCHAR(45)    NULL,
///     Referer   NVARCHAR(2048)  NULL,
///     UserAgent NVARCHAR(512)   NULL
///   );
/// </summary>
public sealed class SqlLogWriter(string connectionString, string tableName = "Logs") : IBatchedLogWriter<SqlLogEntry>
{
	/// <summary>
	/// Bulk-inserts all entries into the configured SQL Server table using <see cref="SqlBulkCopy"/>.
	/// </summary>
	/// <param name="entries">The log entries to insert.</param>
	/// <param name="ct">A token that can cancel the operation.</param>
	public async Task WriteBatchAsync(List<SqlLogEntry> entries, CancellationToken ct)
	{
		await using var connection = new SqlConnection(connectionString);
		await connection.OpenAsync(ct);

		using var bulkCopy = new SqlBulkCopy(connection)
		{
			DestinationTableName = tableName,
			BulkCopyTimeout = 30
		};

		bulkCopy.ColumnMappings.Add(nameof(SqlLogEntry.Timestamp), "Timestamp");
		bulkCopy.ColumnMappings.Add(nameof(SqlLogEntry.Level),     "Level");
		bulkCopy.ColumnMappings.Add(nameof(SqlLogEntry.Category),  "Category");
		bulkCopy.ColumnMappings.Add(nameof(SqlLogEntry.Message),   "Message");
		bulkCopy.ColumnMappings.Add(nameof(SqlLogEntry.Exception), "Exception");
		bulkCopy.ColumnMappings.Add(nameof(SqlLogEntry.Path),      "Path");
		bulkCopy.ColumnMappings.Add(nameof(SqlLogEntry.Method),    "Method");
		bulkCopy.ColumnMappings.Add(nameof(SqlLogEntry.ClientIp),  "ClientIp");
		bulkCopy.ColumnMappings.Add(nameof(SqlLogEntry.Referer),   "Referer");
		bulkCopy.ColumnMappings.Add(nameof(SqlLogEntry.UserAgent), "UserAgent");

		var table = ToDataTable(entries);
		await bulkCopy.WriteToServerAsync(table, ct);
	}

	/// <inheritdoc/>
	public ValueTask DisposeAsync() => ValueTask.CompletedTask;

	private static System.Data.DataTable ToDataTable(List<SqlLogEntry> entries)
	{
		var table = new System.Data.DataTable();
		table.Columns.Add("Timestamp", typeof(DateTime));
		table.Columns.Add("Level",     typeof(string));
		table.Columns.Add("Category",  typeof(string));
		table.Columns.Add("Message",   typeof(string));
		table.Columns.Add("Exception", typeof(string));
		table.Columns.Add("Path",      typeof(string));
		table.Columns.Add("Method",    typeof(string));
		table.Columns.Add("ClientIp",  typeof(string));
		table.Columns.Add("Referer",   typeof(string));
		table.Columns.Add("UserAgent", typeof(string));

		foreach (var e in entries)
			table.Rows.Add(
				e.Timestamp, e.Level, e.Category, e.Message,
				(object?)e.Exception ?? DBNull.Value,
				(object?)e.Path      ?? DBNull.Value,
				(object?)e.Method    ?? DBNull.Value,
				(object?)e.ClientIp  ?? DBNull.Value,
				(object?)e.Referer   ?? DBNull.Value,
				(object?)e.UserAgent ?? DBNull.Value);

		return table;
	}
}