Switch dashboard storage to SQL Server

Replaced file-based dashboard storage with SQL Server-backed storage using the new SqlDashboardStorage class and TBDD_SMF_CONFIG table. Updated Program.cs to use the new storage and ensure default dashboards are loaded into the database. Simplified DefaultDashboard.xml to remove old items. Added SQL script for the dashboard storage table. Cleaned up unused folder references in the project file. This centralizes dashboard management and supports multi-instance scenarios.
This commit is contained in:
OlgunR
2026-02-03 11:56:26 +01:00
parent 70e5cbc19f
commit 9db55fd2fd
5 changed files with 146 additions and 54 deletions

View File

@@ -0,0 +1,106 @@
using System.Data;
using System.Text;
using System.Xml.Linq;
using DevExpress.DashboardWeb;
using Microsoft.Data.SqlClient;
namespace DbFirst.API.Dashboards;
public sealed class SqlDashboardStorage : IEditableDashboardStorage
{
private readonly string _connectionString;
private readonly string _tableName;
private readonly Func<string?>? _userProvider;
public SqlDashboardStorage(string connectionString, string tableName = "TBDD_SMF_CONFIG", Func<string?>? userProvider = null)
{
_connectionString = connectionString;
_tableName = tableName;
_userProvider = userProvider;
}
public IEnumerable<DashboardInfo> GetAvailableDashboardsInfo()
{
var dashboards = new List<DashboardInfo>();
using var connection = new SqlConnection(_connectionString);
using var command = new SqlCommand($"SELECT DashboardId, DashboardName FROM dbo.[{_tableName}] WHERE ACTIVE = 1 ORDER BY DashboardName", connection);
connection.Open();
using var reader = command.ExecuteReader();
while (reader.Read())
{
var id = reader.GetString(0);
var name = reader.GetString(1);
dashboards.Add(new DashboardInfo { ID = id, Name = name });
}
return dashboards;
}
public XDocument LoadDashboard(string dashboardId)
{
using var connection = new SqlConnection(_connectionString);
using var command = new SqlCommand($"SELECT DashboardData FROM dbo.[{_tableName}] WHERE DashboardId = @Id AND ACTIVE = 1", connection);
command.Parameters.Add(new SqlParameter("@Id", SqlDbType.NVarChar, 128) { Value = dashboardId });
connection.Open();
var data = command.ExecuteScalar() as byte[];
if (data == null)
{
throw new ArgumentException($"Dashboard '{dashboardId}' not found.");
}
var xml = Encoding.UTF8.GetString(data);
return XDocument.Parse(xml);
}
public string AddDashboard(XDocument dashboard, string dashboardName)
{
var id = string.IsNullOrWhiteSpace(dashboardName)
? Guid.NewGuid().ToString("N")
: dashboardName;
var payload = Encoding.UTF8.GetBytes(dashboard.ToString(SaveOptions.DisableFormatting));
var userName = _userProvider?.Invoke();
using var connection = new SqlConnection(_connectionString);
using var command = new SqlCommand($"INSERT INTO dbo.[{_tableName}] (ACTIVE, DashboardId, DashboardName, DashboardData, ADDED_WHO, ADDED_WHEN) VALUES (1, @Id, @Name, @Data, COALESCE(@User, SUSER_SNAME()), SYSUTCDATETIME())", connection);
command.Parameters.Add(new SqlParameter("@Id", SqlDbType.NVarChar, 128) { Value = id });
command.Parameters.Add(new SqlParameter("@Name", SqlDbType.NVarChar, 256) { Value = string.IsNullOrWhiteSpace(dashboardName) ? id : dashboardName });
command.Parameters.Add(new SqlParameter("@Data", SqlDbType.VarBinary, -1) { Value = payload });
command.Parameters.Add(new SqlParameter("@User", SqlDbType.NVarChar, 50) { Value = (object?)userName ?? DBNull.Value });
connection.Open();
command.ExecuteNonQuery();
return id;
}
public void SaveDashboard(string dashboardId, XDocument dashboard)
{
var payload = Encoding.UTF8.GetBytes(dashboard.ToString(SaveOptions.DisableFormatting));
var userName = _userProvider?.Invoke();
using var connection = new SqlConnection(_connectionString);
using var command = new SqlCommand($"UPDATE dbo.[{_tableName}] SET DashboardData = @Data, CHANGED_WHO = COALESCE(@User, SUSER_SNAME()), CHANGED_WHEN = SYSUTCDATETIME() WHERE DashboardId = @Id", connection);
command.Parameters.Add(new SqlParameter("@Id", SqlDbType.NVarChar, 128) { Value = dashboardId });
command.Parameters.Add(new SqlParameter("@Data", SqlDbType.VarBinary, -1) { Value = payload });
command.Parameters.Add(new SqlParameter("@User", SqlDbType.NVarChar, 50) { Value = (object?)userName ?? DBNull.Value });
connection.Open();
var rows = command.ExecuteNonQuery();
if (rows == 0)
{
throw new ArgumentException($"Dashboard '{dashboardId}' not found.");
}
}
public void DeleteDashboard(string dashboardId)
{
using var connection = new SqlConnection(_connectionString);
using var command = new SqlCommand($"DELETE FROM dbo.[{_tableName}] WHERE DashboardId = @Id", connection);
command.Parameters.Add(new SqlParameter("@Id", SqlDbType.NVarChar, 128) { Value = dashboardId });
connection.Open();
command.ExecuteNonQuery();
}
}

View File

@@ -0,0 +1,17 @@
CREATE TABLE dbo.TBDD_SMF_CONFIG (
[GUID] [bigint] IDENTITY(1,1) NOT NULL,
[ACTIVE] [bit] NOT NULL,
DashboardId NVARCHAR(128) NOT NULL,
DashboardName NVARCHAR(256) NOT NULL,
DashboardData VARBINARY(MAX) NOT NULL,
--- INSERT YOUR COLUMNS HERE ---
[ADDED_WHO] [nvarchar](50) NOT NULL,
[ADDED_WHEN] [datetime] NOT NULL,
[CHANGED_WHO] [nvarchar](50) NULL,
[CHANGED_WHEN] [datetime] NULL,
CONSTRAINT [PK_TBDD_SMF_CONFIG_DashboardStorage] PRIMARY KEY CLUSTERED
(
[GUID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = ON, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 80) ON [PRIMARY]
) ON [PRIMARY]
GO

View File

@@ -10,48 +10,6 @@
</JsonDataSource> </JsonDataSource>
</DataSources> </DataSources>
<Items> <Items>
<Grid ComponentName="gridDashboardItem1" Name="Grid 1" DataSource="jsonDataSource1">
<DataItems>
<Dimension DataMember="Address" DefaultId="DataItem0" />
<Dimension DataMember="City" DefaultId="DataItem1" />
<Dimension DataMember="CompanyName" DefaultId="DataItem2" />
</DataItems>
<GridColumns>
<GridDimensionColumn>
<Dimension DefaultId="DataItem0" />
</GridDimensionColumn>
<GridDimensionColumn>
<Dimension DefaultId="DataItem1" />
</GridDimensionColumn>
<GridDimensionColumn>
<Dimension DefaultId="DataItem2" />
</GridDimensionColumn>
</GridColumns>
<GridOptions />
<ColumnFilterOptions />
</Grid>
<Chart ComponentName="chartDashboardItem1" Name="Chart 1" DataSource="jsonDataSource1">
<DataItems>
<Measure DataMember="Address" SummaryType="Count" DefaultId="DataItem0" />
<Measure DataMember="City" SummaryType="Count" DefaultId="DataItem1" />
<Measure DataMember="CompanyName" SummaryType="Count" DefaultId="DataItem2" />
</DataItems>
<Panes>
<Pane Name="Pane 1">
<Series>
<Simple>
<Value DefaultId="DataItem0" />
</Simple>
<Simple>
<Value DefaultId="DataItem1" />
</Simple>
<Simple>
<Value DefaultId="DataItem2" />
</Simple>
</Series>
</Pane>
</Panes>
</Chart>
<Grid ComponentName="gridDashboardItem2" Name="Grid 2" DataSource="catalogsDataSource"> <Grid ComponentName="gridDashboardItem2" Name="Grid 2" DataSource="catalogsDataSource">
<DataItems> <DataItems>
<Measure DataMember="guid" DefaultId="DataItem0" /> <Measure DataMember="guid" DefaultId="DataItem0" />
@@ -91,13 +49,7 @@
</Items> </Items>
<LayoutTree> <LayoutTree>
<LayoutGroup> <LayoutGroup>
<LayoutGroup Orientation="Vertical">
<LayoutGroup>
<LayoutItem DashboardItem="gridDashboardItem1" />
<LayoutItem DashboardItem="gridDashboardItem2" /> <LayoutItem DashboardItem="gridDashboardItem2" />
</LayoutGroup> </LayoutGroup>
<LayoutItem DashboardItem="chartDashboardItem1" />
</LayoutGroup>
</LayoutGroup>
</LayoutTree> </LayoutTree>
</Dashboard> </Dashboard>

View File

@@ -26,8 +26,4 @@
<ProjectReference Include="..\DbFirst.Infrastructure\DbFirst.Infrastructure.csproj" /> <ProjectReference Include="..\DbFirst.Infrastructure\DbFirst.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Data\Dashboards\" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,4 +1,5 @@
using DbFirst.API.Middleware; using DbFirst.API.Middleware;
using DbFirst.API.Dashboards;
using DbFirst.Application; using DbFirst.Application;
using DbFirst.Application.Repositories; using DbFirst.Application.Repositories;
using DbFirst.Domain; using DbFirst.Domain;
@@ -10,6 +11,7 @@ using DevExpress.DashboardAspNetCore;
using DevExpress.DashboardCommon; using DevExpress.DashboardCommon;
using DevExpress.DashboardWeb; using DevExpress.DashboardWeb;
using DevExpress.DataAccess.Json; using DevExpress.DataAccess.Json;
using System.Xml.Linq;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -108,7 +110,10 @@ builder.Services.AddScoped<DashboardConfigurator>((IServiceProvider serviceProvi
} }
DashboardConfigurator configurator = new DashboardConfigurator(); DashboardConfigurator configurator = new DashboardConfigurator();
configurator.SetDashboardStorage(new DashboardFileStorage(dashboardsPath));
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? string.Empty;
var dashboardStorage = new SqlDashboardStorage(connectionString, "TBDD_SMF_CONFIG");
configurator.SetDashboardStorage(dashboardStorage);
DataSourceInMemoryStorage dataSourceStorage = new DataSourceInMemoryStorage(); DataSourceInMemoryStorage dataSourceStorage = new DataSourceInMemoryStorage();
DashboardJsonDataSource jsonDataSourceUrl = new DashboardJsonDataSource("JSON Data Source (URL)"); DashboardJsonDataSource jsonDataSourceUrl = new DashboardJsonDataSource("JSON Data Source (URL)");
@@ -126,6 +131,10 @@ builder.Services.AddScoped<DashboardConfigurator>((IServiceProvider serviceProvi
dataSourceStorage.RegisterDataSource(catalogsJsonDataSource.Name, catalogsJsonDataSource.SaveToXml()); dataSourceStorage.RegisterDataSource(catalogsJsonDataSource.Name, catalogsJsonDataSource.SaveToXml());
configurator.SetDataSourceStorage(dataSourceStorage); configurator.SetDataSourceStorage(dataSourceStorage);
EnsureDashboardInStorage(dashboardStorage, "DefaultDashboard", defaultDashboardPath);
EnsureDashboardInStorage(dashboardStorage, "CatalogsGrid", catalogsGridDashboardPath);
return configurator; return configurator;
}); });
@@ -150,3 +159,15 @@ app.MapDashboardRoute("api/dashboard", "DefaultDashboard");
app.MapControllers(); app.MapControllers();
app.Run(); app.Run();
static void EnsureDashboardInStorage(IEditableDashboardStorage storage, string id, string filePath)
{
var exists = storage.GetAvailableDashboardsInfo().Any(info => string.Equals(info.ID, id, StringComparison.OrdinalIgnoreCase));
if (exists || !File.Exists(filePath))
{
return;
}
var doc = XDocument.Load(filePath);
storage.AddDashboard(doc, id);
}