From 0f1ec072ad1032e6fa0367e3393e239caeac955c Mon Sep 17 00:00:00 2001 From: Suphonchai Phoonsawat Date: Wed, 13 May 2026 07:00:22 +0700 Subject: [PATCH] change logic --- .claude/settings.local.json | 11 ++ BMA.EHR.Recruit.csproj | 1 + CLAUDE.md | 76 ++++++++ Services/ImportBackgroundService.cs | 265 ++++++++++++++++++------- obj/project.assets.json | 289 ++++++++++++++++++++++++++++ obj/project.nuget.cache | 11 +- 6 files changed, 579 insertions(+), 74 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 CLAUDE.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..a8da82e --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(dotnet build:*)", + "WebSearch", + "Bash(dotnet add:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/BMA.EHR.Recruit.csproj b/BMA.EHR.Recruit.csproj index eca7d61..bba397f 100644 --- a/BMA.EHR.Recruit.csproj +++ b/BMA.EHR.Recruit.csproj @@ -22,6 +22,7 @@ + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..273523d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,76 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build & Run + +```bash +dotnet build BMA.EHR.Recruit.Service.sln +dotnet build BMA.EHR.Recruit.Service.sln -c Release +dotnet publish -c Release -o /app/publish /p:UseAppHost=false +``` + +No test projects exist in this solution. + +## Architecture + +**Stack:** ASP.NET Core 7.0 Web API / EF Core 7.0 / MySQL (Pomelo) / MinIO (S3) / Keycloak (JWT) + +**Pattern:** Controllers → Services → EF Core DbContext (no repository layer for main data) + +### Multiple DbContexts +Three separate MySQL databases, each with its own `DbContext`: +- `ApplicationDbContext` — Recruitment data (Recruits, Scores, Imports) +- `OrgDbContext` — Organization data +- `MetadataDbContext` — Metadata + +All registered as `Transient` lifetime. Auto-migration runs on startup. + +### Controllers +- `BaseController` provides standardized `Success()` / `Error()` response methods returning `ResponseObject` (Status, Message, Result) +- `RecruitController` is the sole business controller (route: `api/v{version}/recruit`) +- API versioning enabled via `Microsoft.AspNetCore.Mvc.Versioning` + +### Background Import System +Excel file imports run asynchronously through a Channel-based queue: +- `ImportBackgroundService` (BackgroundService) — dequeues and processes jobs +- `ImportJobQueue` — bounded Channel (capacity 100) +- `ImportJobTracker` — in-memory ConcurrentDictionary tracking + +Four import types: `CandidateFile`, `CandidateFileById`, `ScoreFile`, `ResultFile` + +All imports use `EFCore.BulkExtensions.MySql` (v6.7.16) for bulk operations to handle 50,000+ rows without memory issues. Pattern: +1. Insert parent/history entities via `SaveChangesAsync` (small operations) +2. `ChangeTracker.Clear()` to release references +3. Collect entities into separate `List` per table +4. `BulkInsertAsync` with `SetOutputIdentity = true` for parent entities +5. Assign generated Ids to child entities +6. `BulkInsertAsync` for each child entity table separately +7. Batch size: 500 + +### Entity Models +All entities inherit from `EntityBase` (Guid `Id` PK, audit fields: `CreatedAt`, `CreatedUserId`, `LastUpdatedAt`, etc.). Models are in `Models/` with subdirectories: `Recruits/`, `Documents/`, `HR/`, `MetaData/`, `Placement/`. + +Key relationships use navigation properties without explicit FK properties (EF shadow properties). Configured via fluent API in `OnModelCreating`. + +### External Services +- **Authentication:** Keycloak JWT Bearer (`hrmsbkk-id.case-collection.com/realms/hrms`) +- **File Storage:** MinIO via `MinIOService` (AWS S3 SDK) +- **Search/Logging:** Elasticsearch (NEST client) +- **Excel:** EPPlus for reading import files + +## Key Files + +- `Program.cs` — Service registration, middleware pipeline, auto-migration +- `Data/ApplicationDbContext.cs` — EF Core fluent API relationship configuration +- `Services/ImportBackgroundService.cs` — All bulk import logic (4 import methods) +- `Services/RecruitService.cs` — Core business logic +- `Controllers/BaseController.cs` — Standard response helpers +- `Responses/ResponseObject.cs` — Standard API response envelope + +## Conventions + +- Language: C# with nullable reference types enabled +- Naming: PascalCase properties, `_camelCase` parameters in service methods +- API responses: Always wrapped in `ResponseObject` +- Authorization: `[Authorize]` on controllers, user context from JWT claims diff --git a/Services/ImportBackgroundService.cs b/Services/ImportBackgroundService.cs index 8023d57..230a37c 100644 --- a/Services/ImportBackgroundService.cs +++ b/Services/ImportBackgroundService.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using OfficeOpenXml; using System.Net.Http.Headers; using BMA.EHR.Recruit.Data; +using EFCore.BulkExtensions; using BMA.EHR.Recruit.Extensions; using BMA.EHR.Recruit.Models.Recruits; using BMA.EHR.Recruit.Requests.Recruits; @@ -112,9 +113,16 @@ public class ImportBackgroundService : BackgroundService int row = 2; int batchCount = 0; - const int batchSize = 100; + const int batchSize = 500; int totalProcessed = 0; + var batchRecruits = new List(); + var batchEducations = new List(); + var batchOccupations = new List(); + var batchAddresses = new List(); + var batchPayments = new List(); + var batchCertificates = new List(); + while (row <= totalRows) { var cell1 = workSheet?.Cells[row, 1]?.GetValue(); @@ -141,7 +149,7 @@ public class ImportBackgroundService : BackgroundService r.PositionLevel = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.PositionLevel)]?.GetValue().IsNull(""); // address - r.Addresses.Add(new RecruitAddress() + var address = new RecruitAddress() { Address = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Address)]?.GetValue() ?? "", Moo = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Moo)]?.GetValue() ?? "", @@ -161,10 +169,10 @@ public class ImportBackgroundService : BackgroundService Amphur1 = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Amphur1)]?.GetValue() ?? "", Province1 = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Province1)]?.GetValue() ?? "", ZipCode1 = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.ZipCode1)]?.GetValue() ?? "", - }); + }; // payment - r.Payments.Add(new RecruitPayment() + var payment = new RecruitPayment() { PaymentId = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.PaymentID)]?.GetValue() ?? "", CompanyCode = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.CompanyCode)]?.GetValue() ?? "", @@ -182,28 +190,28 @@ public class ImportBackgroundService : BackgroundService ChequeNo = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.ChequeNo)]?.GetValue() ?? "", Amount = (decimal)workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Amount)]?.GetValue(), ChqueBankCode = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.ChqBankCode)]?.GetValue() ?? "" - }); + }; // occupation - r.Occupations.Add(new RecruitOccupation() + var occupation = new RecruitOccupation() { Occupation = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Occupation)]?.GetValue() ?? "", Position = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Position)]?.GetValue() ?? "", Workplace = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Workplace)]?.GetValue() ?? "", Telephone = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.WorkplaceTelephone)]?.GetValue() ?? "", WorkAge = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.WorkAge)]?.GetValue() ?? "", - }); + }; // certificate - r.Certificates.Add(new RecruitCertificate() + var certificate = new RecruitCertificate() { CertificateNo = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.CertificateNo)]?.GetValue() ?? "", Description = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.CertificateDesc)]?.GetValue() ?? "", IssueDate = Convert.ToDateTime(workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.CertificateIssueDate)]?.GetValue().ToDateTime(DateTimeFormat.Ymd, "-")), ExpiredDate = Convert.ToDateTime(workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.CertificateExpireDate)]?.GetValue().ToDateTime(DateTimeFormat.Ymd, "-")) - }); + }; - r.Educations.Add(new RecruitEducation() + var education = new RecruitEducation() { Degree = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Degree)]?.GetValue() ?? "", Major = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.Major)]?.GetValue() ?? "", @@ -214,10 +222,21 @@ public class ImportBackgroundService : BackgroundService Specialist = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.SpecialList)]?.GetValue() ?? "", HighDegree = workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.HighDegree)]?.GetValue() ?? "", BachelorDate = Convert.ToDateTime(workSheet?.Cells[row, GetColumnIndex(cols, CandidateFileHeader.BachelorDate)]?.GetValue().ToDateTime(DateTimeFormat.Ymd, "-")) - }); + }; + + r.Addresses.Add(address); + r.Payments.Add(payment); + r.Occupations.Add(occupation); + r.Certificates.Add(certificate); + r.Educations.Add(education); r.RecruitImport = imported; - _context.Recruits.Add(r); + batchRecruits.Add(r); + batchAddresses.Add(address); + batchPayments.Add(payment); + batchOccupations.Add(occupation); + batchCertificates.Add(certificate); + batchEducations.Add(education); row++; batchCount++; @@ -225,16 +244,56 @@ public class ImportBackgroundService : BackgroundService if (batchCount >= batchSize) { - _context.SaveChanges(); - _context.ChangeTracker.Clear(); - _context.Entry(imported).State = EntityState.Unchanged; + await _context.BulkInsertAsync(batchRecruits, options => { options.SetOutputIdentity = true; }); + + for (int j = 0; j < batchRecruits.Count; j++) + { + batchAddresses[j].Recruit = batchRecruits[j]; + batchPayments[j].Recruit = batchRecruits[j]; + batchOccupations[j].Recruit = batchRecruits[j]; + batchCertificates[j].Recruit = batchRecruits[j]; + batchEducations[j].Recruit = batchRecruits[j]; + } + + await _context.BulkInsertAsync(batchAddresses); + await _context.BulkInsertAsync(batchPayments); + await _context.BulkInsertAsync(batchOccupations); + await _context.BulkInsertAsync(batchCertificates); + await _context.BulkInsertAsync(batchEducations); + + batchRecruits.Clear(); + batchAddresses.Clear(); + batchPayments.Clear(); + batchOccupations.Clear(); + batchCertificates.Clear(); + batchEducations.Clear(); batchCount = 0; _tracker.UpdateStatus(job.JobId, ImportJobStatus.Running, totalProcessed); } } + + // Process remaining records + if (batchRecruits.Count > 0) + { + await _context.BulkInsertAsync(batchRecruits, options => { options.SetOutputIdentity = true; }); + + for (int j = 0; j < batchRecruits.Count; j++) + { + batchAddresses[j].Recruit = batchRecruits[j]; + batchPayments[j].Recruit = batchRecruits[j]; + batchOccupations[j].Recruit = batchRecruits[j]; + batchCertificates[j].Recruit = batchRecruits[j]; + batchEducations[j].Recruit = batchRecruits[j]; + } + + await _context.BulkInsertAsync(batchAddresses); + await _context.BulkInsertAsync(batchPayments); + await _context.BulkInsertAsync(batchOccupations); + await _context.BulkInsertAsync(batchCertificates); + await _context.BulkInsertAsync(batchEducations); + } } - _context.SaveChanges(); job.TotalCount = _tracker.GetJob(job.JobId)?.ProcessedCount ?? 0; } @@ -244,13 +303,10 @@ public class ImportBackgroundService : BackgroundService private async Task ProcessCandidateFileByIdAsync(ApplicationDbContext _context, MinIOService _minioService, RecruitService _recruitService, IWebHostEnvironment _webHostEnv, ImportJobInfo job) { - var imported = await _context.RecruitImports.AsQueryable() - .Include(x => x.ImportHostories) - .Include(x => x.ImportFile) - .FirstOrDefaultAsync(x => x.Id == job.RecruitImportId); - + var imported = await _context.RecruitImports.FindAsync(job.RecruitImportId); if (imported == null) throw new Exception("RecruitImport not found"); + // Save import history using regular SaveChanges (small operation) imported.ImportHostories.Add(new RecruitImportHistory { Description = "นำเข้าข้อมูลผู้สมัครสอบแข่งขัน", @@ -261,19 +317,27 @@ public class ImportBackgroundService : BackgroundService LastUpdateUserId = job.UserId ?? "", LastUpdateFullName = job.FullName ?? "System Administrator", }); + await _context.SaveChangesAsync(); + _context.ChangeTracker.Clear(); + + var importId = imported.Id; using var c_package = new ExcelPackage(new FileInfo(job.ImportFile)); for (int i = 0; i < c_package.Workbook.Worksheets.Count; i++) { var workSheet = c_package.Workbook.Worksheets[i]; var totalRows = workSheet.Dimension.Rows; - var cols = workSheet.GetHeaderColumns(); int row = 2; int batchCount = 0; - const int batchSize = 50; + const int batchSize = 500; int totalProcessed = 0; - var batchList = new List(); + + var batchRecruits = new List(); + var batchEducations = new List(); + var batchOccupations = new List(); + var batchAddresses = new List(); + var batchPayments = new List(); while (row <= totalRows) { @@ -308,8 +372,8 @@ public class ImportBackgroundService : BackgroundService r.LastUpdateUserId = job.UserId ?? ""; r.LastUpdateFullName = job.FullName ?? "System Administrator"; - // education - r.Educations.Add(new RecruitEducation() + // Store child entities in separate lists for bulk insert + var education = new RecruitEducation() { Degree = workSheet?.Cells[row, 18]?.GetValue() ?? "", Major = workSheet?.Cells[row, 19]?.GetValue() == "อื่น ๆ" ? workSheet?.Cells[row, 20]?.GetValue() ?? "" : workSheet?.Cells[row, 19]?.GetValue() ?? "", @@ -326,10 +390,9 @@ public class ImportBackgroundService : BackgroundService LastUpdatedAt = DateTime.Now, LastUpdateUserId = job.UserId ?? "", LastUpdateFullName = job.FullName ?? "System Administrator" - }); + }; - // occupation - r.Occupations.Add(new RecruitOccupation() + var occupation = new RecruitOccupation() { Occupation = workSheet?.Cells[row, 33]?.GetValue() == "อื่น ๆ" ? workSheet?.Cells[row, 34]?.GetValue() ?? "" : workSheet?.Cells[row, 33]?.GetValue() ?? "", Position = workSheet?.Cells[row, 37]?.GetValue() ?? "", @@ -342,10 +405,9 @@ public class ImportBackgroundService : BackgroundService LastUpdatedAt = DateTime.Now, LastUpdateUserId = job.UserId ?? "", LastUpdateFullName = job.FullName ?? "System Administrator" - }); + }; - // address - r.Addresses.Add(new RecruitAddress() + var address = new RecruitAddress() { Address = $"{(workSheet?.Cells[row, 49]?.GetValue() ?? "")} {(workSheet?.Cells[row, 50]?.GetValue() ?? "")}", Moo = workSheet?.Cells[row, 51]?.GetValue() ?? "", @@ -371,10 +433,9 @@ public class ImportBackgroundService : BackgroundService LastUpdatedAt = DateTime.Now, LastUpdateUserId = job.UserId ?? "", LastUpdateFullName = job.FullName ?? "System Administrator" - }); + }; - // payment - r.Payments.Add(new RecruitPayment() + var payment = new RecruitPayment() { PaymentId = workSheet?.Cells[row, 104]?.GetValue() ?? "", CompanyCode = workSheet?.Cells[row, 105]?.GetValue() ?? "", @@ -398,10 +459,18 @@ public class ImportBackgroundService : BackgroundService LastUpdatedAt = DateTime.Now, LastUpdateUserId = job.UserId ?? "", LastUpdateFullName = job.FullName ?? "System Administrator" - }); + }; - r.RecruitImport = imported; - batchList.Add(r); + r.Educations.Add(education); + r.Occupations.Add(occupation); + r.Addresses.Add(address); + r.Payments.Add(payment); + + batchRecruits.Add(r); + batchEducations.Add(education); + batchOccupations.Add(occupation); + batchAddresses.Add(address); + batchPayments.Add(payment); row++; batchCount++; @@ -409,23 +478,61 @@ public class ImportBackgroundService : BackgroundService if (batchCount >= batchSize) { - _context.Recruits.AddRange(batchList); - _context.SaveChanges(); - _context.ChangeTracker.Clear(); - _context.Entry(imported).State = EntityState.Unchanged; - batchList.Clear(); + // BulkInsert Recruits first (with SetOutputIdentity to get generated Ids) + await _context.BulkInsertAsync(batchRecruits, options => + { + options.SetOutputIdentity = true; + }); + + // Assign generated Recruit Id to child entities + for (int j = 0; j < batchRecruits.Count; j++) + { + batchEducations[j].Recruit = batchRecruits[j]; + batchOccupations[j].Recruit = batchRecruits[j]; + batchAddresses[j].Recruit = batchRecruits[j]; + batchPayments[j].Recruit = batchRecruits[j]; + } + + // BulkInsert child entities (no identity output needed) + await _context.BulkInsertAsync(batchEducations); + await _context.BulkInsertAsync(batchOccupations); + await _context.BulkInsertAsync(batchAddresses); + await _context.BulkInsertAsync(batchPayments); + + // Clear all lists for next batch + batchRecruits.Clear(); + batchEducations.Clear(); + batchOccupations.Clear(); + batchAddresses.Clear(); + batchPayments.Clear(); batchCount = 0; _tracker.UpdateStatus(job.JobId, ImportJobStatus.Running, totalProcessed); } } - if (batchList.Count > 0) + // Process remaining records + if (batchRecruits.Count > 0) { - _context.Recruits.AddRange(batchList); + await _context.BulkInsertAsync(batchRecruits, options => + { + options.SetOutputIdentity = true; + }); + + for (int j = 0; j < batchRecruits.Count; j++) + { + batchEducations[j].Recruit = batchRecruits[j]; + batchOccupations[j].Recruit = batchRecruits[j]; + batchAddresses[j].Recruit = batchRecruits[j]; + batchPayments[j].Recruit = batchRecruits[j]; + } + + await _context.BulkInsertAsync(batchEducations); + await _context.BulkInsertAsync(batchOccupations); + await _context.BulkInsertAsync(batchAddresses); + await _context.BulkInsertAsync(batchPayments); } } - _context.SaveChanges(); job.TotalCount = _tracker.GetJob(job.JobId)?.ProcessedCount ?? 0; } @@ -445,8 +552,7 @@ public class ImportBackgroundService : BackgroundService if (rec_import.ScoreImport != null && rec_import.ScoreImport.Scores != null) { - _context.RecruitScores.RemoveRange(rec_import.ScoreImport.Scores); - await _context.SaveChangesAsync(); + await _context.BulkDeleteAsync(rec_import.ScoreImport.Scores.ToList()); } rec_import.ImportHostories.Add(new RecruitImportHistory @@ -475,7 +581,12 @@ public class ImportBackgroundService : BackgroundService Scores = new List() }; - // preload recruits + // Save ScoreImport parent first to get its Id + rec_import.ScoreImport = imported; + await _context.SaveChangesAsync(); + _context.ChangeTracker.Clear(); + + // preload recruits (lightweight - only ExamId) var recruitsDict = await _context.Recruits .Where(x => x.RecruitImport.Id == rec_import.Id) .GroupBy(x => x.ExamId) @@ -490,10 +601,12 @@ public class ImportBackgroundService : BackgroundService var cols = workSheet.GetHeaderColumns(); int row = 8; int batchCount = 0; - const int batchSize = 100; + const int batchSize = 500; int totalProcessed = 0; var endRow = workSheet.Dimension.End.Row; + var batchScores = new List(); + while (row <= endRow) { var cell1 = workSheet?.Cells[row, 1]?.GetValue(); @@ -542,8 +655,9 @@ public class ImportBackgroundService : BackgroundService r.LastUpdatedAt = DateTime.Now; r.LastUpdateUserId = job.UserId ?? ""; r.LastUpdateFullName = job.FullName ?? "System Administrator"; + r.ScoreImport = imported; - imported.Scores.Add(r); + batchScores.Add(r); } row++; @@ -552,23 +666,20 @@ public class ImportBackgroundService : BackgroundService if (batchCount >= batchSize) { - rec_import.ScoreImport = imported; - await _context.SaveChangesAsync(); - _context.ChangeTracker.Clear(); - _context.Attach(rec_import); - _context.Attach(imported); - imported.Scores.Clear(); + await _context.BulkInsertAsync(batchScores); + batchScores.Clear(); batchCount = 0; _tracker.UpdateStatus(job.JobId, ImportJobStatus.Running, totalProcessed); } } + + // Process remaining records + if (batchScores.Count > 0) + { + await _context.BulkInsertAsync(batchScores); + } } - if (imported.Scores.Count > 0) - { - rec_import.ScoreImport = imported; - await _context.SaveChangesAsync(); - } job.TotalCount = _tracker.GetJob(job.JobId)?.ProcessedCount ?? 0; } @@ -599,7 +710,7 @@ public class ImportBackgroundService : BackgroundService x.Number = string.Empty; x.RemarkExamOrder = string.Empty; } - await _context.SaveChangesAsync(); + await _context.BulkUpdateAsync(oldScores); } } @@ -613,14 +724,17 @@ public class ImportBackgroundService : BackgroundService LastUpdateUserId = job.UserId ?? "", LastUpdateFullName = job.FullName ?? "System Administrator", }); + await _context.SaveChangesAsync(); + _context.ChangeTracker.Clear(); - // preload scores - var score = rec_import.ScoreImport.Scores - .Where(s => !string.IsNullOrEmpty(s.ExamId)) + // preload scores - re-query from DB to avoid tracking issues + var scoreList = await _context.RecruitScores + .Where(s => s.ScoreImport.RecruitImportId == rec_import.Id && !string.IsNullOrEmpty(s.ExamId)) .GroupBy(x => x.ExamId) .Where(g => g.Count() == 1) .Select(g => g.First()) - .ToDictionary(s => s.ExamId, s => s); + .ToListAsync(); + var score = scoreList.ToDictionary(s => s.ExamId!, s => s); // Read from saved file (ResultFile uses stream from Form, but we saved to disk) using var stream = System.IO.File.OpenRead(job.ImportFile); @@ -630,8 +744,9 @@ public class ImportBackgroundService : BackgroundService { int row = 7; int batchCount = 0; - const int batchSize = 100; + const int batchSize = 500; var endRow = workSheet.Dimension.End.Row; + var batchUpdates = new List(); while (row <= endRow) { @@ -649,6 +764,7 @@ public class ImportBackgroundService : BackgroundService existingScore.LastUpdatedAt = DateTime.Now; existingScore.LastUpdateUserId = job.UserId ?? ""; existingScore.LastUpdateFullName = job.FullName ?? "System Administrator"; + batchUpdates.Add(existingScore); batchCount++; } @@ -656,15 +772,18 @@ public class ImportBackgroundService : BackgroundService if (batchCount >= batchSize) { - await _context.SaveChangesAsync(); - _context.ChangeTracker.Clear(); - _context.Entry(rec_import).State = EntityState.Unchanged; + await _context.BulkUpdateAsync(batchUpdates); + batchUpdates.Clear(); batchCount = 0; } } - } - await _context.SaveChangesAsync(); + // Process remaining records + if (batchUpdates.Count > 0) + { + await _context.BulkUpdateAsync(batchUpdates); + } + } } #endregion diff --git a/obj/project.assets.json b/obj/project.assets.json index 424a2d0..818122a 100644 --- a/obj/project.assets.json +++ b/obj/project.assets.json @@ -155,6 +155,43 @@ } } }, + "dotMorten.Microsoft.SqlServer.Types/1.4.0": { + "type": "package", + "dependencies": { + "System.Data.SqlClient": "4.8.3", + "System.Memory": "4.5.4" + }, + "compile": { + "lib/netstandard2.0/Microsoft.SqlServer.Types.dll": { + "related": ".pdb;.xml" + } + }, + "runtime": { + "lib/netstandard2.0/Microsoft.SqlServer.Types.dll": { + "related": ".pdb;.xml" + } + } + }, + "EFCore.BulkExtensions.MySql/6.7.16": { + "type": "package", + "dependencies": { + "EntityFrameworkCore.SqlServer.HierarchyId": "3.0.1", + "MedallionTopologicalSort": "1.0.0", + "Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite": "6.0.21", + "Pomelo.EntityFrameworkCore.MySql": "6.0.2", + "StrongNamer": "0.2.5" + }, + "compile": { + "lib/net6.0/EFCore.BulkExtensions.MySql.dll": { + "related": ".pdb;.xml" + } + }, + "runtime": { + "lib/net6.0/EFCore.BulkExtensions.MySql.dll": { + "related": ".pdb;.xml" + } + } + }, "Elasticsearch.Net/7.17.5": { "type": "package", "dependencies": { @@ -173,6 +210,42 @@ } } }, + "EntityFrameworkCore.SqlServer.HierarchyId/3.0.1": { + "type": "package", + "dependencies": { + "EntityFrameworkCore.SqlServer.HierarchyId.Abstractions": "3.0.1", + "Microsoft.EntityFrameworkCore.SqlServer": "6.0.1" + }, + "compile": { + "lib/net6.0/EntityFrameworkCore.SqlServer.HierarchyId.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/EntityFrameworkCore.SqlServer.HierarchyId.dll": { + "related": ".xml" + } + }, + "build": { + "build/net6.0/_._": {} + } + }, + "EntityFrameworkCore.SqlServer.HierarchyId.Abstractions/3.0.1": { + "type": "package", + "dependencies": { + "dotMorten.Microsoft.SqlServer.Types": "1.4.0" + }, + "compile": { + "lib/netstandard2.0/EntityFrameworkCore.SqlServer.HierarchyId.Abstractions.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/netstandard2.0/EntityFrameworkCore.SqlServer.HierarchyId.Abstractions.dll": { + "related": ".xml" + } + } + }, "EPPlus/6.1.3": { "type": "package", "dependencies": { @@ -304,6 +377,19 @@ } } }, + "MedallionTopologicalSort/1.0.0": { + "type": "package", + "compile": { + "lib/netstandard2.0/MedallionTopologicalSort.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/netstandard2.0/MedallionTopologicalSort.dll": { + "related": ".xml" + } + } + }, "Microsoft.AspNetCore.Antiforgery/2.2.0": { "type": "package", "dependencies": { @@ -1582,6 +1668,27 @@ } } }, + "Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite/6.0.21": { + "type": "package", + "dependencies": { + "Microsoft.EntityFrameworkCore.SqlServer": "6.0.21", + "NetTopologySuite": "2.3.0", + "NetTopologySuite.IO.SqlServerBytes": "2.0.0" + }, + "compile": { + "lib/net6.0/Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite.dll": { + "related": ".xml" + } + }, + "build": { + "build/net6.0/_._": {} + } + }, "Microsoft.EntityFrameworkCore.Tools/7.0.3": { "type": "package", "dependencies": { @@ -2627,6 +2734,38 @@ "System.Xml.XDocument": "4.3.0" } }, + "NetTopologySuite/2.3.0": { + "type": "package", + "dependencies": { + "System.Memory": "4.5.3" + }, + "compile": { + "lib/netstandard2.0/NetTopologySuite.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/netstandard2.0/NetTopologySuite.dll": { + "related": ".xml" + } + } + }, + "NetTopologySuite.IO.SqlServerBytes/2.0.0": { + "type": "package", + "dependencies": { + "NetTopologySuite": "[2.0.0, 3.0.0-A)" + }, + "compile": { + "lib/netstandard2.0/NetTopologySuite.IO.SqlServerBytes.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/netstandard2.0/NetTopologySuite.IO.SqlServerBytes.dll": { + "related": ".xml" + } + } + }, "Newtonsoft.Json/13.0.3": { "type": "package", "compile": { @@ -3336,6 +3475,12 @@ } } }, + "StrongNamer/0.2.5": { + "type": "package", + "build": { + "build/_._": {} + } + }, "Swashbuckle.AspNetCore/6.5.0": { "type": "package", "dependencies": { @@ -5377,6 +5522,38 @@ "lib/netstandard2.1/DnsClient.xml" ] }, + "dotMorten.Microsoft.SqlServer.Types/1.4.0": { + "sha512": "MYxVbuBguObk8QFNTuBZ+ZEC/m1zbvG774FbFvwiDZjc0RYq/co27THrHN5Dyd52ie0R5bt2uxSZj4tIb3lYFg==", + "type": "package", + "path": "dotmorten.microsoft.sqlserver.types/1.4.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "README.md", + "dotmorten.microsoft.sqlserver.types.1.4.0.nupkg.sha512", + "dotmorten.microsoft.sqlserver.types.nuspec", + "lib/netstandard2.0/Microsoft.SqlServer.Types.dll", + "lib/netstandard2.0/Microsoft.SqlServer.Types.pdb", + "lib/netstandard2.0/Microsoft.SqlServer.Types.xml" + ] + }, + "EFCore.BulkExtensions.MySql/6.7.16": { + "sha512": "XJbeYxAKeRrI/gVXw08Nx9ZJiP2aRDyqJgW7GRTmxIUUVyp8hgVqasLUaBh0LA6PmtQr+eCg8LzyUYvSF4U9hA==", + "type": "package", + "path": "efcore.bulkextensions.mysql/6.7.16", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "EFCoreBulk.png", + "LICENSE.txt", + "README.md", + "efcore.bulkextensions.mysql.6.7.16.nupkg.sha512", + "efcore.bulkextensions.mysql.nuspec", + "lib/net6.0/EFCore.BulkExtensions.MySql.dll", + "lib/net6.0/EFCore.BulkExtensions.MySql.pdb", + "lib/net6.0/EFCore.BulkExtensions.MySql.xml" + ] + }, "Elasticsearch.Net/7.17.5": { "sha512": "orChsQi1Ceho/NyIylNOn6y4vuGcsbCfMZnCueNN0fzqYEGQmQdPfcVmsR5+3fwpXTgxCdjTUVmqOwvHpCSB+Q==", "type": "package", @@ -5399,6 +5576,33 @@ "nuget-icon.png" ] }, + "EntityFrameworkCore.SqlServer.HierarchyId/3.0.1": { + "sha512": "NDN8PwDIyuWfOR6nV0muaCEvCDuAFYPEtKOku+/TIzOWMmvPNqqLF0WF0BQpk4cVuvQ2fr5/9F7aZk4DkVTq4g==", + "type": "package", + "path": "entityframeworkcore.sqlserver.hierarchyid/3.0.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "build/net6.0/EntityFrameworkCore.SqlServer.HierarchyId.targets", + "entityframeworkcore.sqlserver.hierarchyid.3.0.1.nupkg.sha512", + "entityframeworkcore.sqlserver.hierarchyid.nuspec", + "lib/net6.0/EntityFrameworkCore.SqlServer.HierarchyId.dll", + "lib/net6.0/EntityFrameworkCore.SqlServer.HierarchyId.xml" + ] + }, + "EntityFrameworkCore.SqlServer.HierarchyId.Abstractions/3.0.1": { + "sha512": "x0Y3QtTLd1oyQMHTpXmUnuXabAs44kZBlP0suVQjlreF76e55x2D9YDiXRwUekvtI+X+b0NLkYfsnAqf9W/58w==", + "type": "package", + "path": "entityframeworkcore.sqlserver.hierarchyid.abstractions/3.0.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "entityframeworkcore.sqlserver.hierarchyid.abstractions.3.0.1.nupkg.sha512", + "entityframeworkcore.sqlserver.hierarchyid.abstractions.nuspec", + "lib/netstandard2.0/EntityFrameworkCore.SqlServer.HierarchyId.Abstractions.dll", + "lib/netstandard2.0/EntityFrameworkCore.SqlServer.HierarchyId.Abstractions.xml" + ] + }, "EPPlus/6.1.3": { "sha512": "1NEgW7wMxHWz7k3hN6D7PPkCCKR24LK86EIIEwfKrBy+yyWQM/fsCrngt+DPAjVgGLOThVmXInSFJqD15X7OCQ==", "type": "package", @@ -5586,6 +5790,21 @@ "litedb.nuspec" ] }, + "MedallionTopologicalSort/1.0.0": { + "sha512": "dcAqM8TcyZQ/T466CvqNMUUn/G0FQE+4R7l62ngXH7hLFP9yA7yoP/ySsLgiXx3pGUQC3J+cUvXmJOOR/eC+oQ==", + "type": "package", + "path": "medalliontopologicalsort/1.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "lib/net45/MedallionTopologicalSort.dll", + "lib/net45/MedallionTopologicalSort.xml", + "lib/netstandard2.0/MedallionTopologicalSort.dll", + "lib/netstandard2.0/MedallionTopologicalSort.xml", + "medalliontopologicalsort.1.0.0.nupkg.sha512", + "medalliontopologicalsort.nuspec" + ] + }, "Microsoft.AspNetCore.Antiforgery/2.2.0": { "sha512": "fVQsSXNZz38Ysx8iKwwqfOLHhLrAeKEMBS5Ia3Lh7BJjOC2vPV28/yk08AovOMsB3SNQPGnE7bv+lsIBTmAkvw==", "type": "package", @@ -6682,6 +6901,21 @@ "microsoft.entityframeworkcore.sqlserver.nuspec" ] }, + "Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite/6.0.21": { + "sha512": "D6c+WWg6GIcpuHKxHVr6hJ5RFVfwPlgK/PQ0vV+z7f8haRY7l3tlK/xhJeYZvXCpPluyijM4KRRPi/qbUFxROA==", + "type": "package", + "path": "microsoft.entityframeworkcore.sqlserver.nettopologysuite/6.0.21", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "build/net6.0/Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite.targets", + "lib/net6.0/Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite.dll", + "lib/net6.0/Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite.xml", + "microsoft.entityframeworkcore.sqlserver.nettopologysuite.6.0.21.nupkg.sha512", + "microsoft.entityframeworkcore.sqlserver.nettopologysuite.nuspec" + ] + }, "Microsoft.EntityFrameworkCore.Tools/7.0.3": { "sha512": "yHFlYPZS3Jx7JMCQnGKfJzv95rJWVcmcUn/OW5cbCyWgQk81JJpTZ9Q6kkvwquYjFRfvYHBGuXNIYhAJokOBTQ==", "type": "package", @@ -8312,6 +8546,33 @@ "netstandard.library.nuspec" ] }, + "NetTopologySuite/2.3.0": { + "sha512": "Y+YOvA5um+75Wm9NKE+EhUNCvLYWiPhPW5Q5eBNi6kVNbxsX60/nvIJtve5iRcbrAl38W9h2w2yBjU81cjDO5g==", + "type": "package", + "path": "nettopologysuite/2.3.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "icon.png", + "lib/netstandard2.0/NetTopologySuite.dll", + "lib/netstandard2.0/NetTopologySuite.xml", + "nettopologysuite.2.3.0.nupkg.sha512", + "nettopologysuite.nuspec" + ] + }, + "NetTopologySuite.IO.SqlServerBytes/2.0.0": { + "sha512": "TuyMB0VSlRJx86UrWeQ+SGgOMudvhIL1qJJdWJw78nWsXIzKRP4ooQAhhhCCH7n8q1lvd0/NW3ByaLlHxxNSPQ==", + "type": "package", + "path": "nettopologysuite.io.sqlserverbytes/2.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "lib/netstandard2.0/NetTopologySuite.IO.SqlServerBytes.dll", + "lib/netstandard2.0/NetTopologySuite.IO.SqlServerBytes.xml", + "nettopologysuite.io.sqlserverbytes.2.0.0.nupkg.sha512", + "nettopologysuite.io.sqlserverbytes.nuspec" + ] + }, "Newtonsoft.Json/13.0.3": { "sha512": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==", "type": "package", @@ -9150,6 +9411,29 @@ "snappier.nuspec" ] }, + "StrongNamer/0.2.5": { + "sha512": "1IWl8gYnsTC6NXHz63iDpXL8r0y5x0M/Cnq/Ju5uM17gTOQYSeclMkgQsvmGglJEqAwVxayY1sIUR3bb2MAy5Q==", + "type": "package", + "path": "strongnamer/0.2.5", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "build/SharedKey.snk", + "build/StrongNamer.targets", + "build/net461/Mono.Cecil.Mdb.dll", + "build/net461/Mono.Cecil.Pdb.dll", + "build/net461/Mono.Cecil.Rocks.dll", + "build/net461/Mono.Cecil.dll", + "build/net461/StrongNamer.dll", + "build/netcoreapp2.1/Mono.Cecil.Mdb.dll", + "build/netcoreapp2.1/Mono.Cecil.Pdb.dll", + "build/netcoreapp2.1/Mono.Cecil.Rocks.dll", + "build/netcoreapp2.1/Mono.Cecil.dll", + "build/netcoreapp2.1/StrongNamer.dll", + "strongnamer.0.2.5.nupkg.sha512", + "strongnamer.nuspec" + ] + }, "Swashbuckle.AspNetCore/6.5.0": { "sha512": "FK05XokgjgwlCI6wCT+D4/abtQkL1X1/B9Oas6uIwHFmYrIO9WUD5aLC9IzMs9GnHfUXOtXZ2S43gN1mhs5+aA==", "type": "package", @@ -13710,6 +13994,7 @@ "net7.0": [ "AWSSDK.S3 >= 3.7.103.35", "CoreAdmin >= 2.7.0", + "EFCore.BulkExtensions.MySql >= 6.7.16", "EPPlus >= 6.1.3", "Microsoft.AspNetCore.Authentication.JwtBearer >= 7.0.20", "Microsoft.AspNetCore.Mvc.NewtonsoftJson >= 7.0.3", @@ -13796,6 +14081,10 @@ "target": "Package", "version": "[2.7.0, )" }, + "EFCore.BulkExtensions.MySql": { + "target": "Package", + "version": "[6.7.16, )" + }, "EPPlus": { "target": "Package", "version": "[6.1.3, )" diff --git a/obj/project.nuget.cache b/obj/project.nuget.cache index e8d0055..e443564 100644 --- a/obj/project.nuget.cache +++ b/obj/project.nuget.cache @@ -1,6 +1,6 @@ { "version": 2, - "dgSpecHash": "D0RTCj18huw=", + "dgSpecHash": "XR3lYvVwNcQ=", "success": true, "projectFilePath": "/Users/suphonchaip/Develop/hrms/hrms-api-recruit/BMA.EHR.Recruit.csproj", "expectedPackageFiles": [ @@ -13,7 +13,11 @@ "/Users/suphonchaip/.nuget/packages/coreadmin/2.7.0/coreadmin.2.7.0.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/dapper/2.0.123/dapper.2.0.123.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/dnsclient/1.6.1/dnsclient.1.6.1.nupkg.sha512", + "/Users/suphonchaip/.nuget/packages/dotmorten.microsoft.sqlserver.types/1.4.0/dotmorten.microsoft.sqlserver.types.1.4.0.nupkg.sha512", + "/Users/suphonchaip/.nuget/packages/efcore.bulkextensions.mysql/6.7.16/efcore.bulkextensions.mysql.6.7.16.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/elasticsearch.net/7.17.5/elasticsearch.net.7.17.5.nupkg.sha512", + "/Users/suphonchaip/.nuget/packages/entityframeworkcore.sqlserver.hierarchyid/3.0.1/entityframeworkcore.sqlserver.hierarchyid.3.0.1.nupkg.sha512", + "/Users/suphonchaip/.nuget/packages/entityframeworkcore.sqlserver.hierarchyid.abstractions/3.0.1/entityframeworkcore.sqlserver.hierarchyid.abstractions.3.0.1.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/epplus/6.1.3/epplus.6.1.3.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/epplus.interfaces/6.1.1/epplus.interfaces.6.1.1.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/epplus.system.drawing/6.1.1/epplus.system.drawing.6.1.1.nupkg.sha512", @@ -23,6 +27,7 @@ "/Users/suphonchaip/.nuget/packages/k4os.compression.lz4.streams/1.2.6/k4os.compression.lz4.streams.1.2.6.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/k4os.hash.xxhash/1.0.6/k4os.hash.xxhash.1.0.6.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/litedb/5.0.11/litedb.5.0.11.nupkg.sha512", + "/Users/suphonchaip/.nuget/packages/medalliontopologicalsort/1.0.0/medalliontopologicalsort.1.0.0.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/microsoft.aspnetcore.antiforgery/2.2.0/microsoft.aspnetcore.antiforgery.2.2.0.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/microsoft.aspnetcore.authentication.abstractions/2.2.0/microsoft.aspnetcore.authentication.abstractions.2.2.0.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/microsoft.aspnetcore.authentication.core/2.2.0/microsoft.aspnetcore.authentication.core.2.2.0.nupkg.sha512", @@ -93,6 +98,7 @@ "/Users/suphonchaip/.nuget/packages/microsoft.entityframeworkcore.relational/7.0.3/microsoft.entityframeworkcore.relational.7.0.3.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/microsoft.entityframeworkcore.relational.design/1.1.1/microsoft.entityframeworkcore.relational.design.1.1.1.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/microsoft.entityframeworkcore.sqlserver/7.0.3/microsoft.entityframeworkcore.sqlserver.7.0.3.nupkg.sha512", + "/Users/suphonchaip/.nuget/packages/microsoft.entityframeworkcore.sqlserver.nettopologysuite/6.0.21/microsoft.entityframeworkcore.sqlserver.nettopologysuite.6.0.21.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/microsoft.entityframeworkcore.tools/7.0.3/microsoft.entityframeworkcore.tools.7.0.3.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/microsoft.extensions.apidescription.server/6.0.5/microsoft.extensions.apidescription.server.6.0.5.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/microsoft.extensions.caching.abstractions/7.0.0/microsoft.extensions.caching.abstractions.7.0.0.nupkg.sha512", @@ -151,6 +157,8 @@ "/Users/suphonchaip/.nuget/packages/mysqlconnector/2.2.5/mysqlconnector.2.2.5.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/nest/7.17.5/nest.7.17.5.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/netstandard.library/1.6.1/netstandard.library.1.6.1.nupkg.sha512", + "/Users/suphonchaip/.nuget/packages/nettopologysuite/2.3.0/nettopologysuite.2.3.0.nupkg.sha512", + "/Users/suphonchaip/.nuget/packages/nettopologysuite.io.sqlserverbytes/2.0.0/nettopologysuite.io.sqlserverbytes.2.0.0.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/newtonsoft.json.bson/1.0.2/newtonsoft.json.bson.1.0.2.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/nonfactors.grid.core.mvc6/7.1.0/nonfactors.grid.core.mvc6.7.1.0.nupkg.sha512", @@ -197,6 +205,7 @@ "/Users/suphonchaip/.nuget/packages/serilog.sinks.periodicbatching/3.1.0/serilog.sinks.periodicbatching.3.1.0.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/sharpcompress/0.30.1/sharpcompress.0.30.1.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/snappier/1.0.0/snappier.1.0.0.nupkg.sha512", + "/Users/suphonchaip/.nuget/packages/strongnamer/0.2.5/strongnamer.0.2.5.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/swashbuckle.aspnetcore/6.5.0/swashbuckle.aspnetcore.6.5.0.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/swashbuckle.aspnetcore.annotations/6.5.0/swashbuckle.aspnetcore.annotations.6.5.0.nupkg.sha512", "/Users/suphonchaip/.nuget/packages/swashbuckle.aspnetcore.swagger/6.5.0/swashbuckle.aspnetcore.swagger.6.5.0.nupkg.sha512",