diff --git a/Serilog.Ui.sln.DotSettings b/Serilog.Ui.sln.DotSettings
index 4d35b8be..e62a5d60 100644
--- a/Serilog.Ui.sln.DotSettings
+++ b/Serilog.Ui.sln.DotSettings
@@ -1,2 +1,3 @@
- True
\ No newline at end of file
+ True
+ True
\ No newline at end of file
diff --git a/src/Serilog.Ui.Core/QueryBuilder/Sql/SinkColumnNames.cs b/src/Serilog.Ui.Core/QueryBuilder/Sql/SinkColumnNames.cs
new file mode 100644
index 00000000..786c1d1a
--- /dev/null
+++ b/src/Serilog.Ui.Core/QueryBuilder/Sql/SinkColumnNames.cs
@@ -0,0 +1,37 @@
+namespace Serilog.Ui.Core.QueryBuilder.Sql;
+
+///
+/// Represents the column names used in the SQL-based sink for logging.
+///
+public abstract class SinkColumnNames
+{
+ ///
+ /// Gets or sets the message of the log entry.
+ ///
+ public string Message { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the message template of the log entry.
+ ///
+ public string MessageTemplate { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the level of the log entry.
+ ///
+ public string Level { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the timestamp of the log entry.
+ ///
+ public string Timestamp { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the exception of the log entry.
+ ///
+ public string Exception { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the serialized log event like properties.
+ ///
+ public string LogEventSerialized { get; set; } = string.Empty;
+}
\ No newline at end of file
diff --git a/src/Serilog.Ui.Core/QueryBuilder/Sql/SqlQueryBuilder.cs b/src/Serilog.Ui.Core/QueryBuilder/Sql/SqlQueryBuilder.cs
new file mode 100644
index 00000000..b9b112b3
--- /dev/null
+++ b/src/Serilog.Ui.Core/QueryBuilder/Sql/SqlQueryBuilder.cs
@@ -0,0 +1,67 @@
+using Serilog.Ui.Core.Attributes;
+using Serilog.Ui.Core.Models;
+using System.Reflection;
+using static Serilog.Ui.Core.Models.SearchOptions;
+
+namespace Serilog.Ui.Core.QueryBuilder.Sql;
+
+///
+/// Abstract class that provides methods to build SQL queries for fetching and counting logs.
+///
+public abstract class SqlQueryBuilder where TModel : LogModel
+{
+ ///
+ /// Builds a SQL query to fetch logs from the specified table.
+ ///
+ /// The column names used in the sink for logging.
+ /// The schema of the table.
+ /// The name of the table.
+ /// The query parameters for fetching logs.
+ /// A SQL query string to fetch logs.
+ public abstract string BuildFetchLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query);
+
+ ///
+ /// Builds a SQL query to count logs in the specified table.
+ ///
+ /// The column names used in the sink for logging.
+ /// The schema of the table.
+ /// The name of the table.
+ /// The query parameters for counting logs.
+ /// A SQL query string to count logs.
+ public abstract string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query);
+
+ ///
+ /// Generates a SQL sort clause based on the specified sort property and direction.
+ ///
+ /// The column names used in the sink for logging.
+ /// The property to sort on.
+ /// The direction to sort by.
+ /// A SQL sort clause string.
+ protected abstract string GenerateSortClause(SinkColumnNames columns, SortProperty sortOn, SortDirection sortBy);
+
+ ///
+ /// Generates a SQL sort clause based on the specified sort property and direction.
+ ///
+ /// The column names used in the sink for logging.
+ /// The property to sort on.
+ /// A SQL sort clause string.
+ protected static string GetSortColumnName(SinkColumnNames columns, SortProperty sortOn) => sortOn switch
+ {
+ SortProperty.Timestamp => columns.Timestamp,
+ SortProperty.Level => columns.Level,
+ SortProperty.Message => columns.Message,
+ _ => columns.Timestamp
+ };
+
+ ///
+ /// Determines whether to add the exception column to the WHERE clause based on the presence of the RemovedColumnAttribute.
+ ///
+ /// True if the exception column should be added to the WHERE clause; otherwise, false.
+ protected static bool AddExceptionToWhereClause()
+ {
+ PropertyInfo? exceptionProperty = typeof(TModel).GetProperty("Exception");
+ RemovedColumnAttribute? att = exceptionProperty?.GetCustomAttribute();
+
+ return att is null;
+ }
+}
\ No newline at end of file
diff --git a/src/Serilog.Ui.Core/Serilog.Ui.Core.csproj b/src/Serilog.Ui.Core/Serilog.Ui.Core.csproj
index cf49d99e..dbba811c 100644
--- a/src/Serilog.Ui.Core/Serilog.Ui.Core.csproj
+++ b/src/Serilog.Ui.Core/Serilog.Ui.Core.csproj
@@ -9,6 +9,6 @@
-
+
\ No newline at end of file
diff --git a/src/Serilog.Ui.MsSqlServerProvider/Extensions/SerilogUiOptionBuilderExtensions.cs b/src/Serilog.Ui.MsSqlServerProvider/Extensions/SerilogUiOptionBuilderExtensions.cs
index a1d67b01..30f36029 100644
--- a/src/Serilog.Ui.MsSqlServerProvider/Extensions/SerilogUiOptionBuilderExtensions.cs
+++ b/src/Serilog.Ui.MsSqlServerProvider/Extensions/SerilogUiOptionBuilderExtensions.cs
@@ -1,64 +1,64 @@
-using System;
-using Dapper;
+using Dapper;
using Microsoft.Extensions.DependencyInjection;
using Serilog.Ui.Core;
using Serilog.Ui.Core.Interfaces;
using Serilog.Ui.Core.Models.Options;
+using System;
-namespace Serilog.Ui.MsSqlServerProvider.Extensions
-{
- ///
- /// SQL Server data provider specific extension methods for .
- ///
- public static class SerilogUiOptionBuilderExtensions
- {
- /// Configures the SerilogUi to connect to a SQL Server database.
- /// The options builder.
- /// The Ms Sql options action.
- ///
- /// Delegate to customize the DateTime parsing.
- /// It throws if the return DateTime isn't UTC kind.
- ///
- public static ISerilogUiOptionsBuilder UseSqlServer(
- this ISerilogUiOptionsBuilder optionsBuilder,
- Action setupOptions,
- Func? dateTimeCustomParsing = null
- ) => optionsBuilder.UseSqlServer(setupOptions, dateTimeCustomParsing);
-
- /// Configures the SerilogUi to connect to a SQL Server database.
- /// The log model, containing any additional columns. It must inherit .
- /// The options builder.
- /// The Ms Sql options action.
- ///
- /// Delegate to customize the DateTime parsing.
- /// It throws if the return DateTime isn't UTC kind.
- ///
- public static ISerilogUiOptionsBuilder UseSqlServer(
- this ISerilogUiOptionsBuilder optionsBuilder,
- Action setupOptions,
- Func? dateTimeCustomParsing = null
- ) where T : SqlServerLogModel
- {
- var dbOptions = new RelationalDbOptions("dbo");
- setupOptions(dbOptions);
- dbOptions.Validate();
-
- var providerName = dbOptions.GetProviderName(SqlServerDataProvider.MsSqlProviderName);
+namespace Serilog.Ui.MsSqlServerProvider.Extensions;
- optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName);
- SqlMapper.AddTypeHandler(new DapperDateTimeHandler(dateTimeCustomParsing));
+///
+/// SQL Server data provider specific extension methods for .
+///
+public static class SerilogUiOptionBuilderExtensions
+{
+ /// Configures the SerilogUi to connect to a SQL Server database.
+ /// The options builder.
+ /// The Ms Sql options action.
+ ///
+ /// Delegate to customize the DateTime parsing.
+ /// It throws if the return DateTime isn't UTC kind.
+ ///
+ public static ISerilogUiOptionsBuilder UseSqlServer(
+ this ISerilogUiOptionsBuilder optionsBuilder,
+ Action setupOptions,
+ Func? dateTimeCustomParsing = null
+ ) => optionsBuilder.UseSqlServer(setupOptions, dateTimeCustomParsing);
- var customModel = typeof(T) != typeof(SqlServerLogModel);
- if (customModel)
- {
- optionsBuilder.RegisterColumnsInfo(providerName);
- optionsBuilder.Services.AddScoped(_ => new SqlServerDataProvider(dbOptions));
+ /// Configures the SerilogUi to connect to a SQL Server database.
+ /// The log model, containing any additional columns. It must inherit .
+ /// The options builder.
+ /// The Ms Sql options action.
+ ///
+ /// Delegate to customize the DateTime parsing.
+ /// It throws if the return DateTime isn't UTC kind.
+ ///
+ public static ISerilogUiOptionsBuilder UseSqlServer(
+ this ISerilogUiOptionsBuilder optionsBuilder,
+ Action setupOptions,
+ Func? dateTimeCustomParsing = null
+ ) where T : SqlServerLogModel
+ {
+ SqlServerDbOptions dbOptions = new("dbo");
+ setupOptions(dbOptions);
+ dbOptions.Validate();
- return optionsBuilder;
- }
+ string providerName = dbOptions.GetProviderName(SqlServerDataProvider.MsSqlProviderName);
+ optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName);
+ SqlMapper.AddTypeHandler(new DapperDateTimeHandler(dateTimeCustomParsing));
- optionsBuilder.Services.AddScoped(_ => new SqlServerDataProvider(dbOptions));
- return optionsBuilder;
+ bool customModel = typeof(T) != typeof(SqlServerLogModel);
+ if (customModel)
+ {
+ optionsBuilder.RegisterColumnsInfo(providerName);
+ optionsBuilder.Services.AddScoped(_ => new SqlServerDataProvider(dbOptions, new SqlServerQueryBuilder()));
}
+ else
+ {
+ optionsBuilder.Services.AddScoped(_ =>
+ new SqlServerDataProvider(dbOptions, new SqlServerQueryBuilder()));
+ }
+
+ return optionsBuilder;
}
}
\ No newline at end of file
diff --git a/src/Serilog.Ui.MsSqlServerProvider/Extensions/SqlServerDbOptions.cs b/src/Serilog.Ui.MsSqlServerProvider/Extensions/SqlServerDbOptions.cs
new file mode 100644
index 00000000..2e221ec4
--- /dev/null
+++ b/src/Serilog.Ui.MsSqlServerProvider/Extensions/SqlServerDbOptions.cs
@@ -0,0 +1,10 @@
+using Serilog.Ui.Core.Models.Options;
+using Serilog.Ui.Core.QueryBuilder.Sql;
+using Serilog.Ui.MsSqlServerProvider.Models;
+
+namespace Serilog.Ui.MsSqlServerProvider.Extensions;
+
+public class SqlServerDbOptions(string defaultSchemaName) : RelationalDbOptions(defaultSchemaName)
+{
+ public SinkColumnNames ColumnNames { get; } = new SqlServerSinkColumnNames();
+}
\ No newline at end of file
diff --git a/src/Serilog.Ui.MsSqlServerProvider/SqlServerLogModel.cs b/src/Serilog.Ui.MsSqlServerProvider/Models/SqlServerLogModel.cs
similarity index 100%
rename from src/Serilog.Ui.MsSqlServerProvider/SqlServerLogModel.cs
rename to src/Serilog.Ui.MsSqlServerProvider/Models/SqlServerLogModel.cs
diff --git a/src/Serilog.Ui.MsSqlServerProvider/Models/SqlServerSinkColumnNames.cs b/src/Serilog.Ui.MsSqlServerProvider/Models/SqlServerSinkColumnNames.cs
new file mode 100644
index 00000000..3806f527
--- /dev/null
+++ b/src/Serilog.Ui.MsSqlServerProvider/Models/SqlServerSinkColumnNames.cs
@@ -0,0 +1,16 @@
+using Serilog.Ui.Core.QueryBuilder.Sql;
+
+namespace Serilog.Ui.MsSqlServerProvider.Models;
+
+internal class SqlServerSinkColumnNames : SinkColumnNames
+{
+ public SqlServerSinkColumnNames()
+ {
+ Exception = "Exception";
+ Level = "Level";
+ LogEventSerialized = "Properties";
+ Message = "Message";
+ MessageTemplate = "";
+ Timestamp = "TimeStamp";
+ }
+}
\ No newline at end of file
diff --git a/src/Serilog.Ui.MsSqlServerProvider/Serilog.Ui.MsSqlServerProvider.csproj b/src/Serilog.Ui.MsSqlServerProvider/Serilog.Ui.MsSqlServerProvider.csproj
index ff8363bd..c01d0388 100644
--- a/src/Serilog.Ui.MsSqlServerProvider/Serilog.Ui.MsSqlServerProvider.csproj
+++ b/src/Serilog.Ui.MsSqlServerProvider/Serilog.Ui.MsSqlServerProvider.csproj
@@ -5,7 +5,7 @@
netstandard2.0
latest
- 3.0.0
+ 3.1.0
Microsoft SQL Server data provider for Serilog UI.
serilog serilog-ui serilog.sinks.mssqlserver mssqlserver
@@ -18,5 +18,6 @@
+
\ No newline at end of file
diff --git a/src/Serilog.Ui.MsSqlServerProvider/SqlServerDataProvider.cs b/src/Serilog.Ui.MsSqlServerProvider/SqlServerDataProvider.cs
index 15100abe..93b57651 100644
--- a/src/Serilog.Ui.MsSqlServerProvider/SqlServerDataProvider.cs
+++ b/src/Serilog.Ui.MsSqlServerProvider/SqlServerDataProvider.cs
@@ -1,168 +1,76 @@
-using System.Collections.Generic;
+using Dapper;
+using Microsoft.Data.SqlClient;
+using Serilog.Ui.Core;
+using Serilog.Ui.Core.Models;
+using Serilog.Ui.MsSqlServerProvider.Extensions;
+using System.Collections.Generic;
using System.Data;
using System.Linq;
-using System.Reflection;
-using System.Text;
using System.Threading;
using System.Threading.Tasks;
-using Ardalis.GuardClauses;
-using Dapper;
-using Microsoft.Data.SqlClient;
-using Serilog.Ui.Core;
-using Serilog.Ui.Core.Attributes;
-using Serilog.Ui.Core.Models;
-using Serilog.Ui.Core.Models.Options;
-using static Serilog.Ui.Core.Models.SearchOptions;
-
-namespace Serilog.Ui.MsSqlServerProvider
-{
- public class SqlServerDataProvider(RelationalDbOptions options) : SqlServerDataProvider(options)
- {
- protected override string SearchCriteriaWhereQuery() => "OR [Exception] LIKE @Search";
-
- protected override string SelectQuery()
- {
- const string level = $"[{ColumnLevelName}]";
- const string message = $"[{ColumnMessageName}]";
- const string timestamp = $"[{ColumnTimestampName}]";
-
- return $"SELECT [Id], {message}, {level}, {timestamp}, [Exception], [Properties] ";
- }
- }
-
- public class SqlServerDataProvider(RelationalDbOptions options) : IDataProvider
- where T : SqlServerLogModel
- {
- internal const string MsSqlProviderName = "MsSQL";
-
- private protected const string ColumnTimestampName = "TimeStamp";
-
- private protected const string ColumnLevelName = "Level";
-
- private protected const string ColumnMessageName = "Message";
-
- private readonly RelationalDbOptions _options = Guard.Against.Null(options);
-
- public string Name => _options.GetProviderName(MsSqlProviderName);
-
- protected virtual string SelectQuery() => "SELECT * ";
- public async Task<(IEnumerable, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default)
- {
- // since sink stores dates in local time, we query by local time
- queryParams.ToLocalDates();
+namespace Serilog.Ui.MsSqlServerProvider;
- var logsTask = GetLogsAsync(queryParams);
- var logCountTask = CountLogsAsync(queryParams);
+///
+public class SqlServerDataProvider(SqlServerDbOptions options, SqlServerQueryBuilder queryBuilder)
+ : SqlServerDataProvider(options, queryBuilder);
- await Task.WhenAll(logsTask, logCountTask);
-
- return (await logsTask, await logCountTask);
- }
-
- private async Task> GetLogsAsync(FetchLogsQuery queryParams)
- {
- var queryBuilder = new StringBuilder();
-
- queryBuilder.Append(SelectQuery());
- queryBuilder.Append($"FROM [{_options.Schema}].[{_options.TableName}] ");
-
- GenerateWhereClause(queryBuilder, queryParams);
-
- GenerateSortClause(queryBuilder, queryParams.SortOn, queryParams.SortBy);
-
- queryBuilder.Append("OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY");
-
- var rowNoStart = queryParams.Page * queryParams.Count;
-
- using IDbConnection connection = new SqlConnection(_options.ConnectionString);
- var logs = await connection.QueryAsync(queryBuilder.ToString(),
- new
- {
- Offset = rowNoStart,
- queryParams.Count,
- queryParams.Level,
- Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null,
- queryParams.StartDate,
- queryParams.EndDate
- });
-
- return logs.Select((item, i) => item.SetRowNo(rowNoStart, i)).ToList();
- }
-
- private async Task CountLogsAsync(FetchLogsQuery queryParams)
- {
- var queryBuilder = new StringBuilder();
- queryBuilder.Append($"SELECT COUNT(Id) FROM [{_options.Schema}].[{_options.TableName}]");
+///
+public class SqlServerDataProvider(SqlServerDbOptions options, SqlServerQueryBuilder queryBuilder) : IDataProvider
+ where T : SqlServerLogModel
+{
+ internal const string MsSqlProviderName = "MsSQL";
- GenerateWhereClause(queryBuilder, queryParams);
+ ///
+ public string Name => options.GetProviderName(MsSqlProviderName);
- using IDbConnection connection = new SqlConnection(_options.ConnectionString);
- return await connection.ExecuteScalarAsync(queryBuilder.ToString(),
- new
- {
- queryParams.Level,
- Search = queryParams.SearchCriteria != null ? "%" + queryParams.SearchCriteria + "%" : null,
- queryParams.StartDate,
- queryParams.EndDate
- });
- }
+ public async Task<(IEnumerable, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default)
+ {
+ // since sink stores dates in local time, we query by local time
+ queryParams.ToLocalDates();
- ///
- /// If Exception property is flagged with ,
- /// it removes the Where query part on the Exception field.
- ///
- ///
- protected virtual string SearchCriteriaWhereQuery()
- {
- var exceptionProperty = typeof(T).GetProperty(nameof(SqlServerLogModel.Exception));
- var att = exceptionProperty?.GetCustomAttribute();
- return att is null ? "OR [Exception] LIKE @Search" : string.Empty;
- }
+ var logsTask = GetLogsAsync(queryParams);
+ var logCountTask = CountLogsAsync(queryParams);
- private void GenerateWhereClause(StringBuilder queryBuilder, FetchLogsQuery queryParams)
- {
- var conditionStart = "WHERE";
+ await Task.WhenAll(logsTask, logCountTask);
- if (!string.IsNullOrWhiteSpace(queryParams.Level))
- {
- queryBuilder.Append($"{conditionStart} [{ColumnLevelName}] = @Level ");
- conditionStart = "AND";
- }
+ return (await logsTask, await logCountTask);
+ }
- if (!string.IsNullOrWhiteSpace(queryParams.SearchCriteria))
- {
- queryBuilder.Append($"{conditionStart} [{ColumnMessageName}] LIKE @Search {SearchCriteriaWhereQuery()} ");
- conditionStart = "AND";
- }
+ private async Task> GetLogsAsync(FetchLogsQuery queryParams)
+ {
+ string query = queryBuilder.BuildFetchLogsQuery(options.ColumnNames, options.Schema, options.TableName, queryParams);
+ int rowNoStart = queryParams.Page * queryParams.Count;
- if (queryParams.StartDate != null)
- {
- queryBuilder.Append($"{conditionStart} [{ColumnTimestampName}] >= @StartDate ");
- conditionStart = "AND";
- }
+ using IDbConnection connection = new SqlConnection(options.ConnectionString);
- if (queryParams.EndDate != null)
+ IEnumerable logs = await connection.QueryAsync(query,
+ new
{
- queryBuilder.Append($"{conditionStart} [{ColumnTimestampName}] <= @EndDate ");
- }
- }
+ Offset = rowNoStart,
+ queryParams.Count,
+ queryParams.Level,
+ Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null,
+ queryParams.StartDate,
+ queryParams.EndDate
+ });
+
+ return logs.Select((item, i) => item.SetRowNo(rowNoStart, i)).ToList();
+ }
- private static void GenerateSortClause(StringBuilder queryBuilder, SortProperty sortOn, SortDirection sortBy)
- {
- var sortOnCol = GetColumnName(sortOn);
- var sortByCol = sortBy.ToString().ToUpper();
+ private async Task CountLogsAsync(FetchLogsQuery queryParams)
+ {
+ string query = queryBuilder.BuildCountLogsQuery(options.ColumnNames, options.Schema, options.TableName, queryParams);
- queryBuilder.Append($"ORDER BY [{sortOnCol}] {sortByCol} ");
- }
+ using IDbConnection connection = new SqlConnection(options.ConnectionString);
- private static string GetColumnName(SortProperty sortOn)
- => sortOn switch
+ return await connection.ExecuteScalarAsync(query,
+ new
{
- SortProperty.Level => ColumnLevelName,
- SortProperty.Message => ColumnMessageName,
- SortProperty.Timestamp => ColumnTimestampName,
- _ => ColumnTimestampName
- };
+ queryParams.Level,
+ Search = queryParams.SearchCriteria != null ? "%" + queryParams.SearchCriteria + "%" : null,
+ queryParams.StartDate,
+ queryParams.EndDate
+ });
}
}
\ No newline at end of file
diff --git a/src/Serilog.Ui.MsSqlServerProvider/SqlServerQueryBuilder.cs b/src/Serilog.Ui.MsSqlServerProvider/SqlServerQueryBuilder.cs
new file mode 100644
index 00000000..1bf038d5
--- /dev/null
+++ b/src/Serilog.Ui.MsSqlServerProvider/SqlServerQueryBuilder.cs
@@ -0,0 +1,126 @@
+using Serilog.Ui.Core.Models;
+using Serilog.Ui.Core.QueryBuilder.Sql;
+using System;
+using System.Text;
+
+namespace Serilog.Ui.MsSqlServerProvider;
+
+///
+/// Provides methods to build SQL queries specifically for SQL Server to fetch and count logs.
+///
+/// The type of the log model.
+public class SqlServerQueryBuilder : SqlQueryBuilder where TModel : LogModel
+{
+ ///
+ public override string BuildFetchLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query)
+ {
+ StringBuilder queryStr = new();
+
+ GenerateSelectClause(queryStr, columns, schema, tableName);
+
+ GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate);
+
+ queryStr.Append($"{GenerateSortClause(columns, query.SortOn, query.SortBy)} OFFSET @Offset ROWS FETCH NEXT @Count ROWS ONLY");
+
+ return queryStr.ToString();
+ }
+
+ ///
+ /// Builds a SQL query to count logs in the specified table.
+ ///
+ /// The column names used in the sink for logging.
+ /// The schema of the table.
+ /// The name of the table.
+ /// The query parameters for counting logs.
+ /// A SQL query string to count logs.
+ public override string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query)
+ {
+ StringBuilder queryStr = new();
+
+ queryStr.Append("SELECT COUNT([Id]) ")
+ .Append($"FROM [{schema}].[{tableName}] ");
+
+ GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate);
+
+ return queryStr.ToString();
+ }
+
+ protected override string GenerateSortClause(SinkColumnNames columns, SearchOptions.SortProperty sortOn, SearchOptions.SortDirection sortBy)
+ => $"ORDER BY [{GetSortColumnName(columns, sortOn)}] {sortBy.ToString().ToUpper()}";
+
+ ///
+ /// Generates the SELECT clause for the SQL query.
+ ///
+ /// The StringBuilder to append the SELECT clause to.
+ /// The column names used in the sink for logging.
+ /// The schema of the table.
+ /// The name of the table.
+ private static void GenerateSelectClause(StringBuilder queryBuilder, SinkColumnNames columns, string schema, string tableName)
+ {
+ if (typeof(TModel) != typeof(SqlServerLogModel))
+ {
+ queryBuilder.Append("SELECT * ");
+ }
+ else
+ {
+ queryBuilder.Append("SELECT [Id], ")
+ .Append($"[{columns.Message}], ")
+ .Append($"[{columns.Level}], ")
+ .Append($"[{columns.Timestamp}], ")
+ .Append($"[{columns.Exception}], ")
+ .Append($"[{columns.LogEventSerialized}] ");
+ }
+
+ queryBuilder.Append($"FROM [{schema}].[{tableName}] ");
+ }
+
+ ///
+ /// Generates the WHERE clause for the SQL query.
+ ///
+ /// The StringBuilder to append the WHERE clause to.
+ /// The column names used in the sink for logging.
+ /// The log level to filter by.
+ /// The search criteria to filter by.
+ /// The start date to filter by.
+ /// The end date to filter by.
+ private static void GenerateWhereClause(
+ StringBuilder queryBuilder,
+ SinkColumnNames columns,
+ string? level,
+ string? searchCriteria,
+ DateTime? startDate,
+ DateTime? endDate)
+ {
+ StringBuilder conditions2 = new();
+
+ if (!string.IsNullOrWhiteSpace(level))
+ {
+ conditions2.Append($"AND [{columns.Level}] = @Level ");
+ }
+
+ if (!string.IsNullOrWhiteSpace(searchCriteria))
+ {
+ conditions2.Append($"AND ([{columns.Message}] LIKE @Search ");
+ conditions2.Append(AddExceptionToWhereClause() ? $"OR [{columns.Exception}] LIKE @Search) " : ") ");
+ }
+
+ if (startDate.HasValue)
+ {
+ conditions2.Append($"AND [{columns.Timestamp}] >= @StartDate ");
+ }
+
+ if (endDate.HasValue)
+ {
+ conditions2.Append($"AND [{columns.Timestamp}] <= @EndDate ");
+ }
+
+ if (conditions2.Length <= 0)
+ {
+ return;
+ }
+
+ queryBuilder
+ .Append("WHERE 1 = 1 ")
+ .Append(conditions2);
+ }
+}
\ No newline at end of file
diff --git a/src/Serilog.Ui.MySqlProvider/Extensions/MariaDbOptions.cs b/src/Serilog.Ui.MySqlProvider/Extensions/MariaDbOptions.cs
new file mode 100644
index 00000000..1f3be6c0
--- /dev/null
+++ b/src/Serilog.Ui.MySqlProvider/Extensions/MariaDbOptions.cs
@@ -0,0 +1,11 @@
+using Serilog.Ui.MySqlProvider.Models;
+
+namespace Serilog.Ui.MySqlProvider.Extensions;
+
+public class MariaDbOptions : MySqlDbOptions
+{
+ public MariaDbOptions(string defaultSchemaName) : base(defaultSchemaName)
+ {
+ ColumnNames = new MariaDbSinkColumnNames();
+ }
+}
\ No newline at end of file
diff --git a/src/Serilog.Ui.MySqlProvider/Extensions/MySqlDbOptions.cs b/src/Serilog.Ui.MySqlProvider/Extensions/MySqlDbOptions.cs
new file mode 100644
index 00000000..f1e07950
--- /dev/null
+++ b/src/Serilog.Ui.MySqlProvider/Extensions/MySqlDbOptions.cs
@@ -0,0 +1,10 @@
+using Serilog.Ui.Core.Models.Options;
+using Serilog.Ui.Core.QueryBuilder.Sql;
+using Serilog.Ui.MySqlProvider.Models;
+
+namespace Serilog.Ui.MySqlProvider.Extensions;
+
+public class MySqlDbOptions(string defaultSchemaName) : RelationalDbOptions(defaultSchemaName)
+{
+ internal SinkColumnNames ColumnNames { get; set; } = new MySqlSinkColumnNames();
+}
\ No newline at end of file
diff --git a/src/Serilog.Ui.MySqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs b/src/Serilog.Ui.MySqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs
index 16673cc5..1348591f 100644
--- a/src/Serilog.Ui.MySqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs
+++ b/src/Serilog.Ui.MySqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs
@@ -1,84 +1,77 @@
-using System;
-using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection;
using Serilog.Ui.Core;
using Serilog.Ui.Core.Interfaces;
using Serilog.Ui.Core.Models.Options;
+using System;
-namespace Serilog.Ui.MySqlProvider.Extensions
+namespace Serilog.Ui.MySqlProvider.Extensions;
+
+///
+/// MySQL data provider specific extension methods for .
+///
+public static class SerilogUiOptionBuilderExtensions
{
///
- /// MySQL data provider specific extension methods for .
+ /// Configures the SerilogUi to connect to a MySQL/MariaDb database expecting
+ /// Serilog.Sinks.MySQL. defaults.
+ /// Provider expects sink to store timestamp in utc.
///
- public static class SerilogUiOptionBuilderExtensions
+ /// The options builder.
+ /// The MySql options action.
+ public static ISerilogUiOptionsBuilder UseMySqlServer(this ISerilogUiOptionsBuilder optionsBuilder, Action setupOptions)
{
- ///
- /// Configures the SerilogUi to connect to a MySQL/MariaDb database expecting
- /// Serilog.Sinks.MySQL. defaults.
- /// Provider expects sink to store timestamp in utc.
- ///
- /// The options builder.
- /// The MySql options action.
- public static ISerilogUiOptionsBuilder UseMySqlServer(
- this ISerilogUiOptionsBuilder optionsBuilder,
- Action setupOptions
- )
- {
- var dbOptions = new RelationalDbOptions("dbo");
- setupOptions(dbOptions);
- dbOptions.Validate();
-
- var providerName = dbOptions.GetProviderName(MySqlDataProvider.MySqlProviderName);
-
- optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName);
+ MySqlDbOptions dbOptions = new("dbo");
+ setupOptions(dbOptions);
+ dbOptions.Validate();
- optionsBuilder.Services.AddScoped(_ => new MySqlDataProvider(dbOptions));
+ string providerName = dbOptions.GetProviderName(MySqlDataProvider.MySqlProviderName);
- return optionsBuilder;
- }
+ optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName);
+ optionsBuilder.Services.AddScoped(_ => new MySqlDataProvider(dbOptions, new MySqlQueryBuilder()));
- ///
- /// Configures the SerilogUi to connect to a MySQL/MariaDb database expecting
- /// Serilog.Sinks.MariaDB. defaults.
- /// Provider expects sink to store timestamp in utc.
- ///
- /// The options builder.
- /// The MySql options action.
- public static ISerilogUiOptionsBuilder UseMariaDbServer(
- this ISerilogUiOptionsBuilder optionsBuilder,
- Action setupOptions
- ) => optionsBuilder.UseMariaDbServer(setupOptions);
+ return optionsBuilder;
+ }
- ///
- /// Configures the SerilogUi to connect to a MySQL/MariaDb database expecting
- /// Serilog.Sinks.MariaDB. defaults.
- /// Provider expects sink to store timestamp in utc.
- ///
- /// The log model, containing any additional columns. It must inherit .
- /// The options builder.
- /// The MySql options action.
- public static ISerilogUiOptionsBuilder UseMariaDbServer(
- this ISerilogUiOptionsBuilder optionsBuilder,
- Action setupOptions
- ) where T : MySqlLogModel
- {
- var dbOptions = new RelationalDbOptions("dbo");
- setupOptions(dbOptions);
- dbOptions.Validate();
+ ///
+ /// Configures the SerilogUi to connect to a MySQL/MariaDb database expecting
+ /// Serilog.Sinks.MariaDB. defaults.
+ /// Provider expects sink to store timestamp in utc.
+ ///
+ /// The options builder.
+ /// The MySql options action.
+ public static ISerilogUiOptionsBuilder UseMariaDbServer(this ISerilogUiOptionsBuilder optionsBuilder, Action setupOptions)
+ => optionsBuilder.UseMariaDbServer(setupOptions);
- var providerName = dbOptions.GetProviderName(MariaDbDataProvider.ProviderName);
+ ///
+ /// Configures the SerilogUi to connect to a MySQL/MariaDb database expecting
+ /// Serilog.Sinks.MariaDB. defaults.
+ /// Provider expects sink to store timestamp in utc.
+ ///
+ /// The log model, containing any additional columns. It must inherit .
+ /// The options builder.
+ /// The MySql options action.
+ public static ISerilogUiOptionsBuilder UseMariaDbServer(this ISerilogUiOptionsBuilder optionsBuilder, Action setupOptions)
+ where T : MySqlLogModel
+ {
+ MariaDbOptions dbOptions = new("dbo");
+ setupOptions(dbOptions);
+ dbOptions.Validate();
- optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName);
+ string providerName = dbOptions.GetProviderName(MariaDbDataProvider.ProviderName);
- var customModel = typeof(T) != typeof(MySqlLogModel);
- if (customModel)
- {
- optionsBuilder.RegisterColumnsInfo(providerName);
- optionsBuilder.Services.AddScoped(_ => new MariaDbDataProvider(dbOptions));
- return optionsBuilder;
- }
+ optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName);
- optionsBuilder.Services.AddScoped(_ => new MariaDbDataProvider(dbOptions));
- return optionsBuilder;
+ bool customModel = typeof(T) != typeof(MySqlLogModel);
+ if (customModel)
+ {
+ optionsBuilder.RegisterColumnsInfo(providerName);
+ optionsBuilder.Services.AddScoped(_ => new MariaDbDataProvider(dbOptions, new MySqlQueryBuilder()));
+ }
+ else
+ {
+ optionsBuilder.Services.AddScoped(_ => new MariaDbDataProvider(dbOptions, new MySqlQueryBuilder()));
}
+
+ return optionsBuilder;
}
}
\ No newline at end of file
diff --git a/src/Serilog.Ui.MySqlProvider/MariaDbDataProvider.cs b/src/Serilog.Ui.MySqlProvider/MariaDbDataProvider.cs
index 92bb825f..37fe6af3 100644
--- a/src/Serilog.Ui.MySqlProvider/MariaDbDataProvider.cs
+++ b/src/Serilog.Ui.MySqlProvider/MariaDbDataProvider.cs
@@ -1,23 +1,16 @@
-using Serilog.Ui.Core.Models.Options;
+using Serilog.Ui.MySqlProvider.Extensions;
using Serilog.Ui.MySqlProvider.Shared;
namespace Serilog.Ui.MySqlProvider;
-public class MariaDbDataProvider(RelationalDbOptions options) : MariaDbDataProvider(options)
-{
- protected override string SelectQuery
- => $"SELECT Id, {ColumnMessageName}, {ColumnLevelName} AS 'Level', {ColumnTimestampName}, Exception, Properties ";
-
- protected override string SearchCriteriaWhereQuery() => "OR Exception LIKE @Search";
-}
+public class MariaDbDataProvider(MariaDbOptions options, MySqlQueryBuilder queryBuilder)
+ : MariaDbDataProvider(options, queryBuilder);
-public class MariaDbDataProvider(RelationalDbOptions options) : DataProvider(options)
+public class MariaDbDataProvider(MariaDbOptions options, MySqlQueryBuilder queryBuilder) : DataProvider(options, queryBuilder)
where T : MySqlLogModel
{
internal const string ProviderName = "MariaDb";
- protected override string ColumnLevelName => "LogLevel";
-
- public override string Name => Options.GetProviderName(ProviderName);
+ public override string Name => options.GetProviderName(ProviderName);
}
\ No newline at end of file
diff --git a/src/Serilog.Ui.MySqlProvider/Models/MariaDbSinkColumnNames.cs b/src/Serilog.Ui.MySqlProvider/Models/MariaDbSinkColumnNames.cs
new file mode 100644
index 00000000..723a6f0a
--- /dev/null
+++ b/src/Serilog.Ui.MySqlProvider/Models/MariaDbSinkColumnNames.cs
@@ -0,0 +1,16 @@
+using Serilog.Ui.Core.QueryBuilder.Sql;
+
+namespace Serilog.Ui.MySqlProvider.Models;
+
+internal class MariaDbSinkColumnNames : SinkColumnNames
+{
+ public MariaDbSinkColumnNames()
+ {
+ Exception = "Exception";
+ Level = "LogLevel";
+ LogEventSerialized = "Properties";
+ Message = "Message";
+ MessageTemplate = "";
+ Timestamp = "TimeStamp";
+ }
+}
\ No newline at end of file
diff --git a/src/Serilog.Ui.MySqlProvider/Models/MySqlLogModel.cs b/src/Serilog.Ui.MySqlProvider/Models/MySqlLogModel.cs
new file mode 100644
index 00000000..a79802d6
--- /dev/null
+++ b/src/Serilog.Ui.MySqlProvider/Models/MySqlLogModel.cs
@@ -0,0 +1,27 @@
+using System;
+using Serilog.Ui.Core.Attributes;
+using Serilog.Ui.Core.Models;
+
+namespace Serilog.Ui.MySqlProvider;
+
+///
+/// MySql/MariaDb Log Model.
+/// , , ,
+/// columns can't be overridden and removed from the model, due to query requirements.
+/// To remove a field, apply on it.
+/// To add a field, register the property with the correct datatype on the child class and the sink.
+///
+public class MySqlLogModel : LogModel
+{
+ public override sealed int RowNo => base.RowNo;
+
+ public override sealed string? Level { get; set; }
+
+ public string LogLevel { get; set; } = string.Empty;
+
+ public override sealed string? Message { get; set; } = string.Empty;
+
+ public override sealed DateTime Timestamp { get; set; }
+
+ public override string PropertyType => "json";
+}
\ No newline at end of file
diff --git a/src/Serilog.Ui.MySqlProvider/Models/MySqlSinkColumnNames.cs b/src/Serilog.Ui.MySqlProvider/Models/MySqlSinkColumnNames.cs
new file mode 100644
index 00000000..67c6888a
--- /dev/null
+++ b/src/Serilog.Ui.MySqlProvider/Models/MySqlSinkColumnNames.cs
@@ -0,0 +1,16 @@
+using Serilog.Ui.Core.QueryBuilder.Sql;
+
+namespace Serilog.Ui.MySqlProvider.Models;
+
+internal class MySqlSinkColumnNames : SinkColumnNames
+{
+ public MySqlSinkColumnNames()
+ {
+ Exception = "Exception";
+ Level = "Level";
+ LogEventSerialized = "Properties";
+ Message = "Message";
+ MessageTemplate = "";
+ Timestamp = "TimeStamp";
+ }
+}
\ No newline at end of file
diff --git a/src/Serilog.Ui.MySqlProvider/MySqlDataProvider.cs b/src/Serilog.Ui.MySqlProvider/MySqlDataProvider.cs
index 6bdd363e..b11e8dcb 100644
--- a/src/Serilog.Ui.MySqlProvider/MySqlDataProvider.cs
+++ b/src/Serilog.Ui.MySqlProvider/MySqlDataProvider.cs
@@ -1,15 +1,14 @@
-using Serilog.Ui.Core.Models.Options;
+using Serilog.Ui.MySqlProvider.Extensions;
using Serilog.Ui.MySqlProvider.Shared;
namespace Serilog.Ui.MySqlProvider;
-public class MySqlDataProvider(RelationalDbOptions options) : DataProvider(options)
+public class MySqlDataProvider(MySqlDbOptions options, MySqlQueryBuilder queryBuilder)
+ : DataProvider(options, queryBuilder)
{
- protected override string SelectQuery
- => $"SELECT Id, {ColumnMessageName}, {ColumnLevelName}, {ColumnTimestampName}, Exception, Properties ";
-
- protected override string SearchCriteriaWhereQuery() => "OR Exception LIKE @Search";
+ private readonly MySqlDbOptions _options = options;
internal const string MySqlProviderName = "MySQL";
- public override string Name => Options.GetProviderName(MySqlProviderName);
+
+ public override string Name => _options.GetProviderName(MySqlProviderName);
}
\ No newline at end of file
diff --git a/src/Serilog.Ui.MySqlProvider/MySqlLogModel.cs b/src/Serilog.Ui.MySqlProvider/MySqlLogModel.cs
deleted file mode 100644
index e1358aaa..00000000
--- a/src/Serilog.Ui.MySqlProvider/MySqlLogModel.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using System;
-using Serilog.Ui.Core.Attributes;
-using Serilog.Ui.Core.Models;
-
-namespace Serilog.Ui.MySqlProvider
-{
- ///
- /// MySql/MariaDb Log Model.
- /// , , ,
- /// columns can't be overridden and removed from the model, due to query requirements.
- /// To remove a field, apply on it.
- /// To add a field, register the property with the correct datatype on the child class and the sink.
- ///
- public class MySqlLogModel : LogModel
- {
- public sealed override int RowNo => base.RowNo;
-
- public sealed override string? Level { get; set; }
-
- public string LogLevel { get; set; } = string.Empty;
-
- public sealed override string? Message { get; set; } = string.Empty;
-
- public sealed override DateTime Timestamp { get; set; }
-
- public override string PropertyType => "json";
- }
-}
\ No newline at end of file
diff --git a/src/Serilog.Ui.MySqlProvider/MySqlQueryBuilder.cs b/src/Serilog.Ui.MySqlProvider/MySqlQueryBuilder.cs
new file mode 100644
index 00000000..1d99e7b6
--- /dev/null
+++ b/src/Serilog.Ui.MySqlProvider/MySqlQueryBuilder.cs
@@ -0,0 +1,126 @@
+using Serilog.Ui.Core.Models;
+using Serilog.Ui.Core.QueryBuilder.Sql;
+using System;
+using System.Text;
+
+namespace Serilog.Ui.MySqlProvider;
+
+///
+/// Provides methods to build SQL queries specifically for MySQL and MariaDB to fetch and count logs.
+///
+/// The type of the log model.
+public class MySqlQueryBuilder : SqlQueryBuilder where TModel : LogModel
+{
+ ///
+ public override string BuildFetchLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query)
+ {
+ StringBuilder queryStr = new();
+
+ GenerateSelectClause(queryStr, columns, schema, tableName);
+
+ GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate);
+
+ queryStr.Append($"{GenerateSortClause(columns, query.SortOn, query.SortBy)} LIMIT @Offset, @Count");
+
+ return queryStr.ToString();
+ }
+
+ ///
+ /// Builds a SQL query to count logs in the specified table.
+ ///
+ /// The column names used in the sink for logging.
+ /// The schema of the table.
+ /// The name of the table.
+ /// The query parameters for counting logs.
+ /// A SQL query string to count logs.
+ public override string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query)
+ {
+ StringBuilder queryStr = new();
+
+ queryStr.Append("SELECT COUNT(Id) ")
+ .Append($"FROM {tableName} ");
+
+ GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate);
+
+ return queryStr.ToString();
+ }
+
+ protected override string GenerateSortClause(SinkColumnNames columns, SearchOptions.SortProperty sortOn, SearchOptions.SortDirection sortBy)
+ => $"ORDER BY {GetSortColumnName(columns, sortOn)} {sortBy.ToString().ToUpper()}";
+
+ ///
+ /// Generates the SELECT clause for the SQL query.
+ ///
+ /// The StringBuilder to append the SELECT clause to.
+ /// The column names used in the sink for logging.
+ /// The schema of the table.
+ /// The name of the table.
+ private static void GenerateSelectClause(StringBuilder queryBuilder, SinkColumnNames columns, string schema, string tableName)
+ {
+ if (typeof(TModel) != typeof(MySqlLogModel))
+ {
+ queryBuilder.Append("SELECT * ");
+ }
+ else
+ {
+ queryBuilder.Append("SELECT Id, ")
+ .Append($"{columns.Message}, ")
+ .Append($"{columns.Level}, ")
+ .Append($"{columns.Timestamp}, ")
+ .Append($"{columns.Exception}, ")
+ .Append($"{columns.LogEventSerialized} ");
+ }
+
+ queryBuilder.Append($"FROM {tableName} ");
+ }
+
+ ///
+ /// Generates the WHERE clause for the SQL query.
+ ///
+ /// The StringBuilder to append the WHERE clause to.
+ /// The column names used in the sink for logging.
+ /// The log level to filter by.
+ /// The search criteria to filter by.
+ /// The start date to filter by.
+ /// The end date to filter by.
+ private static void GenerateWhereClause(
+ StringBuilder queryBuilder,
+ SinkColumnNames columns,
+ string? level,
+ string? searchCriteria,
+ DateTime? startDate,
+ DateTime? endDate)
+ {
+ StringBuilder conditions2 = new();
+
+ if (!string.IsNullOrWhiteSpace(level))
+ {
+ conditions2.Append($"AND {columns.Level} = @Level ");
+ }
+
+ if (!string.IsNullOrWhiteSpace(searchCriteria))
+ {
+ conditions2.Append($"AND ({columns.Message} LIKE @Search ");
+ conditions2.Append(AddExceptionToWhereClause() ? $"OR {columns.Exception} LIKE @Search) " : ") ");
+ }
+
+ if (startDate.HasValue)
+ {
+ conditions2.Append($"AND {columns.Timestamp} >= @StartDate ");
+ }
+
+ if (endDate.HasValue)
+ {
+ conditions2.Append($"AND {columns.Timestamp} <= @EndDate ");
+ }
+
+ if (conditions2.Length <= 0)
+ {
+ return;
+ }
+
+ queryBuilder
+ .Append("WHERE TRUE ")
+ .Append(conditions2);
+ }
+}
\ No newline at end of file
diff --git a/src/Serilog.Ui.MySqlProvider/Serilog.Ui.MySqlProvider.csproj b/src/Serilog.Ui.MySqlProvider/Serilog.Ui.MySqlProvider.csproj
index 209595e7..05fc1b6e 100644
--- a/src/Serilog.Ui.MySqlProvider/Serilog.Ui.MySqlProvider.csproj
+++ b/src/Serilog.Ui.MySqlProvider/Serilog.Ui.MySqlProvider.csproj
@@ -17,5 +17,6 @@
+
\ No newline at end of file
diff --git a/src/Serilog.Ui.MySqlProvider/Shared/DataProvider.cs b/src/Serilog.Ui.MySqlProvider/Shared/DataProvider.cs
index 5a603b03..24c8ccb8 100644
--- a/src/Serilog.Ui.MySqlProvider/Shared/DataProvider.cs
+++ b/src/Serilog.Ui.MySqlProvider/Shared/DataProvider.cs
@@ -1,29 +1,20 @@
-using System;
+using Dapper;
+using MySqlConnector;
+using Serilog.Ui.Core;
+using Serilog.Ui.Core.Models;
+using Serilog.Ui.MySqlProvider.Extensions;
+using System;
using System.Collections.Generic;
using System.Linq;
-using System.Reflection;
-using System.Text;
using System.Threading;
using System.Threading.Tasks;
-using Dapper;
-using MySqlConnector;
-using Serilog.Ui.Core;
-using Serilog.Ui.Core.Attributes;
-using Serilog.Ui.Core.Models;
-using Serilog.Ui.Core.Models.Options;
namespace Serilog.Ui.MySqlProvider.Shared;
-public abstract class DataProvider(RelationalDbOptions options) : IDataProvider
+public abstract class DataProvider(MySqlDbOptions options, MySqlQueryBuilder queryBuilder) : IDataProvider
where T : MySqlLogModel
{
- protected virtual string ColumnTimestampName => "TimeStamp";
-
- protected virtual string ColumnLevelName => "Level";
-
- protected virtual string ColumnMessageName => "Message";
-
- protected readonly RelationalDbOptions Options = options ?? throw new ArgumentNullException(nameof(options));
+ public abstract string Name { get; }
public async Task<(IEnumerable, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default)
{
@@ -31,30 +22,19 @@ public abstract class DataProvider(RelationalDbOptions options) : IDataProvid
var logsTask = GetLogsAsync(queryParams);
var logCountTask = CountLogsAsync(queryParams);
-
- await Task.WhenAll(logsTask, logCountTask);
+ await Task.WhenAll(logsTask);
return (await logsTask, await logCountTask);
}
- public abstract string Name { get; }
-
- protected virtual string SelectQuery => "SELECT * ";
-
private async Task> GetLogsAsync(FetchLogsQuery queryParams)
{
- var queryBuilder = new StringBuilder();
- queryBuilder.Append(SelectQuery).Append($"FROM `{Options.TableName}` ");
-
- GenerateWhereClause(queryBuilder, queryParams);
- var sortClause = GenerateSortClause(queryParams.SortOn, queryParams.SortBy);
+ string query = queryBuilder.BuildFetchLogsQuery(options.ColumnNames, options.Schema, options.TableName, queryParams);
+ int rowNoStart = queryParams.Page * queryParams.Count;
- queryBuilder.Append($"ORDER BY {sortClause} LIMIT @Offset, @Count");
+ using MySqlConnection connection = new(options.ConnectionString);
- var rowNoStart = queryParams.Page * queryParams.Count;
-
- using var connection = new MySqlConnection(Options.ConnectionString);
- var param = new
+ IEnumerable logs = await connection.QueryAsync(query, new
{
Offset = rowNoStart,
queryParams.Count,
@@ -62,18 +42,15 @@ private async Task> GetLogsAsync(FetchLogsQuery queryParam
Search = queryParams.SearchCriteria != null ? $"%{queryParams.SearchCriteria}%" : null,
queryParams.StartDate,
queryParams.EndDate
- };
-
- var logs = await connection.QueryAsync(queryBuilder.ToString(), param);
+ });
return logs
.Select((item, i) =>
{
item.SetRowNo(rowNoStart, i);
item.Level ??= item.LogLevel;
- // both sinks save UTC but MariaDb is queried as Unspecified, MySql is queried as Local
- var ts = DateTime.SpecifyKind(item.Timestamp,
- item.Timestamp.Kind == DateTimeKind.Unspecified ? DateTimeKind.Utc : item.Timestamp.Kind);
+ // both sinks save UTC but MariaDb is queried as Unspecified, MySql is queried as Local
+ var ts = DateTime.SpecifyKind(item.Timestamp, item.Timestamp.Kind == DateTimeKind.Unspecified ? DateTimeKind.Utc : item.Timestamp.Kind);
item.Timestamp = ts.ToUniversalTime();
return item;
})
@@ -82,13 +59,11 @@ private async Task> GetLogsAsync(FetchLogsQuery queryParam
private async Task CountLogsAsync(FetchLogsQuery queryParams)
{
- var queryBuilder = new StringBuilder();
- queryBuilder.Append($"SELECT COUNT(Id) FROM `{Options.TableName}` ");
+ string query = queryBuilder.BuildCountLogsQuery(options.ColumnNames, options.Schema, options.TableName, queryParams);
- GenerateWhereClause(queryBuilder, queryParams);
+ using MySqlConnection connection = new(options.ConnectionString);
- using var connection = new MySqlConnection(Options.ConnectionString);
- return await connection.ExecuteScalarAsync(queryBuilder.ToString(),
+ return await connection.ExecuteScalarAsync(query,
new
{
queryParams.Level,
@@ -97,50 +72,4 @@ private async Task CountLogsAsync(FetchLogsQuery queryParams)
queryParams.EndDate
});
}
-
- ///
- /// If Exception property is flagged with ,
- /// it removes the Where query part on the Exception field.
- ///
- ///
- protected virtual string SearchCriteriaWhereQuery()
- {
- var exceptionProperty = typeof(T).GetProperty(nameof(MySqlLogModel.Exception));
- var att = exceptionProperty?.GetCustomAttribute();
- return att is null ? "OR Exception LIKE @Search" : string.Empty;
- }
-
- private void GenerateWhereClause(StringBuilder queryBuilder, FetchLogsQuery queryParams)
- {
- var conditionStart = "WHERE";
-
- if (!string.IsNullOrWhiteSpace(queryParams.Level))
- {
- queryBuilder.Append($"WHERE {ColumnLevelName} = @Level ");
- conditionStart = "AND";
- }
-
- if (!string.IsNullOrWhiteSpace(queryParams.SearchCriteria))
- {
- queryBuilder.Append($"{conditionStart} ({ColumnMessageName} LIKE @Search {SearchCriteriaWhereQuery()}) ");
- conditionStart = "AND";
- }
-
- if (queryParams.StartDate != null)
- {
- queryBuilder.Append($"{conditionStart} {ColumnTimestampName} >= @StartDate ");
- conditionStart = "AND";
- }
-
- if (queryParams.EndDate != null)
- {
- queryBuilder.Append($"{conditionStart} {ColumnTimestampName} <= @EndDate ");
- }
- }
-
- private string GenerateSortClause(SearchOptions.SortProperty sortOn, SearchOptions.SortDirection sortBy)
- {
- var sortProperty = sortOn == SearchOptions.SortProperty.Level ? ColumnLevelName : sortOn.ToString();
- return $"{sortProperty} {sortBy.ToString().ToUpper()}";
- }
}
\ No newline at end of file
diff --git a/src/Serilog.Ui.PostgreSqlProvider/Extensions/PostgreSqlDbOptions.cs b/src/Serilog.Ui.PostgreSqlProvider/Extensions/PostgreSqlDbOptions.cs
index 06cea7c4..0ff48ee9 100644
--- a/src/Serilog.Ui.PostgreSqlProvider/Extensions/PostgreSqlDbOptions.cs
+++ b/src/Serilog.Ui.PostgreSqlProvider/Extensions/PostgreSqlDbOptions.cs
@@ -1,4 +1,5 @@
using Serilog.Ui.Core.Models.Options;
+using Serilog.Ui.Core.QueryBuilder.Sql;
using Serilog.Ui.PostgreSqlProvider.Models;
namespace Serilog.Ui.PostgreSqlProvider.Extensions;
@@ -9,10 +10,9 @@ public class PostgreSqlDbOptions : RelationalDbOptions
///
public PostgreSqlDbOptions(string defaultSchemaName) : base(defaultSchemaName)
{
+ ColumnNames = new PostgreSqlAlternativeSinkColumnNames();
}
- internal SinkColumnNames ColumnNames = new PostgreSqlAlternativeSinkColumnNames();
-
///
/// It gets or sets SinkType.
/// The sink that used to store logs in the PostgreSQL database. This data provider supports
@@ -31,6 +31,9 @@ public PostgreSqlDbOptions WithSinkType(PostgreSqlSinkType sinkType)
ColumnNames = sinkType == PostgreSqlSinkType.SerilogSinksPostgreSQLAlternative
? new PostgreSqlAlternativeSinkColumnNames()
: new PostgreSqlSinkColumnNames();
+
return this;
}
+
+ internal SinkColumnNames ColumnNames { get; set; }
}
\ No newline at end of file
diff --git a/src/Serilog.Ui.PostgreSqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs b/src/Serilog.Ui.PostgreSqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs
index 9f217bcb..8ba23502 100644
--- a/src/Serilog.Ui.PostgreSqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs
+++ b/src/Serilog.Ui.PostgreSqlProvider/Extensions/SerilogUiOptionBuilderExtensions.cs
@@ -1,56 +1,51 @@
-using System;
-using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection;
using Serilog.Ui.Core;
using Serilog.Ui.Core.Interfaces;
using Serilog.Ui.PostgreSqlProvider.Models;
+using System;
-namespace Serilog.Ui.PostgreSqlProvider.Extensions
+namespace Serilog.Ui.PostgreSqlProvider.Extensions;
+
+///
+/// PostgreSQL data provider specific extension methods for .
+///
+public static class SerilogUiOptionBuilderExtensions
{
///
- /// PostgreSQL data provider specific extension methods for .
+ /// Configures the SerilogUi to connect to a PostgreSQL database.
///
- public static class SerilogUiOptionBuilderExtensions
- {
- ///
- /// Configures the SerilogUi to connect to a PostgreSQL database.
- ///
- /// The Serilog UI option builder.
- /// The Postgres Sql options action.
- public static ISerilogUiOptionsBuilder UseNpgSql(
- this ISerilogUiOptionsBuilder optionsBuilder,
- Action setupOptions
- ) => optionsBuilder.UseNpgSql(setupOptions);
-
- ///
- /// Configures the SerilogUi to connect to a PostgreSQL database.
- ///
- /// The log model, containing any additional columns. It must inherit .
- /// The Serilog UI option builder.
- /// The Postgres Sql options action.
- public static ISerilogUiOptionsBuilder UseNpgSql(
- this ISerilogUiOptionsBuilder optionsBuilder,
- Action setupOptions
- ) where T : PostgresLogModel
- {
- var dbOptions = new PostgreSqlDbOptions("public");
- setupOptions(dbOptions);
- dbOptions.Validate();
+ /// The Serilog UI option builder.
+ /// The Postgres Sql options action.
+ public static ISerilogUiOptionsBuilder UseNpgSql(this ISerilogUiOptionsBuilder optionsBuilder, Action setupOptions)
+ => optionsBuilder.UseNpgSql(setupOptions);
- var providerName = dbOptions.GetProviderName(PostgresDataProvider.ProviderName);
-
- optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName);
-
- var customModel = typeof(T) != typeof(PostgresLogModel);
- if (customModel)
- {
- optionsBuilder.RegisterColumnsInfo(providerName);
- optionsBuilder.Services.AddScoped(_ => new PostgresDataProvider(dbOptions));
+ ///
+ /// Configures the SerilogUi to connect to a PostgreSQL database.
+ ///
+ /// The log model, containing any additional columns. It must inherit .
+ /// The Serilog UI option builder.
+ /// The Postgres Sql options action.
+ public static ISerilogUiOptionsBuilder UseNpgSql(this ISerilogUiOptionsBuilder optionsBuilder, Action setupOptions)
+ where T : PostgresLogModel
+ {
+ PostgreSqlDbOptions dbOptions = new("public");
+ setupOptions(dbOptions);
+ dbOptions.Validate();
- return optionsBuilder;
- }
+ string providerName = dbOptions.GetProviderName(PostgresDataProvider.ProviderName);
+ optionsBuilder.RegisterExceptionAsStringForProviderKey(providerName);
- optionsBuilder.Services.AddScoped(_ => new PostgresDataProvider(dbOptions));
- return optionsBuilder;
+ bool customModel = typeof(T) != typeof(PostgresLogModel);
+ if (customModel)
+ {
+ optionsBuilder.RegisterColumnsInfo(providerName);
+ optionsBuilder.Services.AddScoped(_ => new PostgresDataProvider(dbOptions, new PostgresQueryBuilder()));
}
+ else
+ {
+ optionsBuilder.Services.AddScoped(_ => new PostgresDataProvider(dbOptions, new PostgresQueryBuilder()));
+ }
+
+ return optionsBuilder;
}
}
\ No newline at end of file
diff --git a/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreLogModel.cs b/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreLogModel.cs
index d0149d7f..546204d3 100644
--- a/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreLogModel.cs
+++ b/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreLogModel.cs
@@ -17,23 +17,23 @@ public class PostgresLogModel : LogModel
private string _level = string.Empty;
///
- public sealed override int RowNo => base.RowNo;
+ public override sealed int RowNo => base.RowNo;
///
- public sealed override string? Message { get; set; }
+ public override sealed string? Message { get; set; }
///
- public sealed override DateTime Timestamp { get; set; }
+ public override sealed DateTime Timestamp { get; set; }
///
- public sealed override string? Level
+ public override sealed string? Level
{
get => _level;
set => _level = LogLevelConverter.GetLevelName(value);
}
///
- /// It get or sets LogEventSerialized.
+ /// It gets or sets LogEventSerialized.
///
[JsonIgnore]
public string LogEvent { get; set; } = string.Empty;
diff --git a/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlAlternativeSinkColumnNames.cs b/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlAlternativeSinkColumnNames.cs
index 8f72b6e4..b1ecae3c 100644
--- a/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlAlternativeSinkColumnNames.cs
+++ b/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlAlternativeSinkColumnNames.cs
@@ -1,4 +1,6 @@
-namespace Serilog.Ui.PostgreSqlProvider.Models;
+using Serilog.Ui.Core.QueryBuilder.Sql;
+
+namespace Serilog.Ui.PostgreSqlProvider.Models;
internal class PostgreSqlAlternativeSinkColumnNames : SinkColumnNames
{
@@ -7,8 +9,8 @@ public PostgreSqlAlternativeSinkColumnNames()
Exception = "Exception";
Level = "Level";
LogEventSerialized = "LogEvent";
+ Message = "Message";
MessageTemplate = "MessageTemplate";
- RenderedMessage = "Message";
Timestamp = "Timestamp";
}
}
\ No newline at end of file
diff --git a/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlSinkColumnNames.cs b/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlSinkColumnNames.cs
index a2d56427..01153b9d 100644
--- a/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlSinkColumnNames.cs
+++ b/src/Serilog.Ui.PostgreSqlProvider/Models/PostgreSqlSinkColumnNames.cs
@@ -1,14 +1,16 @@
-namespace Serilog.Ui.PostgreSqlProvider.Models;
+using Serilog.Ui.Core.QueryBuilder.Sql;
+
+namespace Serilog.Ui.PostgreSqlProvider.Models;
internal class PostgreSqlSinkColumnNames : SinkColumnNames
{
public PostgreSqlSinkColumnNames()
{
- RenderedMessage = "message";
- MessageTemplate = "message_template";
- Level = "level";
- Timestamp = "timestamp";
Exception = "exception";
+ Level = "level";
LogEventSerialized = "log_event";
+ Message = "message";
+ MessageTemplate = "message_template";
+ Timestamp = "timestamp";
}
}
\ No newline at end of file
diff --git a/src/Serilog.Ui.PostgreSqlProvider/Models/SinkColumnNames.cs b/src/Serilog.Ui.PostgreSqlProvider/Models/SinkColumnNames.cs
deleted file mode 100644
index 296c5d29..00000000
--- a/src/Serilog.Ui.PostgreSqlProvider/Models/SinkColumnNames.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-namespace Serilog.Ui.PostgreSqlProvider.Models;
-
-internal abstract class SinkColumnNames
-{
- public string RenderedMessage { get; set; } = string.Empty;
-
- public string MessageTemplate { get; set; } = string.Empty;
-
- public string Level { get; set; } = string.Empty;
-
- public string Timestamp { get; set; } = string.Empty;
-
- public string Exception { get; set; } = string.Empty;
-
- public string LogEventSerialized { get; set; } = string.Empty;
-}
\ No newline at end of file
diff --git a/src/Serilog.Ui.PostgreSqlProvider/PostgreDataProvider.cs b/src/Serilog.Ui.PostgreSqlProvider/PostgresDataProvider.cs
similarity index 67%
rename from src/Serilog.Ui.PostgreSqlProvider/PostgreDataProvider.cs
rename to src/Serilog.Ui.PostgreSqlProvider/PostgresDataProvider.cs
index fc76c115..3a2d44be 100644
--- a/src/Serilog.Ui.PostgreSqlProvider/PostgreDataProvider.cs
+++ b/src/Serilog.Ui.PostgreSqlProvider/PostgresDataProvider.cs
@@ -1,38 +1,36 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Dapper;
+using Dapper;
using Npgsql;
using Serilog.Ui.Core;
using Serilog.Ui.Core.Models;
using Serilog.Ui.PostgreSqlProvider.Extensions;
using Serilog.Ui.PostgreSqlProvider.Models;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
namespace Serilog.Ui.PostgreSqlProvider;
///
-public class PostgresDataProvider(PostgreSqlDbOptions options) : PostgresDataProvider(options);
+public class PostgresDataProvider(PostgreSqlDbOptions options, PostgresQueryBuilder queryBuilder)
+ : PostgresDataProvider(options, queryBuilder);
///
-public class PostgresDataProvider(PostgreSqlDbOptions options) : IDataProvider
+public class PostgresDataProvider(PostgreSqlDbOptions options, PostgresQueryBuilder queryBuilder) : IDataProvider
where T : PostgresLogModel
{
internal const string ProviderName = "NPGSQL";
- private readonly PostgreSqlDbOptions _options = options ?? throw new ArgumentNullException(nameof(options));
-
///
- public string Name => _options.GetProviderName(ProviderName);
+ public string Name => options.GetProviderName(ProviderName);
///
public async Task<(IEnumerable, int)> FetchDataAsync(FetchLogsQuery queryParams, CancellationToken cancellationToken = default)
{
queryParams.ToUtcDates();
- var logsTask = GetLogsAsync(queryParams);
- var logCountTask = CountLogsAsync(queryParams);
+ Task> logsTask = GetLogsAsync(queryParams);
+ Task logCountTask = CountLogsAsync(queryParams);
await Task.WhenAll(logsTask, logCountTask);
return (await logsTask, await logCountTask);
@@ -40,12 +38,12 @@ public class PostgresDataProvider(PostgreSqlDbOptions options) : IDataProvide
private async Task> GetLogsAsync(FetchLogsQuery queryParams)
{
- var query = options.ColumnNames.BuildFetchLogsQuery(_options.Schema, _options.TableName, queryParams);
- var rowNoStart = queryParams.Page * queryParams.Count;
+ string query = queryBuilder.BuildFetchLogsQuery(options.ColumnNames, options.Schema, options.TableName, queryParams);
+ int rowNoStart = queryParams.Page * queryParams.Count;
- await using var connection = new NpgsqlConnection(_options.ConnectionString);
+ await using NpgsqlConnection connection = new(options.ConnectionString);
- var logs = await connection.QueryAsync(query,
+ IEnumerable logs = await connection.QueryAsync(query,
new
{
Offset = rowNoStart,
@@ -68,9 +66,9 @@ private async Task> GetLogsAsync(FetchLogsQuery queryParam
private async Task CountLogsAsync(FetchLogsQuery queryParams)
{
- var query = options.ColumnNames.BuildCountLogsQuery(_options.Schema, _options.TableName, queryParams);
+ string query = queryBuilder.BuildCountLogsQuery(options.ColumnNames, options.Schema, options.TableName, queryParams);
- await using var connection = new NpgsqlConnection(_options.ConnectionString);
+ await using NpgsqlConnection connection = new(options.ConnectionString);
return await connection.ExecuteScalarAsync(query,
new
diff --git a/src/Serilog.Ui.PostgreSqlProvider/PostgresQueryBuilder.cs b/src/Serilog.Ui.PostgreSqlProvider/PostgresQueryBuilder.cs
new file mode 100644
index 00000000..675fe49a
--- /dev/null
+++ b/src/Serilog.Ui.PostgreSqlProvider/PostgresQueryBuilder.cs
@@ -0,0 +1,122 @@
+using Serilog.Ui.Core.Models;
+using Serilog.Ui.Core.QueryBuilder.Sql;
+using Serilog.Ui.PostgreSqlProvider.Models;
+using System;
+using System.Text;
+using static Serilog.Ui.Core.Models.SearchOptions;
+
+namespace Serilog.Ui.PostgreSqlProvider;
+
+///
+/// Provides methods to build SQL queries specifically for PostgreSQL to fetch and count logs.
+///
+/// The type of the log model.
+public class PostgresQueryBuilder : SqlQueryBuilder where TModel : LogModel
+{
+ ///
+ public override string BuildFetchLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query)
+ {
+ StringBuilder queryStr = new();
+
+ GenerateSelectClause(queryStr, columns, schema, tableName);
+
+ GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate);
+
+ queryStr.Append($"{GenerateSortClause(columns, query.SortOn, query.SortBy)} LIMIT @Count OFFSET @Offset");
+
+ return queryStr.ToString();
+ }
+
+ ///
+ public override string BuildCountLogsQuery(SinkColumnNames columns, string schema, string tableName, FetchLogsQuery query)
+ {
+ StringBuilder queryStr = new();
+
+ queryStr.Append($"SELECT COUNT(\"{columns.Message}\") ")
+ .Append($"FROM \"{schema}\".\"{tableName}\"");
+
+ GenerateWhereClause(queryStr, columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate);
+
+ return queryStr.ToString();
+ }
+
+ ///
+ protected override string GenerateSortClause(SinkColumnNames columns, SortProperty sortOn, SortDirection sortBy)
+ => $"ORDER BY \"{GetSortColumnName(columns, sortOn)}\" {sortBy.ToString().ToUpper()}";
+
+ ///
+ /// Generates the SELECT clause for the SQL query.
+ ///
+ /// The StringBuilder to append the SELECT clause to.
+ /// The column names used in the sink for logging.
+ /// The schema of the table.
+ /// The name of the table.
+ private static void GenerateSelectClause(StringBuilder queryBuilder, SinkColumnNames columns, string schema, string tableName)
+ {
+ if (typeof(TModel) != typeof(PostgresLogModel))
+ {
+ queryBuilder.Append("SELECT *");
+ }
+ else
+ {
+ queryBuilder.Append($"SELECT \"{columns.Message}\", ")
+ .Append($"\"{columns.MessageTemplate}\", ")
+ .Append($"\"{columns.Level}\", ")
+ .Append($"\"{columns.Timestamp}\", ")
+ .Append($"\"{columns.Exception}\", ")
+ .Append($"\"{columns.LogEventSerialized}\" AS \"Properties\"");
+ }
+
+ queryBuilder.Append($" FROM \"{schema}\".\"{tableName}\" ");
+ }
+
+ ///
+ /// Generates the WHERE clause for the SQL query.
+ ///
+ /// The StringBuilder to append the WHERE clause to.
+ /// The column names used in the sink for logging.
+ /// The log level to filter by.
+ /// The search criteria to filter by.
+ /// The start date to filter by.
+ /// The end date to filter by.
+ private static void GenerateWhereClause(
+ StringBuilder queryBuilder,
+ SinkColumnNames columns,
+ string? level,
+ string? searchCriteria,
+ DateTime? startDate,
+ DateTime? endDate)
+ {
+ StringBuilder conditions = new();
+
+ if (!string.IsNullOrWhiteSpace(level))
+ {
+ conditions.Append($"AND \"{columns.Level}\" = @Level ");
+ }
+
+ if (!string.IsNullOrWhiteSpace(searchCriteria))
+ {
+ conditions.Append($"AND (\"{columns.Message}\" LIKE @Search ");
+ conditions.Append(AddExceptionToWhereClause() ? $"OR \"{columns.Exception}\" LIKE @Search) " : ") ");
+ }
+
+ if (startDate.HasValue)
+ {
+ conditions.Append($"AND \"{columns.Timestamp}\" >= @StartDate ");
+ }
+
+ if (endDate.HasValue)
+ {
+ conditions.Append($"AND \"{columns.Timestamp}\" <= @EndDate ");
+ }
+
+ if (conditions.Length <= 0)
+ {
+ return;
+ }
+
+ queryBuilder
+ .Append("WHERE TRUE ")
+ .Append(conditions);
+ }
+}
\ No newline at end of file
diff --git a/src/Serilog.Ui.PostgreSqlProvider/QueryBuilder.cs b/src/Serilog.Ui.PostgreSqlProvider/QueryBuilder.cs
deleted file mode 100644
index 49d8171a..00000000
--- a/src/Serilog.Ui.PostgreSqlProvider/QueryBuilder.cs
+++ /dev/null
@@ -1,111 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Reflection;
-using System.Text;
-using Serilog.Ui.Core.Attributes;
-using Serilog.Ui.Core.Models;
-using Serilog.Ui.PostgreSqlProvider.Models;
-using static Serilog.Ui.Core.Models.SearchOptions;
-
-namespace Serilog.Ui.PostgreSqlProvider;
-
-internal static class QueryBuilder
-{
- internal static string BuildFetchLogsQuery(this SinkColumnNames _columns, string schema, string tableName, FetchLogsQuery query)
- where T : PostgresLogModel
- {
- var sortClause = _columns.GenerateSortClause(query.SortOn, query.SortBy);
-
- return new StringBuilder()
- .GenerateSelectClause(_columns)
- .Append($" FROM \"{schema}\".\"{tableName}\"")
- .GenerateWhereClause(_columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate)
- .Append($" ORDER BY {sortClause} LIMIT @Count OFFSET @Offset")
- .ToString();
- }
-
- internal static string BuildCountLogsQuery(this SinkColumnNames _columns, string schema, string tableName, FetchLogsQuery query)
- where T : PostgresLogModel
- {
- return new StringBuilder()
- .Append($"SELECT COUNT(\"{_columns.RenderedMessage}\") ")
- .Append($"FROM \"{schema}\".\"{tableName}\"")
- .GenerateWhereClause(_columns, query.Level, query.SearchCriteria, query.StartDate, query.EndDate)
- .ToString();
- }
-
- private static StringBuilder GenerateSelectClause(this StringBuilder queryBuilder, SinkColumnNames _columns)
- where T : PostgresLogModel
- {
- if (typeof(T) != typeof(PostgresLogModel))
- {
- return queryBuilder.Append("SELECT *");
- }
-
- return queryBuilder.Append($"SELECT \"{_columns.RenderedMessage}\", ")
- .Append($"\"{_columns.MessageTemplate}\", ")
- .Append($"\"{_columns.Level}\", ")
- .Append($"\"{_columns.Timestamp}\", ")
- .Append($"\"{_columns.Exception}\", ")
- .Append($"\"{_columns.LogEventSerialized}\" AS \"Properties\"");
- }
-
- private static StringBuilder GenerateWhereClause(this StringBuilder queryBuilder,
- SinkColumnNames _columns,
- string? level,
- string? searchCriteria,
- DateTime? startDate,
- DateTime? endDate)
- where T : PostgresLogModel
- {
- var conditions = new List();
-
- if (!string.IsNullOrWhiteSpace(level))
- {
- conditions.Add($"\"{_columns.Level}\" = @Level");
- }
-
- if (!string.IsNullOrWhiteSpace(searchCriteria))
- {
- var exceptionCondition = AddExceptionToWhereClause() ? $"OR \"{_columns.Exception}\" LIKE @Search" : string.Empty;
- conditions.Add($"(\"{_columns.RenderedMessage}\" LIKE @Search {exceptionCondition})");
- }
-
- if (startDate.HasValue)
- {
- conditions.Add($"\"{_columns.Timestamp}\" >= @StartDate");
- }
-
- if (endDate.HasValue)
- {
- conditions.Add($"\"{_columns.Timestamp}\" <= @EndDate");
- }
-
- if (conditions.Count <= 0) return queryBuilder;
-
- return queryBuilder
- .Append(" WHERE TRUE AND ")
- .Append(string.Join(" AND ", conditions));
- }
-
- private static bool AddExceptionToWhereClause()
- where T : PostgresLogModel
- {
- var exceptionProperty = typeof(T).GetProperty(nameof(PostgresLogModel.Exception));
- var att = exceptionProperty?.GetCustomAttribute();
- return att is null;
- }
-
- private static string GenerateSortClause(this SinkColumnNames _columns, SortProperty sortOn, SortDirection sortBy)
- {
- var sortPropertyName = sortOn switch
- {
- SortProperty.Timestamp => _columns.Timestamp,
- SortProperty.Level => _columns.Level,
- SortProperty.Message => _columns.RenderedMessage,
- _ => _columns.Timestamp,
- };
-
- return $"\"{sortPropertyName}\" {sortBy.ToString().ToUpper()}";
- }
-}
\ No newline at end of file
diff --git a/src/Serilog.Ui.PostgreSqlProvider/Serilog.Ui.PostgreSqlProvider.csproj b/src/Serilog.Ui.PostgreSqlProvider/Serilog.Ui.PostgreSqlProvider.csproj
index 9aaffd0f..47a97a7a 100644
--- a/src/Serilog.Ui.PostgreSqlProvider/Serilog.Ui.PostgreSqlProvider.csproj
+++ b/src/Serilog.Ui.PostgreSqlProvider/Serilog.Ui.PostgreSqlProvider.csproj
@@ -4,10 +4,8 @@
Serilog.UI.PostgreSqlProvider
netstandard2.0
latest
- 3.0.0
-
+ 3.1.0
True
-
PostgreSQL data provider for Serilog UI.
serilog serilog-ui serilog.sinks.postgresql postgresql
diff --git a/src/Serilog.Ui.Web/Serilog.Ui.Web.csproj b/src/Serilog.Ui.Web/Serilog.Ui.Web.csproj
index a8905db6..ce1ab57f 100644
--- a/src/Serilog.Ui.Web/Serilog.Ui.Web.csproj
+++ b/src/Serilog.Ui.Web/Serilog.Ui.Web.csproj
@@ -7,12 +7,12 @@
3.0.2
-
-
-
+
+
+
-
+
diff --git a/tests/Serilog.Ui.MongoDbProvider.Tests/Serilog.Ui.MongoDbProvider.Tests.csproj b/tests/Serilog.Ui.MongoDbProvider.Tests/Serilog.Ui.MongoDbProvider.Tests.csproj
index 6b181e90..0564f22b 100644
--- a/tests/Serilog.Ui.MongoDbProvider.Tests/Serilog.Ui.MongoDbProvider.Tests.csproj
+++ b/tests/Serilog.Ui.MongoDbProvider.Tests/Serilog.Ui.MongoDbProvider.Tests.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/DataProviderBaseTest.cs b/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/DataProviderBaseTest.cs
index 34ab68d9..6278d88b 100644
--- a/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/DataProviderBaseTest.cs
+++ b/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/DataProviderBaseTest.cs
@@ -7,42 +7,39 @@
using Serilog.Ui.Common.Tests.TestSuites;
using Serilog.Ui.Core.Extensions;
using Serilog.Ui.Core.Models;
-using Serilog.Ui.Core.Models.Options;
using Serilog.Ui.MsSqlServerProvider;
+using Serilog.Ui.MsSqlServerProvider.Extensions;
using Xunit;
-namespace MsSql.Tests.DataProvider
+namespace MsSql.Tests.DataProvider;
+
+[Trait("Unit-Base", "MsSql")]
+public class DataProviderBaseTest : IUnitBaseTests
{
- [Trait("Unit-Base", "MsSql")]
- public class DataProviderBaseTest : IUnitBaseTests
+ [Fact(Skip = "Not required")]
+ public void It_throws_when_any_dependency_is_null()
+ => throw new NotImplementedException();
+
+ [Fact]
+ public async Task It_logs_and_throws_when_db_read_breaks_down()
{
- [Fact]
- public void It_throws_when_any_dependency_is_null()
- {
- var suts = new List
- {
- () => { _ = new SqlServerDataProvider(null!); },
- () => { _ = new SqlServerDataProvider(null!); },
- };
+ // Arrange
+ SqlServerDataProvider sut = new(
+ new SqlServerDbOptions("dbo").WithConnectionString("connString").WithTable("logs"),
+ new SqlServerQueryBuilder());
- suts.ForEach(sut => sut.Should().ThrowExactly());
- }
+ SqlServerDataProvider sutWithCols = new(
+ new SqlServerDbOptions("dbo").WithConnectionString("connString").WithTable("logs"),
+ new SqlServerQueryBuilder());
- [Fact]
- public async Task It_logs_and_throws_when_db_read_breaks_down()
- {
- // Arrange
- var sut = new SqlServerDataProvider(new RelationalDbOptions("dbo").WithConnectionString("connString").WithTable("logs"));
- var sutWithAdditionalCols =
- new SqlServerDataProvider(new RelationalDbOptions("dbo").WithConnectionString("connString").WithTable("logs"));
- var query = new Dictionary { ["page"] = "1", ["count"] = "10", };
+ Dictionary query = new() { ["page"] = "1", ["count"] = "10" };
- // Act
- var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query));
- var assertWithAdditionalCols = () => sutWithAdditionalCols.FetchDataAsync(FetchLogsQuery.ParseQuery(query));
+ // Act
+ var assert = () => sut.FetchDataAsync(FetchLogsQuery.ParseQuery(query));
+ var assertWithCols = () => sutWithCols.FetchDataAsync(FetchLogsQuery.ParseQuery(query));
- await assert.Should().ThrowExactlyAsync();
- await assertWithAdditionalCols.Should().ThrowExactlyAsync();
- }
+ // Assert
+ await assert.Should().ThrowExactlyAsync();
+ await assertWithCols.Should().ThrowExactlyAsync();
}
}
\ No newline at end of file
diff --git a/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/QueryBuilderTests.cs b/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/QueryBuilderTests.cs
new file mode 100644
index 00000000..1c3ce788
--- /dev/null
+++ b/tests/Serilog.Ui.MsSqlServerProvider.Tests/DataProvider/QueryBuilderTests.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using FluentAssertions;
+using Microsoft.Extensions.Primitives;
+using MsSql.Tests.Util;
+using Serilog.Ui.Core.Models;
+using Serilog.Ui.MsSqlServerProvider;
+using Serilog.Ui.MsSqlServerProvider.Models;
+using Xunit;
+
+namespace MsSql.Tests.DataProvider;
+
+[Trait("Unit-QueryBuilder", "MsSql")]
+public class QueryBuilderTests
+{
+ [Theory]
+ [ClassData(typeof(QueryBuilderTestData))]
+ public void BuildFetchLogsQuery_ForSink_ReturnsCorrectQuery(
+ string schema,
+ string tableName,
+ string level,
+ string searchCriteria,
+ DateTime? startDate,
+ DateTime? endDate,
+ string expectedQuery)
+ {
+ // Arrange
+ Dictionary queryLogs = new()
+ {
+ ["level"] = level,
+ ["search"] = searchCriteria,
+ ["startDate"] = startDate?.ToString("O"),
+ ["endDate"] = endDate?.ToString("O")
+ };
+
+ SqlServerSinkColumnNames sinkColumns = new();
+ SqlServerQueryBuilder sut = new();
+
+ // Act
+ string query = sut.BuildFetchLogsQuery(sinkColumns, schema, tableName, FetchLogsQuery.ParseQuery(queryLogs));
+
+ // Assert
+ query.Should().Be(expectedQuery);
+ }
+
+ [Fact]
+ public void BuildFetchLogsQuery_not_includes_Exception_if_custom_log_model()
+ {
+ // Arrange
+ Dictionary queryLogs = new()
+ {
+ ["level"] = "level",
+ ["search"] = "criteria"
+ };
+
+ SqlServerSinkColumnNames sinkColumns = new();
+ SqlServerQueryBuilder sut = new();
+
+ // Act
+ string query = sut.BuildFetchLogsQuery(sinkColumns, "test", "logs", FetchLogsQuery.ParseQuery(queryLogs));
+
+ // Assert
+ query.ToLowerInvariant().Should().StartWith("select");
+ query.ToLowerInvariant().Should().NotContain("exception");
+ }
+
+ public class QueryBuilderTestData : IEnumerable