From 01db1c6b230376f3078cf6e5578fbb5deac1343a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96zt=C3=BCrk?= Date: Thu, 6 Nov 2025 23:33:22 +0300 Subject: [PATCH] =?UTF-8?q?DapperTransaction=20g=C3=BCncellemesi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hatalar vermeye başladı. Listelerdeki veriler yüklenmeden yeni butonuna basınca hata aldım. --- .../appsettings.Dev.json | 2 +- .../appsettings.Production.json | 2 +- .../Kurs.Platform.DbMigrator/appsettings.json | 2 +- .../DynamicData/DapperTransactionApi.cs | 92 ++--- .../DynamicData/MsDynamicDataRepository.cs | 364 +++++++++--------- .../DynamicData/PgDynamicDataRepository.cs | 173 ++++++--- .../Tenants/Seeds/TenantData.json | 98 ++++- .../Tenants/TenantDataSeeder.cs | 37 +- .../Tenants/TenantSeederDto.cs | 26 +- .../appsettings.Dev.json | 2 +- .../appsettings.Production.json | 2 +- .../appsettings.json | 2 +- .../TenantsConnectionString.tsx | 2 +- 13 files changed, 513 insertions(+), 291 deletions(-) diff --git a/api/src/Kurs.Platform.DbMigrator/appsettings.Dev.json b/api/src/Kurs.Platform.DbMigrator/appsettings.Dev.json index 9a144145..59cf69df 100644 --- a/api/src/Kurs.Platform.DbMigrator/appsettings.Dev.json +++ b/api/src/Kurs.Platform.DbMigrator/appsettings.Dev.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "SqlServer": "Server=sql;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;MultipleActiveResultSets=true;", + "SqlServer": "Server=sql;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;", "PostgreSql": "User ID=sa;Password=NvQp8s@l;Host=postgres;Port=5432;Database=Erp;" }, "Redis": { diff --git a/api/src/Kurs.Platform.DbMigrator/appsettings.Production.json b/api/src/Kurs.Platform.DbMigrator/appsettings.Production.json index 212d8083..2505fc40 100644 --- a/api/src/Kurs.Platform.DbMigrator/appsettings.Production.json +++ b/api/src/Kurs.Platform.DbMigrator/appsettings.Production.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "SqlServer": "Server=sql;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;MultipleActiveResultSets=true;", + "SqlServer": "Server=sql;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;", "PostgreSql": "User ID=sa;Password=NvQp8s@l;Host=postgres;Port=5432;Database=Erp;" }, "Redis": { diff --git a/api/src/Kurs.Platform.DbMigrator/appsettings.json b/api/src/Kurs.Platform.DbMigrator/appsettings.json index e2ee705e..9aff8e97 100644 --- a/api/src/Kurs.Platform.DbMigrator/appsettings.json +++ b/api/src/Kurs.Platform.DbMigrator/appsettings.json @@ -1,7 +1,7 @@ { "Seed": false, "ConnectionStrings": { - "SqlServer": "Server=localhost;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;MultipleActiveResultSets=true;", + "SqlServer": "Server=localhost;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;", "PostgreSql": "User ID=sa;Password=NvQp8s@l;Host=localhost;Port=5432;Database=Erp;" }, "Redis": { diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/DynamicData/DapperTransactionApi.cs b/api/src/Kurs.Platform.EntityFrameworkCore/DynamicData/DapperTransactionApi.cs index 390d946f..e630dacb 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/DynamicData/DapperTransactionApi.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/DynamicData/DapperTransactionApi.cs @@ -1,4 +1,5 @@ -using System.Data; +using System; +using System.Data; using System.Data.Common; using System.Threading; using System.Threading.Tasks; @@ -25,77 +26,76 @@ public class DapperTransactionApi : ITransactionApi, ISupportsRollback public async Task CommitAsync(CancellationToken cancellationToken = default) { - // Check if transaction is still active if (_isCompleted) { - return; // Already completed, nothing to do + return; } - // Check if connection is still open and transaction is not disposed if (DbTransaction?.Connection == null || DbTransaction.Connection.State != ConnectionState.Open) { _isCompleted = true; - return; // Connection closed or transaction disposed + return; } try { await DbTransaction.CommitAsync(CancellationTokenProvider.FallbackToProvider(cancellationToken)); - _isCompleted = true; } - catch (System.InvalidOperationException) + catch (InvalidOperationException) { // Transaction already completed or disposed + } + finally + { + _isCompleted = true; + } + } + + public async Task RollbackAsync(CancellationToken cancellationToken = default) + { + if (_isCompleted) + { + return; + } + + if (DbTransaction?.Connection == null || DbTransaction.Connection.State != ConnectionState.Open) + { + _isCompleted = true; + return; + } + + try + { + await DbTransaction.RollbackAsync(CancellationTokenProvider.FallbackToProvider(cancellationToken)); + } + catch (InvalidOperationException) + { + // Transaction already completed or disposed + } + finally + { _isCompleted = true; } } public void Dispose() { - if (!_isCompleted && DbTransaction?.Connection != null) - { - try - { - // If not completed, rollback before disposing - DbTransaction?.Rollback(); - } - catch - { - // Ignore rollback errors during disposal - } - finally - { - _isCompleted = true; - } - } - - DbTransaction?.Dispose(); - } - - public async Task RollbackAsync(CancellationToken cancellationToken) - { - // Check if transaction is still active - if (_isCompleted) - { - return; // Already completed, nothing to do - } - - // Check if connection is still open and transaction is not disposed - if (DbTransaction?.Connection == null || DbTransaction.Connection.State != ConnectionState.Open) - { - _isCompleted = true; - return; // Connection closed or transaction disposed - } - try { - await DbTransaction.RollbackAsync(CancellationTokenProvider.FallbackToProvider(cancellationToken)); - _isCompleted = true; + if (!_isCompleted && DbTransaction?.Connection?.State == ConnectionState.Open) + { + // UoW commit/rollback çağırmadıysa, dispose sırasında rollback dene + DbTransaction.Rollback(); + } } - catch (System.InvalidOperationException) + catch + { + // Rollback sırasında hata olursa yut + } + finally { - // Transaction already completed or disposed _isCompleted = true; + DbTransaction?.Dispose(); } } } diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/DynamicData/MsDynamicDataRepository.cs b/api/src/Kurs.Platform.EntityFrameworkCore/DynamicData/MsDynamicDataRepository.cs index 673d20e6..859111da 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/DynamicData/MsDynamicDataRepository.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/DynamicData/MsDynamicDataRepository.cs @@ -15,189 +15,183 @@ namespace Kurs.Platform.Domain.DynamicData; [ExposeKeyedService("Ms")] public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency, IUnitOfWorkEnabled, IDisposable { - private readonly IUnitOfWorkManager unitOfWorkManager; - private readonly ICancellationTokenProvider cancellationTokenProvider; - private readonly Dictionary transactions; - private readonly Dictionary connections; - private readonly HashSet registeredTransactions; // Track registered transactions + private readonly IUnitOfWorkManager _unitOfWorkManager; + private readonly ICancellationTokenProvider _cancellationTokenProvider; + + private readonly Dictionary _transactions; + private readonly Dictionary _connections; + private readonly HashSet _registeredTransactions; + private readonly HashSet _registeredConnections; private readonly object _lock = new object(); + public bool IsDisposed { get; private set; } public MsDynamicDataRepository( IUnitOfWorkManager unitOfWorkManager, ICancellationTokenProvider cancellationTokenProvider) { - this.unitOfWorkManager = unitOfWorkManager; - this.cancellationTokenProvider = cancellationTokenProvider; - transactions = new Dictionary(); - connections = new Dictionary(); - registeredTransactions = new HashSet(); + _unitOfWorkManager = unitOfWorkManager; + _cancellationTokenProvider = cancellationTokenProvider; + + _transactions = []; + _connections = []; + _registeredTransactions = []; + _registeredConnections = []; } - private async Task GetOrCreateTransactionAsync(SqlConnection con) + private string BuildKey(string cs) { - var key = $"Dapper_{con.ConnectionString}"; - - lock (_lock) - { - // Check if we have a valid transaction for this connection - if (transactions.TryGetValue(key, out var transaction)) - { - // Validate transaction is still usable - if (transaction?.Connection != null && - transaction.Connection == con && - transaction.Connection.State == ConnectionState.Open) - { - return transaction; - } - - // Invalid transaction, remove it - try { transaction?.Dispose(); } catch { } - transactions.Remove(key); - } - } - - // Ensure connection is open - if (con.State != ConnectionState.Open) - { - await con.OpenAsync(); - } - - // Create new transaction - var newTransaction = await con.BeginTransactionAsync(); - bool shouldRegister = false; - - lock (_lock) - { - transactions[key] = newTransaction; - - // Only register with UnitOfWork once per transaction key - if (!registeredTransactions.Contains(key)) - { - registeredTransactions.Add(key); - shouldRegister = true; - } - } - - // Register with UnitOfWork if available and not already registered - if (shouldRegister && unitOfWorkManager.Current != null) - { - unitOfWorkManager.Current.AddTransactionApi(key, new DapperTransactionApi(newTransaction, cancellationTokenProvider)); - - unitOfWorkManager.Current.OnCompleted(() => - { - lock (_lock) - { - transactions.Remove(key); - registeredTransactions.Remove(key); - } - return Task.CompletedTask; - }); - } - - return newTransaction; + var uowId = _unitOfWorkManager.Current?.GetHashCode() ?? 0; + return $"Dapper_{uowId}_{cs}"; } private async Task GetOrCreateConnectionAsync(string cs) { + var key = BuildKey(cs); SqlConnection connection; - var key = $"Dapper_{cs}"; - + lock (_lock) { - // Check if we have an existing connection - if (connections.TryGetValue(key, out connection)) + if (_connections.TryGetValue(key, out connection)) { - // Connection exists, check its state - if (connection.State == ConnectionState.Open) - { - return connection; - } - - // Connection is not open, will handle outside lock + // varsa aynı connection'ı kullan } else { - // Create new connection connection = new SqlConnection(cs); - connections[key] = connection; + _connections[key] = connection; } } - - // Handle connection state outside of lock - try + + // Lock dışında state yönetimi + if (connection.State == ConnectionState.Broken) { - if (connection.State == ConnectionState.Broken) - { - connection.Close(); - } - - if (connection.State == ConnectionState.Closed) - { - await connection.OpenAsync(); - } + connection.Close(); } - catch + + if (connection.State == ConnectionState.Closed) + { + await connection.OpenAsync(_cancellationTokenProvider.FallbackToProvider(default)); + } + + // UoW tamamlandığında connection'ı kapatmak için tek seferlik kayıt + if (_unitOfWorkManager.Current != null) { - // If connection failed, create a new one lock (_lock) { - connections.Remove(key); - } - - connection = new SqlConnection(cs); - lock (_lock) - { - connections[key] = connection; - } - await connection.OpenAsync(); - } - - // Register cleanup on UnitOfWork completion (only once) - if (unitOfWorkManager.Current != null) - { - unitOfWorkManager.Current.OnCompleted(async () => - { - SqlConnection conn = null; - lock (_lock) + if (!_registeredConnections.Contains(key)) { - if (connections.TryGetValue(key, out conn)) + _registeredConnections.Add(key); + + _unitOfWorkManager.Current.OnCompleted(async () => { - connections.Remove(key); - } - } - - if (conn != null) - { - try - { - if (conn.State != ConnectionState.Closed) + SqlConnection conn = null; + lock (_lock) { - await conn.CloseAsync(); + if (_connections.TryGetValue(key, out conn)) + { + _connections.Remove(key); + } + + _registeredConnections.Remove(key); } - conn.Dispose(); - } - catch { } + + if (conn != null) + { + try + { + if (conn.State != ConnectionState.Closed) + { + await conn.CloseAsync(); + } + conn.Dispose(); + } + catch + { + // ignore + } + } + }); } - }); + } } - + return connection; } + private async Task GetOrCreateTransactionAsync(SqlConnection con, string cs) + { + var key = BuildKey(cs); + + lock (_lock) + { + if (_transactions.TryGetValue(key, out var existing)) + { + if (existing?.Connection != null && + existing.Connection == con && + existing.Connection.State == ConnectionState.Open) + { + return existing; + } + + try { existing?.Dispose(); } catch { } + _transactions.Remove(key); + } + } + + var newTransaction = await con.BeginTransactionAsync(_cancellationTokenProvider.FallbackToProvider(default)); + bool shouldRegister = false; + + lock (_lock) + { + _transactions[key] = newTransaction; + + if (!_registeredTransactions.Contains(key)) + { + _registeredTransactions.Add(key); + shouldRegister = true; + } + } + + if (shouldRegister && _unitOfWorkManager.Current != null) + { + _unitOfWorkManager.Current.AddTransactionApi( + key, + new DapperTransactionApi(newTransaction, _cancellationTokenProvider) + ); + + _unitOfWorkManager.Current.OnCompleted(() => + { + lock (_lock) + { + _transactions.Remove(key); + _registeredTransactions.Remove(key); + } + return Task.CompletedTask; + }); + } + + return newTransaction; + } + + // ------------------ Dapper metotları ------------------ + public virtual async Task> QueryAsync(string sql, string cs, Dictionary parameters = null) { var param = new DynamicParameters(parameters); var dbConnection = await GetOrCreateConnectionAsync(cs); - var transaction = await GetOrCreateTransactionAsync(dbConnection); - return (await dbConnection.QueryAsync(sql, param, transaction)).AsList(); + var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); + + var result = await dbConnection.QueryAsync(sql, param, transaction); + return result.AsList(); // buffered => reader kapanır, MARS gerekmez } public virtual async Task> QueryAsync(string sql, string cs, Dictionary parameters = null) { var param = new DynamicParameters(parameters); var dbConnection = await GetOrCreateConnectionAsync(cs); - var transaction = await GetOrCreateTransactionAsync(dbConnection); + var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); + return await dbConnection.QueryAsync(sql, param, transaction); } @@ -205,7 +199,8 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency { var param = new DynamicParameters(parameters); var dbConnection = await GetOrCreateConnectionAsync(cs); - var transaction = await GetOrCreateTransactionAsync(dbConnection); + var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); + return await dbConnection.QuerySingleAsync(sql, param, transaction); } @@ -213,25 +208,30 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency { var param = new DynamicParameters(parameters); var dbConnection = await GetOrCreateConnectionAsync(cs); - var transaction = await GetOrCreateTransactionAsync(dbConnection); + var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); + return await dbConnection.ExecuteScalarAsync(sql, param, transaction); } public virtual async Task GetSingleField(string cs, string tableName, string id, string selectFieldName, string idFieldName = "Id") { var param = new { ID = id }; - string sql = $"select {selectFieldName} from {tableName} where {idFieldName} = @ID "; + var sql = $"select {selectFieldName} from {tableName} where {idFieldName} = @ID "; + var dbConnection = await GetOrCreateConnectionAsync(cs); - var transaction = await GetOrCreateTransactionAsync(dbConnection); + var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); + return await dbConnection.QueryFirstAsync(sql, param, transaction); } public virtual async Task UpdateField(string cs, string tableName, string id, string updateFieldName, string value, string idFieldName = "Id") { var param = new { ID = id }; - string sql = $"update {tableName} set {updateFieldName} = '{value}' where Id = @ID "; + var sql = $"update {tableName} set {updateFieldName} = '{value}' where {idFieldName} = @ID "; + var dbConnection = await GetOrCreateConnectionAsync(cs); - var transaction = await GetOrCreateTransactionAsync(dbConnection); + var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); + return await dbConnection.ExecuteAsync(sql, param, transaction); } @@ -239,10 +239,13 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency { var param = new DynamicParameters(parameters); var dbConnection = await GetOrCreateConnectionAsync(cs); - var transaction = await GetOrCreateTransactionAsync(dbConnection); + var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); + return await dbConnection.ExecuteAsync(sql, param, transaction); } + // ------------------ Dispose ------------------ + public void Dispose() { Dispose(true); @@ -251,49 +254,42 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency private void Dispose(bool disposing) { - if (!IsDisposed) + if (IsDisposed) { - if (disposing) - { - lock (_lock) - { - // Dispose transactions first - foreach (var transaction in transactions.Values) - { - try - { - transaction?.Dispose(); - } - catch - { - // Ignore disposal errors - } - } - transactions.Clear(); - - // Then dispose connections - foreach (var connection in connections.Values) - { - try - { - if (connection != null) - { - if (connection.State == ConnectionState.Open) - { - connection.Close(); - } - connection.Dispose(); - } - } - catch - { - // Ignore disposal errors - } - } - connections.Clear(); - } - } - IsDisposed = true; + return; } + + if (disposing) + { + lock (_lock) + { + foreach (var tx in _transactions.Values) + { + try { tx?.Dispose(); } catch { } + } + _transactions.Clear(); + _registeredTransactions.Clear(); + + foreach (var conn in _connections.Values) + { + try + { + if (conn != null) + { + if (conn.State != ConnectionState.Closed) + { + conn.Close(); + } + conn.Dispose(); + } + } + catch { } + } + _connections.Clear(); + _registeredConnections.Clear(); + } + } + + IsDisposed = true; } } diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/DynamicData/PgDynamicDataRepository.cs b/api/src/Kurs.Platform.EntityFrameworkCore/DynamicData/PgDynamicDataRepository.cs index 1437b077..43688f50 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/DynamicData/PgDynamicDataRepository.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/DynamicData/PgDynamicDataRepository.cs @@ -15,69 +15,130 @@ namespace Kurs.Platform.Domain.DynamicData; [ExposeKeyedService("Pg")] public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency, IUnitOfWorkEnabled, IDisposable { - private readonly IUnitOfWorkManager unitOfWorkManager; - private readonly ICancellationTokenProvider cancellationTokenProvider; - private Dictionary transactions; - private Dictionary connections; + private readonly IUnitOfWorkManager _unitOfWorkManager; + private readonly ICancellationTokenProvider _cancellationTokenProvider; + + private readonly Dictionary _transactions; + private readonly Dictionary _connections; public bool IsDisposed { get; private set; } public PgDynamicDataRepository( IUnitOfWorkManager unitOfWorkManager, ICancellationTokenProvider cancellationTokenProvider) { - this.unitOfWorkManager = unitOfWorkManager; - this.cancellationTokenProvider = cancellationTokenProvider; - transactions = []; - connections = []; + _unitOfWorkManager = unitOfWorkManager; + _cancellationTokenProvider = cancellationTokenProvider; + _transactions = []; + _connections = []; } - private async Task GetOrCreateTransactionAsync(NpgsqlConnection con) + private string BuildKey(string cs) { - var key = $"Dapper_{con.ConnectionString}"; - var transaction = transactions.GetOrDefault(key); - if (transaction == null || transaction.Connection == null) - { - transaction = await con.BeginTransactionAsync(); - unitOfWorkManager.Current.AddTransactionApi(key, new DapperTransactionApi(transaction, cancellationTokenProvider)); - transactions.Add(key, transaction); - unitOfWorkManager.Current.OnCompleted(() => - { - transaction = null; - return Task.CompletedTask; - }); - } - return transaction; + var uowId = _unitOfWorkManager.Current?.GetHashCode() ?? 0; + return $"Dapper_{uowId}_{cs}"; } private async Task GetOrCreateConnectionAsync(string cs) { - var key = $"Dapper_{cs}"; - var connection = connections.GetOrDefault(key); - if (connection == null) + var key = BuildKey(cs); + if (!_connections.TryGetValue(key, out var connection)) { connection = new NpgsqlConnection(cs); - connections.Add(key, connection); + _connections[key] = connection; } + + if (connection.State == ConnectionState.Broken) + { + connection.Close(); + } + if (connection.State != ConnectionState.Open) { - await connection.OpenAsync(); + await connection.OpenAsync(_cancellationTokenProvider.FallbackToProvider(default)); } + + if (_unitOfWorkManager.Current != null) + { + _unitOfWorkManager.Current.OnCompleted(async () => + { + if (_connections.TryGetValue(key, out var conn)) + { + _connections.Remove(key); + + try + { + if (conn.State != ConnectionState.Closed) + { + await conn.CloseAsync(); + } + conn.Dispose(); + } + catch + { + // ignore + } + } + }); + } + return connection; } + private async Task GetOrCreateTransactionAsync(NpgsqlConnection con, string cs) + { + var key = BuildKey(cs); + + if (_transactions.TryGetValue(key, out var tx)) + { + if (tx?.Connection != null && + tx.Connection == con && + tx.Connection.State == ConnectionState.Open) + { + return tx; + } + + try { tx?.Dispose(); } catch { } + _transactions.Remove(key); + } + + var newTx = await con.BeginTransactionAsync(_cancellationTokenProvider.FallbackToProvider(default)); + _transactions[key] = newTx; + + if (_unitOfWorkManager.Current != null) + { + _unitOfWorkManager.Current.AddTransactionApi( + key, + new DapperTransactionApi(newTx, _cancellationTokenProvider) + ); + + _unitOfWorkManager.Current.OnCompleted(() => + { + _transactions.Remove(key); + return Task.CompletedTask; + }); + } + + return newTx; + } + + // ------------------ Dapper metotları ------------------ + public virtual async Task> QueryAsync(string sql, string cs, Dictionary parameters = null) { var param = new DynamicParameters(parameters); var dbConnection = await GetOrCreateConnectionAsync(cs); - var transaction = await GetOrCreateTransactionAsync(dbConnection); - return (await dbConnection.QueryAsync(sql, param, transaction)).AsList(); + var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); + + var result = await dbConnection.QueryAsync(sql, param, transaction); + return result.AsList(); } public virtual async Task> QueryAsync(string sql, string cs, Dictionary parameters = null) { var param = new DynamicParameters(parameters); var dbConnection = await GetOrCreateConnectionAsync(cs); - var transaction = await GetOrCreateTransactionAsync(dbConnection); + var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); + return await dbConnection.QueryAsync(sql, param, transaction); } @@ -85,7 +146,8 @@ public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency { var param = new DynamicParameters(parameters); var dbConnection = await GetOrCreateConnectionAsync(cs); - var transaction = await GetOrCreateTransactionAsync(dbConnection); + var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); + return await dbConnection.QuerySingleAsync(sql, param, transaction); } @@ -93,25 +155,30 @@ public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency { var param = new DynamicParameters(parameters); var dbConnection = await GetOrCreateConnectionAsync(cs); - var transaction = await GetOrCreateTransactionAsync(dbConnection); + var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); + return await dbConnection.ExecuteScalarAsync(sql, param, transaction); } public virtual async Task GetSingleField(string cs, string tableName, string id, string selectFieldName, string idFieldName = "Id") { var param = new { ID = id }; - string sql = $"select {selectFieldName} from {tableName} where {idFieldName} = @ID "; + var sql = $"select {selectFieldName} from {tableName} where {idFieldName} = @ID "; + var dbConnection = await GetOrCreateConnectionAsync(cs); - var transaction = await GetOrCreateTransactionAsync(dbConnection); + var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); + return await dbConnection.QueryFirstAsync(sql, param, transaction); } public virtual async Task UpdateField(string cs, string tableName, string id, string updateFieldName, string value, string idFieldName = "Id") { var param = new { ID = id }; - string sql = $"update {tableName} set {updateFieldName} = '{value}' where Id = @ID "; + var sql = $"update {tableName} set {updateFieldName} = '{value}' where {idFieldName} = @ID "; + var dbConnection = await GetOrCreateConnectionAsync(cs); - var transaction = await GetOrCreateTransactionAsync(dbConnection); + var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); + return await dbConnection.ExecuteAsync(sql, param, transaction); } @@ -119,10 +186,13 @@ public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency { var param = new DynamicParameters(parameters); var dbConnection = await GetOrCreateConnectionAsync(cs); - var transaction = await GetOrCreateTransactionAsync(dbConnection); + var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); + return await dbConnection.ExecuteAsync(sql, param, transaction); } + // ------------------ Dispose ------------------ + public void Dispose() { Dispose(true); @@ -131,23 +201,40 @@ public class PgDynamicDataRepository : IDynamicDataRepository, IScopedDependency private void Dispose(bool disposing) { - if (!IsDisposed) + if (IsDisposed) { - if (disposing) + return; + } + + if (disposing) + { + foreach (var tx in _transactions.Values) { - foreach (var connection in connections.Values) + try { tx?.Dispose(); } catch { } + } + _transactions.Clear(); + + foreach (var connection in _connections.Values) + { + try { if (connection != null) { - if (connection.State == ConnectionState.Open) + if (connection.State != ConnectionState.Closed) { connection.Close(); } connection.Dispose(); } } + catch + { + // ignore + } } - IsDisposed = true; + _connections.Clear(); } + + IsDisposed = true; } } diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/Seeds/TenantData.json b/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/Seeds/TenantData.json index 3bd33fba..604d29c1 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/Seeds/TenantData.json +++ b/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/Seeds/TenantData.json @@ -100,7 +100,9 @@ "props": null, "description": null, "isActive": true, - "dependencies": ["DynamicEntityComponent"] + "dependencies": [ + "DynamicEntityComponent" + ] } ], "ReportCategories": [ @@ -2406,7 +2408,12 @@ "minSalary": 80000, "maxSalary": 120000, "currencyCode": "USD", - "requiredSkills": ["JavaScript", "TypeScript", "React", "Node.js"], + "requiredSkills": [ + "JavaScript", + "TypeScript", + "React", + "Node.js" + ], "responsibilities": [ "Develop frontend and backend applications", "Write clean and maintainable code", @@ -4101,7 +4108,9 @@ { "postContent": "CI/CD pipeline güncellememiz tamamlandı! Deployment süremiz %40 azaldı. Otomasyonun gücü 💪", "type": "video", - "urls": ["https://www.w3schools.com/html/mov_bbb.mp4"] + "urls": [ + "https://www.w3schools.com/html/mov_bbb.mp4" + ] } ], "SocialPollOptions": [ @@ -4250,11 +4259,88 @@ "parentGroupCode": "METAL", "isActive": true }, -{ + { "code": "YARI-MAMUL", "name": "Yarı Mamuller", "parentGroupCode": "MAMUL", "isActive": true - } + } + ], + "Materials": [ + { + "code": "MT001", + "name": "Yüksek Kaliteli Çelik Levha 10mm", + "barcode": "1234567890123", + "description": "Çelik Levha 2mm", + "materialTypeCode": "RAW", + "materialGroupCode": "METAL", + "uomName": "kg", + "costPrice": 15.5, + "salesPrice": 18.75, + "currencyCode": "TRY", + "isActive": true, + "totalStock": 2500.0, + "trackingType": "Quantity" + }, + { + "code": "MT002", + "name": "Alüminyum Profil 40x40", + "barcode": "1234567890124", + "description": "Alüminyum Profil 40x40", + "materialTypeCode": "SEMI", + "materialGroupCode": "METAL", + "uomName": "Adet", + "costPrice": 45.0, + "salesPrice": 55.0, + "currencyCode": "TRY", + "isActive": true, + "totalStock": 1200.0, + "trackingType": "Lot" + }, + { + "code": "PR001", + "name": "Montajlı Motor Grubu A-Type", + "barcode": "1234567890125", + "description": "Motor Grubu A-Type", + "materialTypeCode": "FINISHED", + "materialGroupCode": "MOTOR", + "uomName": "Adet", + "costPrice": 850.0, + "salesPrice": 1200.0, + "currencyCode": "TRY", + "isActive": true, + "totalStock": 45.0, + "trackingType": "Lot" + }, + { + "code": "SF001", + "name": "Kesme Yağı Premium", + "barcode": "1234567890126", + "description": "Kesme Yağı Premium", + "materialTypeCode": "FINISHED", + "materialGroupCode": "PLASTIK", + "uomName": "Adet", + "costPrice": 25.0, + "salesPrice": 35.0, + "currencyCode": "TRY", + "isActive": true, + "totalStock": 150.0, + "trackingType": "Quantity" + }, + { + "code": "PK001", + "name": "Plastik Kapak Komponenti", + "barcode": "1234567890127", + "description": "Plastik Kapak", + "materialTypeCode": "SEMI", + "materialGroupCode": "KIMYA", + "uomName": "Adet", + "costPrice": 8.5, + "salesPrice": 15.0, + "currencyCode": "TRY", + "isActive": true, + "totalStock": 850.0, + "trackingType": "Serial" + } ] -} +} \ No newline at end of file diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/TenantDataSeeder.cs b/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/TenantDataSeeder.cs index f3400cc0..b1de5b93 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/TenantDataSeeder.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/TenantDataSeeder.cs @@ -94,6 +94,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency private readonly IRepository _socialLikeRepository; private readonly IRepository _materialTypeRepository; private readonly IRepository _materialGroupRepository; + private readonly IRepository _materialRepository; public TenantDataSeeder( IClock clock, @@ -170,7 +171,8 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency IRepository socialCommentRepository, IRepository socialLikeRepository, IRepository materialTypeRepository, - IRepository materialGroupRepository + IRepository materialGroupRepository, + IRepository materialRepository ) { _clock = clock; @@ -249,6 +251,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency _socialLikeRepository = socialLikeRepository; _materialTypeRepository = materialTypeRepository; _materialGroupRepository = materialGroupRepository; + _materialRepository = materialRepository; } private static IConfigurationRoot BuildConfiguration() @@ -1641,5 +1644,37 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency }, autoSave: true); } } + + foreach (var item in items.Materials) + { + if (string.IsNullOrWhiteSpace(item.Code)) + continue; + + var exists = await _materialRepository.AnyAsync(x => x.Code == item.Code); + if (exists) + continue; + + var type = await _materialTypeRepository.FirstOrDefaultAsync(x => x.Code == item.MaterialTypeCode); + var group = await _materialGroupRepository.FirstOrDefaultAsync(x => x.Code == item.MaterialGroupCode); + var currency = await _currencyRepository.FirstOrDefaultAsync(x => x.Code == item.CurrencyCode); + var uom = await _uomRepository.FirstOrDefaultAsync(x => x.Name == item.UomName); + + await _materialRepository.InsertAsync(new Material + { + Code = item.Code, + Name = item.Name, + Description = item.Description, + UomId = uom?.Id, + CostPrice = item.CostPrice, + SalesPrice = item.SalesPrice, + CurrencyId = currency?.Id, + IsActive = item.IsActive, + TotalStock = item.TotalStock, + MaterialTypeId = type?.Id, + MaterialGroupId = group?.Id, + Barcode = item.Barcode, + TrackingType = item.TrackingType + }, autoSave: true); + } } } diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/TenantSeederDto.cs b/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/TenantSeederDto.cs index 7f213f50..b6812458 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/TenantSeederDto.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/TenantSeederDto.cs @@ -52,10 +52,6 @@ public class TenantSeederDto public List Interesting { get; set; } public List Programs { get; set; } - //Supply Chain - public List MaterialTypes { get; set; } - public List MaterialGroups { get; set; } - //Hr public List EmployeeTypes { get; set; } public List JobPositions { get; set; } @@ -83,8 +79,30 @@ public class TenantSeederDto public List SocialPollOptions { get; set; } public List SocialComments { get; set; } public List SocialLikes { get; set; } + + //Supply Chain + public List MaterialTypes { get; set; } + public List MaterialGroups { get; set; } + public List Materials { get; set; } } +public class MaterialSeedDto +{ + public string Code { get; set; } + public string Name { get; set; } + public string Barcode { get; set; } + public string Description { get; set; } + public string MaterialTypeCode { get; set; } + public string MaterialGroupCode { get; set; } + public string UomName { get; set; } + public decimal CostPrice { get; set; } + public decimal SalesPrice { get; set; } + public string CurrencyCode { get; set; } + public bool IsActive { get; set; } + public decimal TotalStock { get; set; } + public string TrackingType { get; set; } +} + public class MaterialTypeSeedDto { public string Code { get; set; } diff --git a/api/src/Kurs.Platform.HttpApi.Host/appsettings.Dev.json b/api/src/Kurs.Platform.HttpApi.Host/appsettings.Dev.json index b776fd35..9946e123 100644 --- a/api/src/Kurs.Platform.HttpApi.Host/appsettings.Dev.json +++ b/api/src/Kurs.Platform.HttpApi.Host/appsettings.Dev.json @@ -9,7 +9,7 @@ "BaseDomain": "sozsoft.com" }, "ConnectionStrings": { - "SqlServer": "Server=sql;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;MultipleActiveResultSets=true;", + "SqlServer": "Server=sql;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;", "PostgreSql": "User ID=sa;Password=NvQp8s@l;Host=postgres;Port=5432;Database=Erp;" }, "Redis": { diff --git a/api/src/Kurs.Platform.HttpApi.Host/appsettings.Production.json b/api/src/Kurs.Platform.HttpApi.Host/appsettings.Production.json index 3a940df4..37e7649e 100644 --- a/api/src/Kurs.Platform.HttpApi.Host/appsettings.Production.json +++ b/api/src/Kurs.Platform.HttpApi.Host/appsettings.Production.json @@ -9,7 +9,7 @@ "BaseDomain": "sozsoft.com" }, "ConnectionStrings": { - "SqlServer": "Server=sql;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;MultipleActiveResultSets=true;", + "SqlServer": "Server=sql;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;", "PostgreSql": "User ID=sa;Password=NvQp8s@l;Host=postgres;Port=5432;Database=Erp;" }, "Redis": { diff --git a/api/src/Kurs.Platform.HttpApi.Host/appsettings.json b/api/src/Kurs.Platform.HttpApi.Host/appsettings.json index 7d3b9938..0aa2fb50 100644 --- a/api/src/Kurs.Platform.HttpApi.Host/appsettings.json +++ b/api/src/Kurs.Platform.HttpApi.Host/appsettings.json @@ -9,7 +9,7 @@ "Version": "1.0.1" }, "ConnectionStrings": { - "SqlServer": "Server=localhost;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;MultipleActiveResultSets=true;", + "SqlServer": "Server=localhost;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;", "PostgreSql": "User ID=sa;Password=NvQp8s@l;Host=localhost;Port=5432;Database=Erp;" }, "Redis": { diff --git a/ui/src/views/admin/tenant-management/TenantsConnectionString.tsx b/ui/src/views/admin/tenant-management/TenantsConnectionString.tsx index 9b57848a..d32790d8 100644 --- a/ui/src/views/admin/tenant-management/TenantsConnectionString.tsx +++ b/ui/src/views/admin/tenant-management/TenantsConnectionString.tsx @@ -279,7 +279,7 @@ function TenantConnectionString({ 'value', 'Server=sql;Database=' + name + - ';User Id=sa;password=@Password;Trusted_Connection=False;TrustServerCertificate=True;Connection Timeout=60;MultipleActiveResultSets=true;', + ';User Id=sa;password=@Password;Trusted_Connection=False;TrustServerCertificate=True;Connection Timeout=60;', ) else if (option?.value == 2) //MsSql