Robert 1 éve
szülő
commit
13bf6d566b

+ 9 - 8
EVCB_OCPP.WSServer/EVCB_OCPP.WSServer.csproj

@@ -41,22 +41,23 @@
   </ItemGroup>
   <ItemGroup>
     <PackageReference Include="Dapper" Version="2.0.123" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.2" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.2" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.5" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.5" />
+    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
     <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="7.0.0" />
     <PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
     <PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
-    <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" />
-    <PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
-    <PackageReference Include="NLog" Version="5.1.1" />
-    <PackageReference Include="NLog.Web.AspNetCore" Version="5.2.1" />
+    <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.18.1" />
+    <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
+    <PackageReference Include="NLog" Version="5.1.4" />
+    <PackageReference Include="NLog.Web.AspNetCore" Version="5.2.3" />
     <PackageReference Include="Polly" Version="7.2.3" />
     <PackageReference Include="Quartz.Extensions.Hosting" Version="3.6.2" />
-    <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
     <PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
-    <PackageReference Include="System.ServiceModel.Federation" Version="4.10.0" />
+    <PackageReference Include="System.ServiceModel.Federation" Version="4.10.2" />
     <PackageReference Include="EntityFramework" Version="6.4.4" />
     <PackageReference Include="log4net" Version="2.0.15" />
+    <PackageReference Include="System.Threading.Tasks.Dataflow" Version="7.0.0" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\SuperWebSocket\SuperWebSocket.csproj" />

+ 2 - 2
EVCB_OCPP.WSServer/Helper/AddPortalDbContext.cs

@@ -19,7 +19,7 @@ public static class AddPortalDbContext
         const string DbPassKey = "MainDbPass";
         const string DbConnectionStringKey = "MainDBContext";
 
-        AddPortalDbContextInternal<MainDBContext>(services,configuration, DbUserIdKey, DbPassKey, DbConnectionStringKey, logToConsole: true);
+        AddPortalDbContextInternal<MainDBContext>(services,configuration, DbUserIdKey, DbPassKey, DbConnectionStringKey, logToConsole: false);
         return services;
     }
 
@@ -29,7 +29,7 @@ public static class AddPortalDbContext
         const string DbPassKey = "MeterValueDbPass";
         const string DbConnectionStringKey = "MeterValueDBContext";
 
-        AddPortalDbContextInternal<MeterValueDBContext>(services, configuration, DbUserIdKey, DbPassKey, DbConnectionStringKey, logToConsole: true);
+        AddPortalDbContextInternal<MeterValueDBContext>(services, configuration, DbUserIdKey, DbPassKey, DbConnectionStringKey, logToConsole: false);
         return services;
     }
 

+ 22 - 10
EVCB_OCPP.WSServer/Helper/GroupSingleHandler.cs

@@ -2,6 +2,7 @@
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
@@ -16,7 +17,7 @@ public class GroupSingleHandler<T>
         this.logger = logger;
 
         //singleWorkLock = new (_WorkerCnt);
-        singleHandleTask = StartHandleTask();
+        //singleHandleTask = StartHandleTask();
 
         _handleTasks = new Task[workerCnt];
         for (int cnt = 0; cnt < workerCnt; cnt++)
@@ -78,32 +79,43 @@ public class GroupSingleHandler<T>
     private Task StartHandleTask()
     {
         return Task.Run(async () => {
-            while (!blockQueue.IsCompleted)
+            while (true)
             {
                 var handleList = new List<(T param, SemaphoreSlim waitLock)>();
                 try
                 {
-                    //若blockQueue沒有資料,Take動作會被Block住,有資料時再往下執行
-                    //若
-                    var  startData = blockQueue.Take();
+                    var startData = blockQueue.Take();
                     handleList.Add(startData);
                 }
-                catch (InvalidOperationException e) {
+                catch (InvalidOperationException e)
+                {
                     logger.LogError(e, "blockQueue.Take Error");
                     break;
                 }
-                while(blockQueue.TryTake(out var data))
+
+                var watch = Stopwatch.StartNew();
+                long t0, t1, t2, t3;
+
+                while (blockQueue.TryTake(out var data))
                 {
                     handleList.Add(data);
                 }
-
-                var task = handleFunc(handleList.Select(x => x.param));
-                await task;
+                t0 = watch.ElapsedMilliseconds;
+                var task = handleFunc(handleList.Select(x => x.param).ToList());
+                t1 = watch.ElapsedMilliseconds;
+                await task.ConfigureAwait(false);
+                t2 = watch.ElapsedMilliseconds;
 
                 foreach (var handled in handleList)
                 {
                     handled.waitLock.Release();
                 }
+                watch.Stop();
+                t3 = watch.ElapsedMilliseconds;
+                if (t3 > 1000)
+                {
+                    logger.LogWarning("StartHandleTask {0}/{1}/{2}/{3}", t0, t1, t2, t3);
+                }
             }
         });
     }

+ 243 - 0
EVCB_OCPP.WSServer/Helper/MeterValueGroupSingleHandler.cs

@@ -0,0 +1,243 @@
+using Dapper;
+using EVCB_OCPP.Domain;
+using EVCB_OCPP.WSServer.Service;
+using Microsoft.Data.SqlClient;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Data;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace EVCB_OCPP.WSServer.Helper;
+
+public class MeterValueGroupSingleHandler
+{
+    public MeterValueGroupSingleHandler(
+        IDbContextFactory<MeterValueDBContext> meterValueDbContextFactory,
+        IConfiguration configuration,
+        ILogger<MeterValueGroupSingleHandler> logger)
+    {
+        this.meterValueDbContextFactory = meterValueDbContextFactory;
+        this.configuration = configuration;
+        this.logger = logger;
+
+        //singleWorkLock = new (_WorkerCnt);
+        //singleHandleTask = StartHandleTask();
+        this.meterValueConnectionString = configuration.GetConnectionString("MeterValueDBContext");
+
+        var workerCnt = 20;
+        _handleTasks = new Task[workerCnt];
+        for (int cnt = 0; cnt < workerCnt; cnt++)
+        {
+            _handleTasks[cnt] = StartHandleTask();
+        }
+    }
+
+    private readonly IDbContextFactory<MeterValueDBContext> meterValueDbContextFactory;
+    private readonly IConfiguration configuration;
+
+    //private readonly Func<IEnumerable<T>, Task> handleFunc;
+    private readonly ILogger logger;
+    private readonly string meterValueConnectionString;
+    private readonly BlockingCollection<(InsertMeterValueParam param, SemaphoreSlim waitLock)> blockQueue = new();
+    private static Queue<string> _existTables = new();
+    //private SemaphoreSlim singleWorkLock;// = new SemaphoreSlim(1);
+    private bool IsStarted = false;
+    private Task singleHandleTask;
+    private Task[] _handleTasks;
+
+    public Task HandleAsync(InsertMeterValueParam param)
+    {
+        IsStarted = true;
+
+        SemaphoreSlim reqLock = new(0);
+        blockQueue.Add((param, reqLock));
+        //TryStartHandler();
+        return reqLock.WaitAsync();
+    }
+
+    //private void TryStartHandler()
+    //{
+    //    if (!singleWorkLock.Wait(0))
+    //    {
+    //        return;
+    //    }
+
+    //    if (waitList.Count == 0)
+    //    {
+    //        singleWorkLock.Release();
+    //        return;
+    //    }
+
+    //    singleHandleTask = StartHandleTask();
+    //}
+
+    private Task StartHandleTask()
+    {
+        return Task.Run(async () => {
+            while (true)
+            {
+                var handleList = new List<(InsertMeterValueParam param, SemaphoreSlim waitLock)>();
+                try
+                {
+                    var startData = blockQueue.Take();
+                    handleList.Add(startData);
+                }
+                catch (InvalidOperationException e)
+                {
+                    logger.LogError(e, "blockQueue.Take Error");
+                    break;
+                }
+
+                var watch = Stopwatch.StartNew();
+                long t0, t1, t2, t3;
+
+                while (blockQueue.TryTake(out var data))
+                {
+                    handleList.Add(data);
+                }
+                t0 = watch.ElapsedMilliseconds;
+                var parms = handleList.Select(x => x.param).ToList();
+                t1 = watch.ElapsedMilliseconds;
+                await BundleInsertWithDapper(parms).ConfigureAwait(false);
+                t2 = watch.ElapsedMilliseconds;
+
+                foreach (var handled in handleList)
+                {
+                    handled.waitLock.Release();
+                }
+                watch.Stop();
+                t3 = watch.ElapsedMilliseconds;
+                if (t3 > 1000)
+                {
+                    logger.LogWarning("MeterValue StartHandleTask {0}/{1}/{2}/{3}", t0, t1, t2, t3);
+                }
+            }
+        });
+    }
+
+    private async Task BundleInsertWithDapper(IEnumerable<InsertMeterValueParam> parms)
+    {
+        var watch = Stopwatch.StartNew();
+        long t0, t1, t2, t3;
+
+        var parmsList = parms.ToList();
+        t0 = watch.ElapsedMilliseconds;
+        foreach (var param in parms)
+        {
+            if (!await GetTableExist(param.createdOn))
+            {
+                await InsertWithStoredProcedure(param);
+                parmsList.Remove(param);
+            }
+            t1 = watch.ElapsedMilliseconds;
+            watch.Stop();
+            if (t1 > 500)
+            {
+                logger.LogWarning("MeterValue InsertWithStoredProcedure {0}/{1}", t0, t1);
+            }
+        }
+
+        t1 = watch.ElapsedMilliseconds;
+        //logger.LogInformation("MeterValue bundle insert cnt {0}", parmsList.Count);
+        var gruopParams = parmsList.GroupBy(x => GetTableName(x.createdOn));
+
+        t2 = watch.ElapsedMilliseconds;
+        foreach (var group in gruopParams)
+        {
+            using SqlConnection sqlConnection = new SqlConnection(meterValueConnectionString);
+            sqlConnection.Open();
+            using var tans = sqlConnection.BeginTransaction();
+
+            var tableName = group.Key;
+            string command = $"""
+                INSERT INTO {tableName} (ConnectorId, Value, CreatedOn, ContextId, FormatId, MeasurandId, PhaseId, LocationId, UnitId, ChargeBoxId, TransactionId)
+                VALUES (@ConnectorId, @Value, @CreatedOn, @ContextId, @FormatId, @MeasurandId, @PhaseId, @LocationId, @UnitId, @ChargeBoxId, @TransactionId);
+                """;
+            foreach (var param in group)
+            {
+                var parameters = new DynamicParameters();
+                parameters.Add("ConnectorId", param.connectorId, DbType.Int16);
+                parameters.Add("Value", param.value, DbType.Decimal, precision: 18, scale: 8);
+                parameters.Add("CreatedOn", param.createdOn, DbType.DateTime);
+                parameters.Add("ContextId", param.contextId, DbType.Int32);
+                parameters.Add("FormatId", param.formatId, DbType.Int32);
+                parameters.Add("MeasurandId", param.measurandId, DbType.Int32);
+                parameters.Add("PhaseId", param.phaseId, DbType.Int32);
+                parameters.Add("LocationId", param.locationId, DbType.Int32);
+                parameters.Add("UnitId", param.unitId, DbType.Int32);
+                parameters.Add("ChargeBoxId", param.chargeBoxId, DbType.String, size: 50);
+                parameters.Add("TransactionId", param.transactionId, DbType.Int32);
+                await sqlConnection.ExecuteAsync(command, parameters, tans);
+            }
+
+            await tans.CommitAsync();
+        }
+
+        watch.Stop();
+        t3 = watch.ElapsedMilliseconds;
+        if (t3 > 400)
+        {
+            logger.LogWarning("MeterValue Dapper {0}/{1}/{2}/{3}", t0, t1, t2, t3);
+        }
+    }
+
+    private async ValueTask<bool> GetTableExist(DateTime tableDateTime)
+    {
+        var tableName = GetTableName(tableDateTime);
+        if (_existTables.Contains(tableName))
+        {
+            return true;
+        }
+
+        FormattableString checkTableSql = $"SELECT Count(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = {tableName}";
+
+        using var db = await meterValueDbContextFactory.CreateDbContextAsync();
+        var resultList = db.Database.SqlQuery<int>(checkTableSql)?.ToList();
+
+        if (resultList is not null && resultList.Count > 0 && resultList[0] > 0)
+        {
+            _existTables.Enqueue(tableName);
+            if (_existTables.Count > 30)
+            {
+                _existTables.TryDequeue(out _);
+            }
+            return true;
+        }
+
+        return false;
+    }
+
+    private async Task InsertWithStoredProcedure(InsertMeterValueParam param)
+    {
+        using var db = await meterValueDbContextFactory.CreateDbContextAsync();
+
+        string sp = "[dbo].[uspInsertMeterValueRecord] @ChargeBoxId," +
+"@ConnectorId,@Value,@CreatedOn,@ContextId,@FormatId,@MeasurandId,@PhaseId,@LocationId,@UnitId,@TransactionId";
+
+        List<SqlParameter> parameter = new List<SqlParameter>();
+        parameter.AddInsertMeterValueRecordSqlParameters(
+            chargeBoxId: param.chargeBoxId
+            , connectorId: (byte)param.connectorId
+            , value: param.value
+            , createdOn: param.createdOn
+            , contextId: param.contextId
+            , formatId: param.formatId
+            , measurandId: param.measurandId
+            , phaseId: param.phaseId
+            , locationId: param.locationId
+            , unitId: param.unitId
+            , transactionId: param.transactionId);
+
+        db.Database.ExecuteSqlRaw(sp, parameter.ToArray());
+    }
+
+    private static string GetTableName(DateTime dateTime)
+        => $"ConnectorMeterValueRecord{dateTime:yyMMdd}";
+}

+ 2 - 0
EVCB_OCPP.WSServer/HostedProtalServer.cs

@@ -20,6 +20,8 @@ namespace EVCB_OCPP.WSServer
     {
         public static void AddProtalServer(this IServiceCollection services, IConfiguration configuration)
         {
+            services.AddMemoryCache();
+
             services.AddPortalServerDatabase(configuration);
             services.AddBusinessServiceFactory();
 

+ 2 - 0
EVCB_OCPP.WSServer/Program.cs

@@ -14,6 +14,7 @@ using NLog.Extensions.Logging;
 using System.IO;
 using System.Data.Common;
 using Microsoft.Data.SqlClient;
+using EVCB_OCPP.WSServer.Helper;
 
 namespace EVCB_OCPP.WSServer
 {
@@ -51,6 +52,7 @@ namespace EVCB_OCPP.WSServer
                 .UseNLog()
                 .ConfigureServices((hostContext, services) =>
                 {
+                    services.AddSingleton<MeterValueGroupSingleHandler>();
                     services.AddProtalServer(hostContext.Configuration);
                 })
                 .Build();

+ 79 - 5
EVCB_OCPP.WSServer/Service/MainDbService.cs

@@ -2,14 +2,18 @@
 using EVCB_OCPP.Domain.Models.Database;
 using EVCB_OCPP.Packet.Messages.Core;
 using EVCB_OCPP.WSServer.Helper;
+using Microsoft.Data.SqlClient;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
+using MongoDB.Driver.Core.Connections;
 using Newtonsoft.Json;
 using OCPPPackage.Profiles;
 using System;
 using System.Collections.Generic;
+using System.Data;
 using System.Linq;
 using System.Text;
 using System.Threading;
@@ -26,7 +30,7 @@ public interface IMainDbService
     Task<string> GetMachineSecurityProfile(string ChargeBoxId);
     Task UpdateMachineBasicInfo(string ChargeBoxId, Machine machine);
     Task AddOCMF(OCMF oCMF);
-    Task<ConnectorStatus> GetConnectorStatus(string ChargeBoxId, int ConnectorId);
+    ValueTask<ConnectorStatus> GetConnectorStatus(string ChargeBoxId, int ConnectorId);
     Task UpdateConnectorStatus(string Id, ConnectorStatus connectorStatus);
     Task AddServerMessage(ServerMessage message);
     Task AddServerMessage(string ChargeBoxId, string OutAction, object OutRequest, string CreatedBy = "", DateTime? CreatedOn = null, string SerialNo = "", string InMessage = "");
@@ -34,11 +38,17 @@ public interface IMainDbService
 
 public class MainDbService : IMainDbService
 {
-    public MainDbService(IDbContextFactory<MainDBContext> contextFactory, IConfiguration configuration, ILoggerFactory loggerFactory)
+    public MainDbService(
+        IDbContextFactory<MainDBContext> contextFactory,
+        IMemoryCache memoryCache,
+        IConfiguration configuration, 
+        ILoggerFactory loggerFactory)
     {
         this.contextFactory = contextFactory;
+        this.memoryCache = memoryCache;
         this.loggerFactory = loggerFactory;
         var startupLimit = GetStartupLimit(configuration);
+        this.connectionString = configuration.GetConnectionString("MainDBContext");
         this.startupSemaphore = new (startupLimit);
 
         var opLimit = GetOpLimit(configuration);
@@ -50,7 +60,9 @@ public class MainDbService : IMainDbService
     }
 
     private readonly IDbContextFactory<MainDBContext> contextFactory;
+    private readonly IMemoryCache memoryCache;
     private readonly ILoggerFactory loggerFactory;
+    private string connectionString;
     private readonly QueueSemaphore startupSemaphore;
     private readonly SemaphoreSlim opSemaphore;
     private GroupSingleHandler<StatusNotificationParam> statusNotificationHandler;
@@ -122,11 +134,18 @@ public class MainDbService : IMainDbService
         await db.SaveChangesAsync();
     }
 
-    public async Task<ConnectorStatus> GetConnectorStatus(string ChargeBoxId, int ConnectorId)
+    public async ValueTask<ConnectorStatus> GetConnectorStatus(string ChargeBoxId, int ConnectorId)
     {
+        var key = $"{ChargeBoxId}{ConnectorId}";
+        if (memoryCache.TryGetValue<ConnectorStatus>(key, out var status))
+        {
+            return status;
+        }
         using var db = contextFactory.CreateDbContext();
-        return await db.ConnectorStatus.Where(x => x.ChargeBoxId == ChargeBoxId
+        var statusFromDb = await db.ConnectorStatus.Where(x => x.ChargeBoxId == ChargeBoxId
                             && x.ConnectorId == ConnectorId).AsNoTracking().FirstOrDefaultAsync();
+        memoryCache.Set(key, statusFromDb);
+        return statusFromDb;
     }
 
     public async Task UpdateConnectorStatus(string Id, ConnectorStatus Status)
@@ -156,6 +175,12 @@ public class MainDbService : IMainDbService
 
         //await db.SaveChangesAsync();
         await statusNotificationHandler.HandleAsync(new StatusNotificationParam(Id, Status));
+        var key = $"{Status.ChargeBoxId}{Status.ConnectorId}";
+        if (memoryCache.TryGetValue<ConnectorStatus>(key, out _))
+        {
+            memoryCache.Remove(key);
+        }
+        memoryCache.Set(key, Status);
         return;
     }
 
@@ -298,7 +323,7 @@ public class MainDbService : IMainDbService
         }
 
         addServerMessageHandler = new GroupSingleHandler<ServerMessage>(
-            handleFunc: BundleAddServerMessage,
+            handleFunc: BulkInsertServerMessage,
             logger: loggerFactory.CreateLogger("AddServerMessageHandler"));
     }
 
@@ -317,6 +342,55 @@ public class MainDbService : IMainDbService
         db.ChangeTracker.Clear();
     }
 
+    private Task BulkInsertServerMessage(IEnumerable<ServerMessage> messages)
+    {
+        var table = new DataTable();
+        table.Columns.Add("ChargeBoxId");
+        table.Columns.Add("SerialNo");
+        table.Columns.Add("OutAction");
+        table.Columns.Add("OutRequest");
+        table.Columns.Add("InMessage");
+        table.Columns.Add("CreatedOn");
+        table.Columns.Add("CreatedBy");
+        table.Columns.Add("UpdatedOn");
+        table.Columns.Add("ReceivedOn");
+
+        foreach (var param in messages)
+        {
+            var row = table.NewRow();
+            row["ChargeBoxId"] = param.ChargeBoxId;
+            row["SerialNo"] = param.SerialNo;
+            row["OutAction"] = param.OutAction;
+            row["OutRequest"] = param.OutRequest;
+            row["InMessage"] = param.InMessage;
+            row["CreatedOn"] = param.CreatedOn;
+            row["CreatedBy"] = param.CreatedBy;
+            row["UpdatedOn"] = param.UpdatedOn;
+            row["ReceivedOn"] = param.ReceivedOn;
+
+            table.Rows.Add(row);
+        }
+
+        using SqlConnection sqlConnection = new SqlConnection(connectionString);
+        sqlConnection.Open();
+        using SqlBulkCopy sqlBulkCopy = new SqlBulkCopy(sqlConnection);
+
+        sqlBulkCopy.BatchSize = messages.Count();
+        sqlBulkCopy.DestinationTableName = "ServerMessage";
+
+        sqlBulkCopy.ColumnMappings.Add("ChargeBoxId", "ChargeBoxId");
+        sqlBulkCopy.ColumnMappings.Add("SerialNo", "SerialNo");
+        sqlBulkCopy.ColumnMappings.Add("OutAction", "OutAction");
+        sqlBulkCopy.ColumnMappings.Add("OutRequest", "OutRequest");
+        sqlBulkCopy.ColumnMappings.Add("InMessage", "InMessage");
+        sqlBulkCopy.ColumnMappings.Add("CreatedOn", "CreatedOn");
+        sqlBulkCopy.ColumnMappings.Add("CreatedBy", "CreatedBy");
+        sqlBulkCopy.ColumnMappings.Add("UpdatedOn", "UpdatedOn");
+        sqlBulkCopy.ColumnMappings.Add("ReceivedOn", "ReceivedOn");
+
+        return sqlBulkCopy.WriteToServerAsync(table);
+    }
+
     private int GetStartupLimit(IConfiguration configuration)
     {
         var limitConfig = configuration["MainDbStartupLimit"];

+ 129 - 4
EVCB_OCPP.WSServer/Service/MeterValueDbService.cs

@@ -1,4 +1,5 @@
-using EVCB_OCPP.Domain;
+using Dapper;
+using EVCB_OCPP.Domain;
 using EVCB_OCPP.Packet.Messages.SubTypes;
 using EVCB_OCPP.WSServer.Helper;
 using Microsoft.Data.SqlClient;
@@ -21,6 +22,7 @@ public class MeterValueDbService
 {
     private readonly IDbContextFactory<MeterValueDBContext> meterValueDbContextFactory;
     private readonly ILoggerFactory loggerFactory;
+    private readonly MeterValueGroupSingleHandler meterValueGroupSingleHandler;
     private readonly QueueSemaphore insertSemaphore;
     private readonly string meterValueConnectionString;
     private readonly ILogger logger;
@@ -31,10 +33,13 @@ public class MeterValueDbService
         IDbContextFactory<MeterValueDBContext> meterValueDbContextFactory, 
         ILogger<MeterValueDbService> logger,
         ILoggerFactory loggerFactory,
-        IConfiguration configuration)
+        IConfiguration configuration
+        //, MeterValueGroupSingleHandler meterValueGroupSingleHandler
+        )
     {
         this.meterValueDbContextFactory = meterValueDbContextFactory;
         this.loggerFactory = loggerFactory;
+        //this.meterValueGroupSingleHandler = meterValueGroupSingleHandler;
         this.meterValueConnectionString = configuration.GetConnectionString("MeterValueDBContext");
         this.logger = logger;
 
@@ -73,6 +78,8 @@ public class MeterValueDbService
         //await db.Database.ExecuteSqlRawAsync(sp, parameter.ToArray());
 
         return insertMeterValueHandler.HandleAsync(param);
+        //return InsertWithDapper(param);
+        //return meterValueGroupSingleHandler.HandleAsync(param);
     }
 
     private void InitInsertMeterValueHandler()
@@ -83,9 +90,10 @@ public class MeterValueDbService
         }
 
         insertMeterValueHandler = new GroupSingleHandler<InsertMeterValueParam>(
-            BulkInsertWithCache,
+            BundleInsertWithDapper,
             //loggerFactory.CreateLogger("InsertMeterValueHandler")
-            logger
+            logger,
+            workerCnt:20
             );
     }
 
@@ -97,6 +105,123 @@ public class MeterValueDbService
         }
     }
 
+    private async Task InsertWithDapper(InsertMeterValueParam param)
+    {
+        var watch = Stopwatch.StartNew();
+        long t0, t1, t2, t3;
+        if (!await GetTableExist(param.createdOn))
+        {
+            t0 = watch.ElapsedMilliseconds;
+            await InsertWithStoredProcedure(param);
+            watch.Stop();
+            t1 = watch.ElapsedMilliseconds;
+            if (t1 > 500)
+            {
+                logger.LogWarning("MeterValue InsertWithStoredProcedure {0}/{1}", t0, t1);
+            }
+            return;
+        }
+
+        t0 = watch.ElapsedMilliseconds;
+        var tableName = GetTableName(param.createdOn);
+        string command = $"""
+            INSERT INTO {tableName} (ConnectorId, Value, CreatedOn, ContextId, FormatId, MeasurandId, PhaseId, LocationId, UnitId, ChargeBoxId, TransactionId)
+            VALUES (@ConnectorId, @Value, @CreatedOn, @ContextId, @FormatId, @MeasurandId, @PhaseId, @LocationId, @UnitId, @ChargeBoxId, @TransactionId);
+            """;
+
+        var parameters = new DynamicParameters();
+        parameters.Add("ConnectorId", param.connectorId, DbType.Int16);
+        parameters.Add("Value", param.value, DbType.Decimal, precision: 18, scale: 8);
+        parameters.Add("CreatedOn", param.createdOn, DbType.DateTime);
+        parameters.Add("ContextId", param.contextId, DbType.Int32);
+        parameters.Add("FormatId", param.formatId, DbType.Int32);
+        parameters.Add("MeasurandId", param.measurandId, DbType.Int32);
+        parameters.Add("PhaseId", param.phaseId, DbType.Int32);
+        parameters.Add("LocationId", param.locationId, DbType.Int32);
+        parameters.Add("UnitId", param.unitId, DbType.Int32);
+        parameters.Add("ChargeBoxId", param.chargeBoxId, DbType.String, size: 50);
+        parameters.Add("TransactionId", param.transactionId, DbType.Int32);
+
+        t1 = watch.ElapsedMilliseconds;
+        using var sqlConnection = new SqlConnection(meterValueConnectionString);
+        t2 = watch.ElapsedMilliseconds;
+        await sqlConnection.ExecuteAsync(command, parameters);
+
+        watch.Stop();
+        t3 = watch.ElapsedMilliseconds;
+        if(t3 > 700)
+        {
+            logger.LogWarning("MeterValue Dapper {0}/{1}/{2}/{3}", t0, t1, t2, t3);
+        }
+    }
+
+    private async Task BundleInsertWithDapper(IEnumerable<InsertMeterValueParam> parms)
+    {
+        var watch = Stopwatch.StartNew();
+        long t0, t1, t2, t3;
+
+        var parmsList = parms.ToList();
+        t0 = watch.ElapsedMilliseconds;
+        foreach (var param in parms)
+        {
+            if (!await GetTableExist(param.createdOn))
+            {
+                await InsertWithStoredProcedure(param);
+                parmsList.Remove(param);
+            }
+            t1 = watch.ElapsedMilliseconds;
+            watch.Stop();
+            if (t1 > 500)
+            {
+                logger.LogWarning("MeterValue InsertWithStoredProcedure {0}/{1}", t0, t1);
+            }
+        }
+
+        t1 = watch.ElapsedMilliseconds;
+        //logger.LogInformation("MeterValue bundle insert cnt {0}", parmsList.Count);
+        var gruopParams = parmsList.GroupBy(x => GetTableName(x.createdOn));
+
+        t2 = watch.ElapsedMilliseconds;
+        using SqlConnection sqlConnection = new SqlConnection(meterValueConnectionString);
+        sqlConnection.Open();
+        using var tans = sqlConnection.BeginTransaction();
+
+        foreach (var group in gruopParams)
+        {
+
+            var tableName = group.Key;
+            string command = $"""
+                INSERT INTO {tableName} (ConnectorId, Value, CreatedOn, ContextId, FormatId, MeasurandId, PhaseId, LocationId, UnitId, ChargeBoxId, TransactionId)
+                VALUES (@ConnectorId, @Value, @CreatedOn, @ContextId, @FormatId, @MeasurandId, @PhaseId, @LocationId, @UnitId, @ChargeBoxId, @TransactionId);
+                """;
+            foreach(var param in group)
+            {
+                var parameters = new DynamicParameters();
+                parameters.Add("ConnectorId", param.connectorId, DbType.Int16);
+                parameters.Add("Value", param.value, DbType.Decimal,precision:18, scale:8);
+                parameters.Add("CreatedOn", param.createdOn, DbType.DateTime);
+                parameters.Add("ContextId", param.contextId, DbType.Int32);
+                parameters.Add("FormatId", param.formatId, DbType.Int32);
+                parameters.Add("MeasurandId", param.measurandId, DbType.Int32);
+                parameters.Add("PhaseId", param.phaseId, DbType.Int32);
+                parameters.Add("LocationId", param.locationId, DbType.Int32);
+                parameters.Add("UnitId", param.unitId, DbType.Int32);
+                parameters.Add("ChargeBoxId", param.chargeBoxId, DbType.String, size:50);
+                parameters.Add("TransactionId", param.transactionId, DbType.Int32);
+                sqlConnection.Execute(command, parameters, tans);
+            }
+        }
+
+        tans.Commit();
+
+        watch.Stop();
+        t3 = watch.ElapsedMilliseconds;
+        if (t3 > 300)
+        {
+            logger.LogWarning("MeterValue Dapper {0}/{1}/{2}/{3}", t0, t1, t2, t3);
+        }
+    }
+
     private async Task BulkInsertWithCache(IEnumerable<InsertMeterValueParam> parms)
     {
         var watcher = Stopwatch.StartNew();

+ 161 - 0
EVCB_OCPP.WSServer/Service/MeterValueInsertHandler.cs

@@ -0,0 +1,161 @@
+using Dapper;
+using EVCB_OCPP.Domain;
+using EVCB_OCPP.WSServer.Helper;
+using Microsoft.Data.SqlClient;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace EVCB_OCPP.WSServer.Service;
+
+public interface IHandler<T>
+{
+    public void Handle(IEnumerable<T> parms);
+}
+
+public class MeterValueInsertHandler : IHandler<InsertMeterValueParam>
+{
+    private static Queue<string> _existTables = new();
+    private readonly IDbContextFactory<MeterValueDBContext> meterValueDbContextFactory;
+    private readonly ILogger<MeterValueInsertHandler> logger;
+    private readonly string meterValueConnectionString;
+
+    public MeterValueInsertHandler(
+        IDbContextFactory<MeterValueDBContext> meterValueDbContextFactory,
+        ILogger<MeterValueInsertHandler> logger,
+        IConfiguration configuration
+        )
+    {
+        this.meterValueDbContextFactory = meterValueDbContextFactory;
+        this.logger = logger;
+        this.meterValueConnectionString = configuration.GetConnectionString("MeterValueDBContext");
+    }
+
+    public void Handle(IEnumerable<InsertMeterValueParam> parms)
+    {
+        //var watch = Stopwatch.StartNew();
+        //long t0, t1, t2, t3;
+
+        var parmsList = parms.ToList();
+        //t0 = watch.ElapsedMilliseconds;
+        foreach (var param in parms)
+        {
+            if (!GetTableExist(param.createdOn).Result)
+            {
+                InsertWithStoredProcedure(param).Wait();
+                parmsList.Remove(param);
+            }
+            //t1 = watch.ElapsedMilliseconds;
+            //watch.Stop();
+            //if (t1 > 500)
+            //{
+            //    logger.LogWarning("MeterValue InsertWithStoredProcedure {0}/{1}", t0, t1);
+            //}
+        }
+
+        //t1 = watch.ElapsedMilliseconds;
+        //logger.LogInformation("MeterValue bundle insert cnt {0}", parmsList.Count);
+        var gruopParams = parmsList.GroupBy(x => GetTableName(x.createdOn));
+
+        //t2 = watch.ElapsedMilliseconds;
+        foreach (var group in gruopParams)
+        {
+            using SqlConnection sqlConnection = new SqlConnection(meterValueConnectionString);
+            sqlConnection.Open();
+            using var tans = sqlConnection.BeginTransaction();
+
+            var tableName = group.Key;
+            string command = $"""
+                INSERT INTO {tableName} (ConnectorId, Value, CreatedOn, ContextId, FormatId, MeasurandId, PhaseId, LocationId, UnitId, ChargeBoxId, TransactionId)
+                VALUES (@ConnectorId, @Value, @CreatedOn, @ContextId, @FormatId, @MeasurandId, @PhaseId, @LocationId, @UnitId, @ChargeBoxId, @TransactionId);
+                """;
+            foreach (var param in group)
+            {
+                var parameters = new DynamicParameters();
+                parameters.Add("ConnectorId", param.connectorId, DbType.Int16);
+                parameters.Add("Value", param.value, DbType.Decimal, precision: 18, scale: 8);
+                parameters.Add("CreatedOn", param.createdOn, DbType.DateTime);
+                parameters.Add("ContextId", param.contextId, DbType.Int32);
+                parameters.Add("FormatId", param.formatId, DbType.Int32);
+                parameters.Add("MeasurandId", param.measurandId, DbType.Int32);
+                parameters.Add("PhaseId", param.phaseId, DbType.Int32);
+                parameters.Add("LocationId", param.locationId, DbType.Int32);
+                parameters.Add("UnitId", param.unitId, DbType.Int32);
+                parameters.Add("ChargeBoxId", param.chargeBoxId, DbType.String, size: 50);
+                parameters.Add("TransactionId", param.transactionId, DbType.Int32);
+                sqlConnection.Execute(command, parameters, tans);
+            }
+
+            tans.Commit();
+
+        }
+
+        //watch.Stop();
+        //t3 = watch.ElapsedMilliseconds;
+        //if (t3 > 700)
+        //{
+        //    logger.LogWarning("MeterValue Dapper {0}/{1}/{2}/{3}", t0, t1, t2, t3);
+        //}
+        return ;
+    }
+
+    private async ValueTask<bool> GetTableExist(DateTime tableDateTime)
+    {
+        var tableName = GetTableName(tableDateTime);
+        if (_existTables.Contains(tableName))
+        {
+            return true;
+        }
+
+        FormattableString checkTableSql = $"SELECT Count(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = {tableName}";
+
+        using var db = await meterValueDbContextFactory.CreateDbContextAsync();
+        var resultList = db.Database.SqlQuery<int>(checkTableSql)?.ToList();
+
+        if (resultList is not null && resultList.Count > 0 && resultList[0] > 0)
+        {
+            _existTables.Enqueue(tableName);
+            if (_existTables.Count > 30)
+            {
+                _existTables.TryDequeue(out _);
+            }
+            return true;
+        }
+
+        return false;
+    }
+
+    private async Task InsertWithStoredProcedure(InsertMeterValueParam param)
+    {
+        using var db = await meterValueDbContextFactory.CreateDbContextAsync();
+
+        string sp = "[dbo].[uspInsertMeterValueRecord] @ChargeBoxId," +
+"@ConnectorId,@Value,@CreatedOn,@ContextId,@FormatId,@MeasurandId,@PhaseId,@LocationId,@UnitId,@TransactionId";
+
+        List<SqlParameter> parameter = new List<SqlParameter>();
+        parameter.AddInsertMeterValueRecordSqlParameters(
+            chargeBoxId: param.chargeBoxId
+            , connectorId: (byte)param.connectorId
+            , value: param.value
+            , createdOn: param.createdOn
+            , contextId: param.contextId
+            , formatId: param.formatId
+            , measurandId: param.measurandId
+            , phaseId: param.phaseId
+            , locationId: param.locationId
+            , unitId: param.unitId
+            , transactionId: param.transactionId);
+
+        db.Database.ExecuteSqlRaw(sp, parameter.ToArray());
+    }
+
+    private static string GetTableName(DateTime dateTime)
+        => $"ConnectorMeterValueRecord{dateTime:yyMMdd}";
+}