Skip to content

Commit a2a2647

Browse files
authored
Merge pull request #757 from PHOENIXCONTACT/port/dev
Port/dev
2 parents 3025f18 + 41ac9f9 commit a2a2647

25 files changed

+1952
-16
lines changed

Directory.Packages.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="$(efCoreVersion)" />
4242
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="$(efCoreVersion)" />
4343
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="$(efCoreVersion)" />
44+
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="$(efCoreVersion)" />
45+
4446
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
4547

4648
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />

MORYX-Framework.sln

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.Model.InMemory", "src
6464
EndProject
6565
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DFC092A6-B935-4D19-A564-9AEDEEF999B9}"
6666
ProjectSection(SolutionItems) = preProject
67-
Build.ps1 = Build.ps1
6867
.build\Common.props = .build\Common.props
6968
Directory.build.props = Directory.build.props
7069
Directory.Build.targets = Directory.Build.targets
@@ -317,6 +316,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moryx.Shifts.Web", "src\Mor
317316
EndProject
318317
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moryx.Shifts.Management.IntegrationTests", "src\Tests\Moryx.Shifts.Management.IntegrationTests\Moryx.Shifts.Management.IntegrationTests.csproj", "{5E02B439-B91F-4297-9AE4-7B92F2AA2AFF}"
319318
EndProject
319+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moryx.Model.SqlServer", "src\Moryx.Model.SqlServer\Moryx.Model.SqlServer.csproj", "{4402EF2E-CBA8-4EEF-B8A6-EC8364960306}"
320+
EndProject
320321
Global
321322
GlobalSection(SolutionConfigurationPlatforms) = preSolution
322323
Debug|Any CPU = Debug|Any CPU
@@ -835,6 +836,10 @@ Global
835836
{5E02B439-B91F-4297-9AE4-7B92F2AA2AFF}.Debug|Any CPU.Build.0 = Debug|Any CPU
836837
{5E02B439-B91F-4297-9AE4-7B92F2AA2AFF}.Release|Any CPU.ActiveCfg = Release|Any CPU
837838
{5E02B439-B91F-4297-9AE4-7B92F2AA2AFF}.Release|Any CPU.Build.0 = Release|Any CPU
839+
{4402EF2E-CBA8-4EEF-B8A6-EC8364960306}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
840+
{4402EF2E-CBA8-4EEF-B8A6-EC8364960306}.Debug|Any CPU.Build.0 = Debug|Any CPU
841+
{4402EF2E-CBA8-4EEF-B8A6-EC8364960306}.Release|Any CPU.ActiveCfg = Release|Any CPU
842+
{4402EF2E-CBA8-4EEF-B8A6-EC8364960306}.Release|Any CPU.Build.0 = Release|Any CPU
838843
EndGlobalSection
839844
GlobalSection(SolutionProperties) = preSolution
840845
HideSolutionNode = FALSE
@@ -963,6 +968,7 @@ Global
963968
{D27EC875-D22E-4AB0-8E91-C4B7ED0DB91E} = {8DF13E64-63FC-44A9-A54C-ADEFC356CDE8}
964969
{22358E04-0AE4-4ADA-9018-56C881E313F1} = {8DF13E64-63FC-44A9-A54C-ADEFC356CDE8}
965970
{5E02B439-B91F-4297-9AE4-7B92F2AA2AFF} = {8517D209-5BC1-47BD-A7C7-9CF9ADD9F5B6}
971+
{4402EF2E-CBA8-4EEF-B8A6-EC8364960306} = {74112169-6672-4907-A187-F055111940A9}
966972
EndGlobalSection
967973
GlobalSection(ExtensibilityGlobals) = postSolution
968974
SolutionGuid = {36EFC961-F4E7-49DC-A36A-99594FFB8243}

src/Moryx.CommandCenter.Web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"@types/react-router-redux": "^5.0.27",
4242
"css-loader": "^7.1.2",
4343
"html-webpack-plugin": "^5.6.0",
44-
"rimraf": "6.0.1",
44+
"rimraf": "6.1.0",
4545
"sass": "^1.72.0",
4646
"sass-loader": "^16.0.5",
4747
"source-map-loader": "^5.0.0",
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
6+
<Description>Adapter for Moryx.Model on SqlServer</Description>
7+
<PackageTags>MORYX;Entity;Framework;EntityFramework;DataModel;Model;Database;SqlServer</PackageTags>
8+
<IsPackable>true</IsPackable>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<ProjectReference Include="..\Moryx.Model\Moryx.Model.csproj" />
13+
</ItemGroup>
14+
15+
<ItemGroup>
16+
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
17+
</ItemGroup>
18+
</Project>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System.ComponentModel;
2+
using System.ComponentModel.DataAnnotations;
3+
using System.Runtime.Serialization;
4+
5+
namespace Moryx.Model.SqlServer;
6+
7+
/// <summary>
8+
/// Database config for the SqlServer databases
9+
/// </summary>
10+
[DataContract]
11+
public class SqlServerDatabaseConfig : DatabaseConfig<SqlServerDatabaseConnectionSettings>
12+
{
13+
/// <summary>
14+
/// Creates a new instance of the <see cref="SqlServerDatabaseConfig"/>
15+
/// </summary>
16+
public SqlServerDatabaseConfig()
17+
{
18+
ConnectionSettings = new SqlServerDatabaseConnectionSettings();
19+
ConfiguratorTypename = typeof(SqlServerModelConfigurator).AssemblyQualifiedName;
20+
}
21+
}
22+
23+
/// <summary>
24+
/// Database connection settings for the SqlServer databases
25+
/// </summary>
26+
public class SqlServerDatabaseConnectionSettings : DatabaseConnectionSettings
27+
{
28+
private string _database;
29+
30+
/// <inheritdoc />
31+
[DataMember]
32+
public override string Database
33+
{
34+
get => _database;
35+
set
36+
{
37+
if (string.IsNullOrEmpty(value)) return;
38+
_database = value;
39+
ConnectionString = ConnectionString?.Replace("<DatabaseName>", value);
40+
}
41+
}
42+
43+
/// <inheritdoc/>
44+
[DataMember, Required, DefaultValue("Server=localhost;Initial Catalog=<DatabaseName>;User Id=sa;Password=password;TrustServerCertificate=True;")]
45+
public override string ConnectionString { get; set; }
46+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) 2023, Phoenix Contact GmbH & Co. KG
2+
// Licensed under the Apache License, Version 2.0
3+
4+
using System;
5+
using Moryx.Model.Attributes;
6+
7+
namespace Moryx.Model.SqlServer;
8+
9+
/// <summary>
10+
/// Attribute to identify SqlServer specific contexts
11+
/// </summary>
12+
[AttributeUsage(AttributeTargets.Class)]
13+
public class SqlServerDbContextAttribute : DatabaseTypeSpecificDbContextAttribute
14+
{
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="SqlServerDbContextAttribute"/> class.
17+
/// </summary>
18+
public SqlServerDbContextAttribute() : base(typeof(SqlServerModelConfigurator))
19+
{
20+
}
21+
22+
/// <summary>
23+
/// Initializes a new instance of the <see cref="SqlServerDbContextAttribute"/> class.
24+
/// </summary>
25+
/// <param name="baseDbContextType">Type of the base DbContext-type</param>
26+
public SqlServerDbContextAttribute(Type baseDbContextType) : base(typeof(SqlServerModelConfigurator), baseDbContextType)
27+
{
28+
}
29+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) 2025, Phoenix Contact GmbH & Co. KG
2+
// Licensed under the Apache License, Version 2.0
3+
4+
using Microsoft.EntityFrameworkCore;
5+
6+
namespace Moryx.Model.SqlServer;
7+
8+
/// <summary>
9+
/// Base class for design time factories for SqlServer DbContexts
10+
/// </summary>
11+
/// <typeparam name="TContext">Type of the DbContext</typeparam>
12+
public abstract class SqlServerDesignTimeDbContextFactory<TContext> : DesignTimeDbContextFactory<TContext>
13+
where TContext : DbContext
14+
{
15+
/// <inheritdoc />
16+
protected override TContext CreateDbContext(string connectionString)
17+
{
18+
var optionsBuilder = new DbContextOptionsBuilder<TContext>();
19+
optionsBuilder.UseSqlServer(connectionString);
20+
21+
return (TContext)Activator.CreateInstance(typeof(TContext), optionsBuilder.Options)!;
22+
}
23+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright (c) 2025, Phoenix Contact GmbH & Co. KG
2+
// Licensed under the Apache License, Version 2.0
3+
4+
using Microsoft.Data.SqlClient;
5+
using Microsoft.EntityFrameworkCore;
6+
using Microsoft.Extensions.Logging;
7+
using Moryx.Model.Configuration;
8+
using System.ComponentModel;
9+
using System.Data.Common;
10+
using System.Text.RegularExpressions;
11+
12+
namespace Moryx.Model.SqlServer;
13+
14+
/// <summary>
15+
/// Used to configure, create and update data models
16+
/// </summary>
17+
[DisplayName("SqlServer Connector")]
18+
public sealed class SqlServerModelConfigurator : ModelConfiguratorBase<SqlServerDatabaseConfig>
19+
{
20+
/// <inheritdoc />
21+
protected override DbConnection CreateConnection(IDatabaseConfig config)
22+
{
23+
return CreateConnection(config, true);
24+
}
25+
26+
/// <inheritdoc />
27+
protected override DbConnection CreateConnection(IDatabaseConfig config, bool includeModel)
28+
{
29+
return new SqlConnection(BuildConnectionString(config, includeModel));
30+
}
31+
32+
/// <inheritdoc />
33+
protected override DbCommand CreateCommand(string cmdText, DbConnection connection)
34+
{
35+
return new SqlCommand(cmdText, (SqlConnection)connection);
36+
}
37+
38+
/// <inheritdoc />
39+
public override async Task DeleteDatabase(IDatabaseConfig config)
40+
{
41+
var settings = (SqlServerDatabaseConnectionSettings)config.ConnectionSettings;
42+
43+
// Create connection and prepare command
44+
await using var connection = new SqlConnection(BuildConnectionString(config, false));
45+
46+
var sqlCommandText = $"ALTER DATABASE {settings.Database} SET SINGLE_USER WITH ROLLBACK IMMEDIATE;" +
47+
$"DROP DATABASE [{settings.Database}]";
48+
49+
await using var command = CreateCommand(sqlCommandText, connection);
50+
51+
// Open connection
52+
await connection.OpenAsync();
53+
await command.ExecuteNonQueryAsync();
54+
}
55+
56+
/// <inheritdoc />
57+
public override async Task DumpDatabase(IDatabaseConfig config, string targetPath)
58+
{
59+
if (!IsValidBackupFilePath(targetPath))
60+
throw new ArgumentException("Invalid backup file path.");
61+
62+
var connectionString = CreateConnectionStringBuilder(config);
63+
64+
var dumpName = $"{DateTime.Now:dd-MM-yyyy-hh-mm-ss}_{connectionString.InitialCatalog}.bak";
65+
var fileName = Path.Combine(targetPath, dumpName);
66+
67+
await using var connection = new SqlConnection(BuildConnectionString(config, false));
68+
await using var command =
69+
CreateCommand($"BACKUP DATABASE [{connectionString.InitialCatalog}] TO DISK = N'{fileName}' WITH INIT",
70+
connection);
71+
72+
Logger.Log(LogLevel.Debug, "Starting to dump database with 'BACKUP DATABASE' to: {fileName}", fileName);
73+
74+
await connection.OpenAsync();
75+
await command.ExecuteNonQueryAsync();
76+
}
77+
78+
private static SqlConnectionStringBuilder CreateConnectionStringBuilder(IDatabaseConfig config, bool includeModel = true)
79+
{
80+
var builder = new SqlConnectionStringBuilder(config.ConnectionSettings.ConnectionString)
81+
{
82+
InitialCatalog = includeModel ? config.ConnectionSettings.Database : string.Empty
83+
};
84+
85+
return builder;
86+
}
87+
88+
/// <inheritdoc />
89+
public override async Task RestoreDatabase(IDatabaseConfig config, string filePath)
90+
{
91+
if (!IsValidBackupFilePath(filePath))
92+
throw new ArgumentException("Invalid backup file path.");
93+
94+
var connectionString = CreateConnectionStringBuilder(config);
95+
96+
await using var connection = new SqlConnection(BuildConnectionString(config, false));
97+
await using var command = CreateCommand($"RESTORE DATABASE [{connectionString.InitialCatalog}] FROM DISK = N'{filePath}' WITH REPLACE",
98+
connection);
99+
100+
Logger.Log(LogLevel.Debug, "Starting to restore database with 'RESTORE DATABASE' from: {filePath}", filePath);
101+
102+
await connection.OpenAsync();
103+
await command.ExecuteNonQueryAsync();
104+
}
105+
106+
/// <inheritdoc />
107+
public override DbContextOptions BuildDbContextOptions(IDatabaseConfig config)
108+
{
109+
var builder = new DbContextOptionsBuilder();
110+
builder.UseSqlServer(BuildConnectionString(config, true));
111+
112+
return builder.Options;
113+
}
114+
115+
private static string BuildConnectionString(IDatabaseConfig config, bool includeModel)
116+
{
117+
if (!IsValidDatabaseName(config.ConnectionSettings.Database))
118+
throw new ArgumentException("Invalid database name.");
119+
120+
var builder = CreateConnectionStringBuilder(config, includeModel);
121+
builder.PersistSecurityInfo = true;
122+
123+
return builder.ToString();
124+
}
125+
126+
/// <inheritdoc />
127+
protected override DbContext CreateMigrationContext(IDatabaseConfig config)
128+
{
129+
var migrationAssemblyType = FindMigrationAssemblyType(typeof(SqlServerDbContextAttribute));
130+
131+
var builder = new DbContextOptionsBuilder();
132+
builder.UseSqlServer(
133+
BuildConnectionString(config, true),
134+
x => x.MigrationsAssembly(migrationAssemblyType.Assembly.FullName));
135+
136+
return CreateContext(migrationAssemblyType, builder.Options);
137+
}
138+
139+
private static bool IsValidDatabaseName(string dbName)
140+
{
141+
// Avoid sql injection by validating the database name
142+
if (string.IsNullOrWhiteSpace(dbName) || dbName.Length > 128)
143+
return false;
144+
145+
// Only allow letters, numbers, and underscores
146+
return Regex.IsMatch(dbName, @"^[A-Za-z0-9_]+$");
147+
}
148+
149+
private static bool IsValidBackupFilePath(string filePath)
150+
{
151+
// Disallow dangerous characters
152+
var invalidStrings = new[] { ";", "'", "\"", "--" };
153+
return invalidStrings.All(s => !filePath.Contains(s));
154+
}
155+
}

src/Moryx.Model.Sqlite/SqliteDatabaseConfig.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ public SqliteDatabaseConfig()
2424
}
2525
}
2626

27-
internal class DefaultSqliteDbPathAttribute : DefaultValueAttribute
27+
internal class DefaultSqliteConnectionStringAttribute : DefaultValueAttribute
2828
{
29-
public DefaultSqliteDbPathAttribute() : base("")
29+
public DefaultSqliteConnectionStringAttribute() : base("")
3030
{
3131
var path = Path.Combine(".", "db", "<DatabaseName>.db");
3232
SetValue($"Data Source={path};Mode=ReadWrite;");
@@ -54,8 +54,7 @@ public override string Database
5454
}
5555

5656
/// <inheritdoc />
57-
[DataMember, Required]
58-
[DefaultSqliteDbPath]
57+
[DataMember, Required, DefaultSqliteConnectionString]
5958
public override string ConnectionString { get; set; }
6059

6160
/// <inheritdoc />

src/Moryx.Model/Configuration/ModelConfiguratorBase.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
using System.Data.Common;
55
using Microsoft.EntityFrameworkCore;
66
using Microsoft.Extensions.Logging;
7-
using Moryx.Configuration;
8-
using Moryx.Model.Attributes;
97
using Moryx.Tools;
108

119
namespace Moryx.Model.Configuration
@@ -252,7 +250,7 @@ private async Task<bool> TestDatabaseConnection(IDatabaseConfig config)
252250
await conn.OpenAsync();
253251
return true;
254252
}
255-
catch (Exception)
253+
catch(Exception e)
256254
{
257255
return false;
258256
}

0 commit comments

Comments
 (0)