Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
<PackageReference Update="Microsoft.EntityFrameworkCore.Relational" Version="$(efCoreVersion)" />
<PackageReference Update="Microsoft.EntityFrameworkCore.InMemory" Version="$(efCoreVersion)" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="$(efCoreVersion)" />
<PackageReference Update="Microsoft.EntityFrameworkCore.SqlServer" Version="$(dotnetVersion)" />
<PackageReference Update="Npgsql.EntityFrameworkCore.PostgreSQL" Version="$(efCoreVersion)" />

<PackageReference Update="System.IO.Ports" Version="$(dotnetVersion)" />
Expand Down
11 changes: 9 additions & 2 deletions MORYX-Framework.sln
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.TestTools.NUnit", "sr
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.TestTools.IntegrationTest", "src\Moryx.TestTools.IntegrationTest\Moryx.TestTools.IntegrationTest.csproj", "{C949164C-0345-4893-9E4C-A79BC1F93F85}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moryx.Model.SqlServer", "src\Moryx.Model.SqlServer\Moryx.Model.SqlServer.csproj", "{4402EF2E-CBA8-4EEF-B8A6-EC8364960306}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -309,6 +311,10 @@ Global
{C949164C-0345-4893-9E4C-A79BC1F93F85}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C949164C-0345-4893-9E4C-A79BC1F93F85}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C949164C-0345-4893-9E4C-A79BC1F93F85}.Release|Any CPU.Build.0 = Release|Any CPU
{4402EF2E-CBA8-4EEF-B8A6-EC8364960306}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4402EF2E-CBA8-4EEF-B8A6-EC8364960306}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4402EF2E-CBA8-4EEF-B8A6-EC8364960306}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4402EF2E-CBA8-4EEF-B8A6-EC8364960306}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -355,10 +361,11 @@ Global
{4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8} = {8517D209-5BC1-47BD-A7C7-9CF9ADD9F5B6}
{6FF878E0-AF61-4C3A-9B9C-71C35A949E51} = {953AAE25-26C8-4A28-AB08-61BAFE41B22F}
{C949164C-0345-4893-9E4C-A79BC1F93F85} = {953AAE25-26C8-4A28-AB08-61BAFE41B22F}
{4402EF2E-CBA8-4EEF-B8A6-EC8364960306} = {74112169-6672-4907-A187-F055111940A9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {36EFC961-F4E7-49DC-A36A-99594FFB8243}
SolutionGuid = {36EFC961-F4E7-49DC-A36A-99594FFB8243}
RESX_TaskErrorCategory = Message
RESX_ShowErrorsInErrorList = True
RESX_ShowErrorsInErrorList = True
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@ namespace Moryx.Model.PostgreSQL.Attributes
/// Attribute to identify Npgsql specific contexts
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class NpgsqlDatabaseContextAttribute : DatabaseSpecificContextAttribute { }
public class NpgsqlDatabaseContextAttribute : DatabaseSpecificContextAttribute
{

}
}
18 changes: 18 additions & 0 deletions src/Moryx.Model.SqlServer/Moryx.Model.SqlServer.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Description>Adapter for Moryx.Model on SqlServer</Description>
<PackageTags>MORYX;Entity;Framework;EntityFramework;DataModel;Model;Database;SqlServer</PackageTags>
<IsPackable>true</IsPackable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Moryx.Model\Moryx.Model.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
</ItemGroup>
</Project>
46 changes: 46 additions & 0 deletions src/Moryx.Model.SqlServer/SqlServerDatabaseConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;

namespace Moryx.Model.SqlServer;

/// <summary>
/// Database config for the SqlServer databases
/// </summary>
[DataContract]
public class SqlServerDatabaseConfig : DatabaseConfig<SqlServerDatabaseConnectionSettings>
{
/// <summary>
/// Creates a new instance of the <see cref="SqlServerDatabaseConfig"/>
/// </summary>
public SqlServerDatabaseConfig()
{
ConnectionSettings = new SqlServerDatabaseConnectionSettings();
ConfiguratorTypename = typeof(SqlServerModelConfigurator).AssemblyQualifiedName;
}
}

/// <summary>
/// Database connection settings for the SqlServer databases
/// </summary>
public class SqlServerDatabaseConnectionSettings : DatabaseConnectionSettings
{
private string _database;

/// <inheritdoc />
[DataMember]
public override string Database
{
get => _database;
set
{
if (string.IsNullOrEmpty(value)) return;
_database = value;
ConnectionString = ConnectionString?.Replace("<DatabaseName>", value);
}
}

/// <inheritdoc/>
[DataMember, Required, DefaultValue("Server=localhost;Initial Catalog=<DatabaseName>;User Id=sa;Password=password;TrustServerCertificate=True;")]
public override string ConnectionString { get; set; }
}
16 changes: 16 additions & 0 deletions src/Moryx.Model.SqlServer/SqlServerDatabaseContextAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) 2023, Phoenix Contact GmbH & Co. KG
// Licensed under the Apache License, Version 2.0

using System;
using Moryx.Model.Attributes;

namespace Moryx.Model.SqlServer;

/// <summary>
/// Attribute to identify SqlServer specific contexts
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class SqlServerDatabaseContextAttribute : DatabaseSpecificContextAttribute
{

}
161 changes: 161 additions & 0 deletions src/Moryx.Model.SqlServer/SqlServerModelConfigurator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright (c) 2025, Phoenix Contact GmbH & Co. KG
// Licensed under the Apache License, Version 2.0

using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moryx.Model.Configuration;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data.Common;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Linq;

namespace Moryx.Model.SqlServer;

/// <summary>
/// Used to configure, create and update data models
/// </summary>
[DisplayName("SqlServer Connector")]
public sealed class SqlServerModelConfigurator : ModelConfiguratorBase<SqlServerDatabaseConfig>
{
/// <inheritdoc />
protected override DbConnection CreateConnection(IDatabaseConfig config)
{
return CreateConnection(config, true);
}

/// <inheritdoc />
protected override DbConnection CreateConnection(IDatabaseConfig config, bool includeModel)
{
return new SqlConnection(BuildConnectionString(config, includeModel));
}

/// <inheritdoc />
protected override DbCommand CreateCommand(string cmdText, DbConnection connection)
{
return new SqlCommand(cmdText, (SqlConnection)connection);
}

/// <inheritdoc />
public override async Task DeleteDatabase(IDatabaseConfig config)
{
var settings = (SqlServerDatabaseConnectionSettings)config.ConnectionSettings;

// Create connection and prepare command
await using var connection = new SqlConnection(BuildConnectionString(config, false));

var sqlCommandText = $"ALTER DATABASE {settings.Database} SET SINGLE_USER WITH ROLLBACK IMMEDIATE;" +
$"DROP DATABASE [{settings.Database}]";

await using var command = CreateCommand(sqlCommandText, connection);

// Open connection
await connection.OpenAsync();
await command.ExecuteNonQueryAsync();
}

/// <inheritdoc />
public override async Task DumpDatabase(IDatabaseConfig config, string targetPath)
{
if (!IsValidBackupFilePath(targetPath))
throw new ArgumentException("Invalid backup file path.");

var connectionString = CreateConnectionStringBuilder(config);

var dumpName = $"{DateTime.Now:dd-MM-yyyy-hh-mm-ss}_{connectionString.InitialCatalog}.bak";
var fileName = Path.Combine(targetPath, dumpName);

await using var connection = new SqlConnection(BuildConnectionString(config, false));
await using var command =
CreateCommand($"BACKUP DATABASE [{connectionString.InitialCatalog}] TO DISK = N'{fileName}' WITH INIT",
connection);

Logger.Log(LogLevel.Debug, "Starting to dump database with 'BACKUP DATABASE' to: {fileName}", fileName);

await connection.OpenAsync();
await command.ExecuteNonQueryAsync();
}

private static SqlConnectionStringBuilder CreateConnectionStringBuilder(IDatabaseConfig config, bool includeModel = true)
{
var builder = new SqlConnectionStringBuilder(config.ConnectionSettings.ConnectionString)
{
InitialCatalog = includeModel ? config.ConnectionSettings.Database : string.Empty
};

return builder;
}

/// <inheritdoc />
public override async Task RestoreDatabase(IDatabaseConfig config, string filePath)
{
if (!IsValidBackupFilePath(filePath))
throw new ArgumentException("Invalid backup file path.");

var connectionString = CreateConnectionStringBuilder(config);

await using var connection = new SqlConnection(BuildConnectionString(config, false));
await using var command = CreateCommand($"RESTORE DATABASE [{connectionString.InitialCatalog}] FROM DISK = N'{filePath}' WITH REPLACE",
connection);

Logger.Log(LogLevel.Debug, "Starting to restore database with 'RESTORE DATABASE' from: {filePath}", filePath);

await connection.OpenAsync();
await command.ExecuteNonQueryAsync();
}

/// <inheritdoc />
public override DbContextOptions BuildDbContextOptions(IDatabaseConfig config)
{
var builder = new DbContextOptionsBuilder();
builder.UseSqlServer(BuildConnectionString(config, true));

return builder.Options;
}

private static string BuildConnectionString(IDatabaseConfig config, bool includeModel)
{
if (!IsValidDatabaseName(config.ConnectionSettings.Database))
throw new ArgumentException("Invalid database name.");

var builder = CreateConnectionStringBuilder(config, includeModel);
builder.PersistSecurityInfo = true;

return builder.ToString();
}

/// <inheritdoc />
protected override DbContext CreateMigrationContext(IDatabaseConfig config)
{
var migrationAssemblyType = FindMigrationAssemblyType(typeof(SqlServerDatabaseContextAttribute));

var builder = new DbContextOptionsBuilder();
builder.UseSqlServer(
BuildConnectionString(config, true),
x => x.MigrationsAssembly(migrationAssemblyType.Assembly.FullName));

return CreateContext(migrationAssemblyType, builder.Options);
}

private static bool IsValidDatabaseName(string dbName)
{
// Avoid sql injection by validating the database name
if (string.IsNullOrWhiteSpace(dbName) || dbName.Length > 128)
return false;

// Only allow letters, numbers, and underscores
return Regex.IsMatch(dbName, @"^[A-Za-z0-9_]+$");
}

private static bool IsValidBackupFilePath(string filePath)
{
// Disallow dangerous characters
var invalidStrings = new[] { ";", "'", "\"", "--" };
return invalidStrings.All(s => !filePath.Contains(s));
}
}
22 changes: 11 additions & 11 deletions src/Moryx.Model.Sqlite/SqliteDatabaseConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,23 @@ public SqliteDatabaseConfig()
ConfiguratorTypename = typeof(SqliteModelConfigurator).AssemblyQualifiedName;
}
}


internal class DefaultSqliteConnectionStringAttribute : DefaultValueAttribute
{
public DefaultSqliteConnectionStringAttribute() : base("")
{
var path = Path.Combine(".", "db", "<DatabaseName>.db");
SetValue($"Data Source={path};Mode=ReadWrite;");
}
}

/// <summary>
/// Database connection settings for the Sqlite databases
/// </summary>
public class SqliteDatabaseConnectionSettings : DatabaseConnectionSettings
{
private string _database;

/// <summary>
/// Default constructor of <see cref="SqliteDatabaseConnectionSettings"/>
/// </summary>
public SqliteDatabaseConnectionSettings()
{
var defaultDbPath = Path.Combine(".", "db", "<DatabaseName>.db");
ConnectionString = $"Data Source={defaultDbPath};Mode=ReadWrite;";
}

/// <inheritdoc />
[DataMember]
public override string Database
Expand All @@ -56,7 +56,7 @@ public override string Database
}

/// <inheritdoc />
[DataMember, Required]
[DataMember, Required, DefaultSqliteConnectionString]
public override string ConnectionString { get; set; }

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@ namespace Moryx.Model.Attributes
/// Attribute to identify database specific contexts
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class DatabaseSpecificContextAttribute : Attribute { }
public class DatabaseSpecificContextAttribute : Attribute
{

}
}
2 changes: 1 addition & 1 deletion src/Moryx.Model/Configuration/ModelConfiguratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ private async Task<bool> TestDatabaseConnection(IDatabaseConfig config)
await conn.OpenAsync();
return true;
}
catch(Exception)
catch(Exception e)
{
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@
<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsPackable>true</IsPackable>
<IsPackable>true</IsPackable>
<Description>ResourceManagement module composing and maintaining the resource graph as the habitat for digital twins of manufacturing assets.</Description>
<PackageTags>MORYX;IIoT;IoT;Manufacturing;API;Resource</PackageTags>
<IsPackable>true</IsPackable>
<IsPackable>true</IsPackable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Moryx.AbstractionLayer\Moryx.AbstractionLayer.csproj" />
<ProjectReference Include="..\Moryx.Model.Sqlite\Moryx.Model.Sqlite.csproj" />
<ProjectReference Include="..\Moryx.Notifications\Moryx.Notifications.csproj" />
<ProjectReference Include="..\Moryx.Runtime\Moryx.Runtime.csproj" />
<ProjectReference Include="..\Moryx.Model.PostgreSQL\Moryx.Model.PostgreSQL.csproj" />
<ProjectReference Include="..\Moryx.Runtime\Moryx.Runtime.csproj" />
<ProjectReference Include="..\Moryx.Model.PostgreSQL\Moryx.Model.PostgreSQL.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading
Loading