Ashutosh_Refactor #107

Merged
ashutosh.nehete merged 52 commits from Ashutosh_Refactor into main 2025-07-18 13:01:29 +00:00
4 changed files with 523 additions and 94 deletions
Showing only changes of commit 3bc51f9cd9 - Show all commits

View File

@ -0,0 +1,45 @@
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.MongoDBModels;
using Microsoft.Extensions.Configuration;
using MongoDB.Driver;
namespace Marco.Pms.CacheHelper
{
public class ReportCache
{
private readonly ApplicationDbContext _context;
private readonly IMongoCollection<ProjectReportEmailMongoDB> _projectReportCollection;
public ReportCache(ApplicationDbContext context, IConfiguration configuration)
{
var connectionString = configuration["MongoDB:ConnectionString"];
_context = context;
var mongoUrl = new MongoUrl(connectionString);
var client = new MongoClient(mongoUrl); // Your MongoDB connection string
var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name
_projectReportCollection = mongoDB.GetCollection<ProjectReportEmailMongoDB>("ProjectReportMail");
}
/// <summary>
/// Retrieves project report emails from the cache based on their sent status.
/// </summary>
/// <param name="isSent">True to get sent reports, false to get unsent reports.</param>
/// <returns>A list of ProjectReportEmailMongoDB objects.</returns>
public async Task<List<ProjectReportEmailMongoDB>> GetProjectReportMailFromCache(bool isSent)
{
var filter = Builders<ProjectReportEmailMongoDB>.Filter.Eq(p => p.IsSent, isSent);
var reports = await _projectReportCollection.Find(filter).ToListAsync();
return reports;
}
/// <summary>
/// Adds a project report email to the cache.
/// </summary>
/// <param name="report">The ProjectReportEmailMongoDB object to add.</param>
/// <returns>A Task representing the asynchronous operation.</returns>
public async Task AddProjectReportMailToCache(ProjectReportEmailMongoDB report)
{
// Consider adding validation or logging here.
await _projectReportCollection.InsertOneAsync(report);
}
}
}

View File

@ -0,0 +1,16 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace Marco.Pms.Model.MongoDBModels
{
public class ProjectReportEmailMongoDB
{
[BsonId] // Tells MongoDB this is the primary key (_id)
[BsonRepresentation(BsonType.ObjectId)] // Optional: if your _id is ObjectId
public string Id { get; set; } = string.Empty;
public string? Body { get; set; }
public string? Subject { get; set; }
public List<string>? Receivers { get; set; }
public bool IsSent { get; set; } = false;
}
}

View File

@ -1,16 +1,19 @@
using System.Data;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Dtos.Mail;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Mail;
using Marco.Pms.Model.MongoDBModels;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Services.Helpers;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
using MongoDB.Driver;
using System.Data;
using System.Globalization;
using System.Net.Mail;
namespace Marco.Pms.Services.Controllers
{
@ -25,7 +28,11 @@ namespace Marco.Pms.Services.Controllers
private readonly UserHelper _userHelper;
private readonly IWebHostEnvironment _env;
private readonly ReportHelper _reportHelper;
public ReportController(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, UserHelper userHelper, IWebHostEnvironment env, ReportHelper reportHelper)
private readonly IConfiguration _configuration;
private readonly CacheUpdateHelper _cache;
private readonly IServiceScopeFactory _serviceScopeFactory;
public ReportController(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, UserHelper userHelper,
IWebHostEnvironment env, ReportHelper reportHelper, IConfiguration configuration, CacheUpdateHelper cache, IServiceScopeFactory serviceScopeFactory)
{
_context = context;
_emailSender = emailSender;
@ -33,27 +40,122 @@ namespace Marco.Pms.Services.Controllers
_userHelper = userHelper;
_env = env;
_reportHelper = reportHelper;
_configuration = configuration;
_cache = cache;
_serviceScopeFactory = serviceScopeFactory;
}
[HttpPost("set-mail")]
/// <summary>
/// Adds new mail details for a project report.
/// </summary>
/// <param name="mailDetailsDto">The mail details data.</param>
/// <returns>An API response indicating success or failure.</returns>
[HttpPost("mail-details")] // More specific route for adding mail details
public async Task<IActionResult> AddMailDetails([FromBody] MailDetailsDto mailDetailsDto)
{
// 1. Get Tenant ID and Basic Authorization Check
Guid tenantId = _userHelper.GetTenantId();
MailDetails mailDetails = new MailDetails
if (tenantId == Guid.Empty)
{
_logger.LogWarning("Authorization Error: Attempt to add mail details with an empty or invalid tenant ID.");
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized", "Tenant ID not found or invalid.", 401));
}
// 2. Input Validation (Leverage Model Validation attributes on DTO)
if (mailDetailsDto == null)
{
_logger.LogWarning("Validation Error: MailDetails DTO is null. TenantId: {TenantId}", tenantId);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Data", "Request body is empty.", 400));
}
// Ensure ProjectId and Recipient are not empty
if (mailDetailsDto.ProjectId == Guid.Empty)
{
_logger.LogWarning("Validation Error: Project ID is empty. TenantId: {TenantId}", tenantId);
return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Project ID cannot be empty.", 400));
}
if (string.IsNullOrWhiteSpace(mailDetailsDto.Recipient))
{
_logger.LogWarning("Validation Error: Recipient email is empty. ProjectId: {ProjectId}, TenantId: {TenantId}", mailDetailsDto.ProjectId, tenantId);
return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Recipient email cannot be empty.", 400));
}
// Optional: Validate email format using regex or System.Net.Mail.MailAddress
try
{
var mailAddress = new MailAddress(mailDetailsDto.Recipient);
_logger.LogInfo("nothing");
}
catch (FormatException)
{
_logger.LogWarning("Validation Error: Invalid recipient email format '{Recipient}'. ProjectId: {ProjectId}, TenantId: {TenantId}", mailDetailsDto.Recipient, mailDetailsDto.ProjectId, tenantId);
return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Invalid recipient email format.", 400));
}
// 3. Validate MailListId (Foreign Key Check)
// Ensure the MailListId refers to an existing MailBody (template) within the same tenant.
if (mailDetailsDto.MailListId != Guid.Empty) // Only validate if a MailListId is provided
{
bool mailTemplateExists;
try
{
mailTemplateExists = await _context.MailingList
.AsNoTracking()
.AnyAsync(m => m.Id == mailDetailsDto.MailListId && m.TenantId == tenantId);
}
catch (Exception ex)
{
_logger.LogError("Database Error: Failed to check existence of MailListId '{MailListId}' for TenantId: {TenantId}. : {Error}", mailDetailsDto.MailListId, tenantId, ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while validating mail template.", 500));
}
if (!mailTemplateExists)
{
_logger.LogWarning("Validation Error: Provided MailListId '{MailListId}' does not exist or does not belong to TenantId: {TenantId}.", mailDetailsDto.MailListId, tenantId);
return NotFound(ApiResponse<object>.ErrorResponse("Invalid Mail Template", "The specified mail template (MailListId) was not found or accessible.", 404));
}
}
// If MailListId can be null/empty and implies no specific template, adjust logic accordingly.
// Currently assumes it must exist if provided.
// 4. Create and Add New Mail Details
var newMailDetails = new MailDetails
{
ProjectId = mailDetailsDto.ProjectId,
Recipient = mailDetailsDto.Recipient,
Schedule = mailDetailsDto.Schedule,
MailListId = mailDetailsDto.MailListId,
TenantId = tenantId
TenantId = tenantId,
};
_context.MailDetails.Add(mailDetails);
await _context.SaveChangesAsync();
return Ok("Success");
try
{
_context.MailDetails.Add(newMailDetails);
await _context.SaveChangesAsync();
_logger.LogInfo("Successfully added new mail details with ID {MailDetailsId} for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}.", newMailDetails.Id, newMailDetails.ProjectId, newMailDetails.Recipient, tenantId);
// 5. Return Success Response (201 Created is ideal for resource creation)
return StatusCode(201, ApiResponse<MailDetails>.SuccessResponse(
newMailDetails, // Return the newly created object (or a DTO of it)
"Mail details added successfully.",
201));
}
catch (DbUpdateException dbEx)
{
_logger.LogError("Database Error: Failed to save new mail details for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}. : {Error}", newMailDetails.ProjectId, newMailDetails.Recipient, tenantId, dbEx.Message);
// Check for specific constraint violations if applicable (e.g., duplicate recipient for a project)
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while saving the mail details.", 500));
}
catch (Exception ex)
{
_logger.LogError("Unexpected Error: An unhandled exception occurred while adding mail details for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}. : {Error}", newMailDetails.ProjectId, newMailDetails.Recipient, tenantId, ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500));
}
}
[HttpPost("mail-template")]
public async Task<IActionResult> AddMailTemplate([FromBody] MailTemeplateDto mailTemeplateDto)
[HttpPost("mail-template1")]
public async Task<IActionResult> AddMailTemplate1([FromBody] MailTemeplateDto mailTemeplateDto)
{
Guid tenantId = _userHelper.GetTenantId();
if (string.IsNullOrWhiteSpace(mailTemeplateDto.Body) && string.IsNullOrWhiteSpace(mailTemeplateDto.Title))
@ -80,116 +182,376 @@ namespace Marco.Pms.Services.Controllers
return Ok("Success");
}
/// <summary>
/// Adds a new mail template.
/// </summary>
/// <param name="mailTemplateDto">The mail template data.</param>
/// <returns>An API response indicating success or failure.</returns>
[HttpPost("mail-template")] // More specific route for adding a template
public async Task<IActionResult> AddMailTemplate([FromBody] MailTemeplateDto mailTemplateDto) // Renamed parameter for consistency
{
// 1. Get Tenant ID and Basic Authorization Check
Guid tenantId = _userHelper.GetTenantId();
if (tenantId == Guid.Empty)
{
_logger.LogWarning("Authorization Error: Attempt to add mail template with an empty or invalid tenant ID.");
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized", "Tenant ID not found or invalid.", 401));
}
// 2. Input Validation (Moved to model validation if possible, or keep explicit)
// Use proper model validation attributes ([Required], [StringLength]) on MailTemeplateDto
// and rely on ASP.NET Core's automatic model validation if possible.
// If not, these checks are good.
if (mailTemplateDto == null)
{
_logger.LogWarning("Validation Error: Mail template DTO is null.");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Data", "Request body is empty.", 400));
}
if (string.IsNullOrWhiteSpace(mailTemplateDto.Title))
{
_logger.LogWarning("Validation Error: Mail template title is empty or whitespace. TenantId: {TenantId}", tenantId);
return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Mail template title cannot be empty.", 400));
}
// The original logic checked both body and title, but often a template needs at least a title.
// Re-evalute if body can be empty. If so, remove the body check. Assuming title is always mandatory.
// If both body and title are empty, it's definitely invalid.
if (string.IsNullOrWhiteSpace(mailTemplateDto.Body) && string.IsNullOrWhiteSpace(mailTemplateDto.Subject))
{
_logger.LogWarning("Validation Error: Mail template body and subject are both empty or whitespace for title '{Title}'. TenantId: {TenantId}", mailTemplateDto.Title, tenantId);
return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Mail template body or subject must be provided.", 400));
}
// 3. Check for Existing Template Title (Case-Insensitive)
// Use AsNoTracking() for read-only query
MailingList? existingTemplate;
try
{
existingTemplate = await _context.MailingList
.AsNoTracking() // Important for read-only checks
.FirstOrDefaultAsync(t => t.Title.ToLower() == mailTemplateDto.Title.ToLower() && t.TenantId == tenantId); // IMPORTANT: Filter by TenantId!
}
catch (Exception ex)
{
_logger.LogError("Database Error: Failed to check for existing mail template with title '{Title}' for TenantId: {TenantId}.: {Error}", mailTemplateDto.Title, tenantId, ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while checking for existing templates.", 500));
}
if (existingTemplate != null)
{
_logger.LogWarning("Conflict Error: User tries to add email template with title '{Title}' which already exists for TenantId: {TenantId}.", mailTemplateDto.Title, tenantId);
return Conflict(ApiResponse<object>.ErrorResponse("Conflict", $"Email template with title '{mailTemplateDto.Title}' already exists.", 409));
}
// 4. Create and Add New Template
var newMailingList = new MailingList
{
Title = mailTemplateDto.Title,
Body = mailTemplateDto.Body,
Subject = mailTemplateDto.Subject,
Keywords = mailTemplateDto.Keywords,
TenantId = tenantId,
};
try
{
_context.MailingList.Add(newMailingList);
await _context.SaveChangesAsync();
_logger.LogInfo("Successfully added new mail template with ID {TemplateId} and title '{Title}' for TenantId: {TenantId}.", newMailingList.Id, newMailingList.Title, tenantId);
// 5. Return Success Response (201 Created is ideal for resource creation)
// It's good practice to return the created resource or its ID.
return StatusCode(201, ApiResponse<MailingList>.SuccessResponse(
newMailingList, // Return the newly created object (or a DTO of it)
"Mail template added successfully.",
201));
}
catch (DbUpdateException dbEx)
{
_logger.LogError("Database Error: Failed to save new mail template '{Title}' for TenantId: {TenantId}. : {Error}", mailTemplateDto.Title, tenantId, dbEx.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while saving the mail template.", 500));
}
catch (Exception ex)
{
_logger.LogError("Unexpected Error: An unhandled exception occurred while adding mail template '{Title}' for TenantId: {TenantId}. : {Error}", mailTemplateDto.Title, tenantId, ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500));
}
}
[HttpGet("project-statistics")]
public async Task<IActionResult> SendProjectReport()
{
Guid tenantId = _userHelper.GetTenantId();
// Use AsNoTracking() for read-only queries to improve performance
List<MailDetails> mailDetails = await _context.MailDetails
// 1. OPTIMIZATION: Perform grouping and projection on the database server.
// This is far more efficient than loading all entities into memory.
var projectMailGroups = await _context.MailDetails
.AsNoTracking()
.Include(m => m.MailBody)
.Where(m => m.TenantId == tenantId)
.ToListAsync();
int successCount = 0;
int notFoundCount = 0;
int invalidIdCount = 0;
var groupedMails = mailDetails
.GroupBy(m => new { m.ProjectId, m.MailListId })
.Select(g => new
{
ProjectId = g.Key.ProjectId,
MailListId = g.Key.MailListId,
Recipients = g.Select(m => m.Recipient).Distinct().ToList(),
MailBody = g.FirstOrDefault()?.MailBody?.Body ?? "",
Subject = g.FirstOrDefault()?.MailBody?.Subject ?? string.Empty,
// Project the mail body and subject from the first record in the group
MailInfo = g.Select(m => new { Body = m.MailBody != null ? m.MailBody.Body : "", Subject = m.MailBody != null ? m.MailBody.Subject : "" }).FirstOrDefault()
})
.ToList();
.ToListAsync();
var semaphore = new SemaphoreSlim(1);
// Using Task.WhenAll to send reports concurrently for better performance
var sendTasks = groupedMails.Select(async mailDetail =>
if (!projectMailGroups.Any())
{
await semaphore.WaitAsync();
try
return Ok(ApiResponse<object>.SuccessResponse(new { }, "No projects found to send reports for.", 200));
}
int successCount = 0;
int notFoundCount = 0;
int invalidIdCount = 0;
int failureCount = 0;
// 2. OPTIMIZATION: Use true concurrency by removing SemaphoreSlim(1)
// and giving each task its own isolated set of services (including DbContext).
var sendTasks = projectMailGroups.Select(async mailGroup =>
{
// SOLUTION: Create a new Dependency Injection scope for each parallel task.
using (var scope = _serviceScopeFactory.CreateScope())
{
var response = await GetProjectStatistics(mailDetail.ProjectId, mailDetail.Recipients, mailDetail.MailBody, mailDetail.Subject, tenantId);
if (response.StatusCode == 200)
Interlocked.Increment(ref successCount);
else if (response.StatusCode == 404)
Interlocked.Increment(ref notFoundCount);
else if (response.StatusCode == 400)
Interlocked.Increment(ref invalidIdCount);
}
finally
{
semaphore.Release();
// Resolve a new instance of the helper from this isolated scope.
// This ensures each task gets its own thread-safe DbContext.
var reportHelper = scope.ServiceProvider.GetRequiredService<ReportHelper>();
try
{
// Ensure MailInfo and ProjectId are valid before proceeding
if (mailGroup.MailInfo == null || mailGroup.ProjectId == Guid.Empty)
{
Interlocked.Increment(ref invalidIdCount);
return;
}
var response = await reportHelper.GetProjectStatistics(
mailGroup.ProjectId,
mailGroup.Recipients,
mailGroup.MailInfo.Body,
mailGroup.MailInfo.Subject,
tenantId);
// Use a switch expression for cleaner counting
switch (response.StatusCode)
{
case 200: Interlocked.Increment(ref successCount); break;
case 404: Interlocked.Increment(ref notFoundCount); break;
case 400: Interlocked.Increment(ref invalidIdCount); break;
default: Interlocked.Increment(ref failureCount); break;
}
}
catch (Exception ex)
{
// 3. OPTIMIZATION: Make the process resilient.
// If one task fails unexpectedly, log it and continue with others.
_logger.LogError("Failed to send report for project {ProjectId} : {Error}", mailGroup.ProjectId, ex.Message);
Interlocked.Increment(ref failureCount);
}
}
}).ToList();
await Task.WhenAll(sendTasks);
//var response = await GetProjectStatistics(Guid.Parse("2618eb89-2823-11f0-9d9e-bc241163f504"), "ashutosh.nehete@marcoaiot.com", tenantId);
var summaryMessage = $"Processing complete. Success: {successCount}, Not Found: {notFoundCount}, Invalid ID: {invalidIdCount}, Failures: {failureCount}.";
_logger.LogInfo(
"Emails of project reports sent for tenant {TenantId}. Successfully sent: {SuccessCount}, Projects not found: {NotFoundCount}, Invalid IDs: {InvalidIdsCount}",
tenantId, successCount, notFoundCount, invalidIdCount);
"Project report sending complete for tenant {TenantId}. Success: {SuccessCount}, Not Found: {NotFoundCount}, Invalid ID: {InvalidIdCount}, Failures: {FailureCount}",
tenantId, successCount, notFoundCount, invalidIdCount, failureCount);
return Ok(ApiResponse<object>.SuccessResponse(
new { },
$"Reports sent successfully: {successCount}. Projects not found: {notFoundCount}. Invalid IDs: {invalidIdCount}.",
new { successCount, notFoundCount, invalidIdCount, failureCount },
summaryMessage,
200));
}
/// <summary>
/// Retrieves project statistics for a given project ID and sends an email report.
/// </summary>
/// <param name="projectId">The ID of the project.</param>
/// <param name="recipientEmail">The email address of the recipient.</param>
/// <returns>An ApiResponse indicating the success or failure of retrieving statistics and sending the email.</returns>
private async Task<ApiResponse<object>> GetProjectStatistics(Guid projectId, List<string> recipientEmails, string body, string subject, Guid tenantId)
//[HttpPost("add-report-mail1")]
//public async Task<IActionResult> StoreProjectStatistics1()
//{
// Guid tenantId = _userHelper.GetTenantId();
// // Use AsNoTracking() for read-only queries to improve performance
// List<MailDetails> mailDetails = await _context.MailDetails
// .AsNoTracking()
// .Include(m => m.MailBody)
// .Where(m => m.TenantId == tenantId)
// .ToListAsync();
// var groupedMails = mailDetails
// .GroupBy(m => new { m.ProjectId, m.MailListId })
// .Select(g => new
// {
// ProjectId = g.Key.ProjectId,
// MailListId = g.Key.MailListId,
// Recipients = g.Select(m => m.Recipient).Distinct().ToList(),
// MailBody = g.FirstOrDefault()?.MailBody?.Body ?? "",
// Subject = g.FirstOrDefault()?.MailBody?.Subject ?? string.Empty,
// })
// .ToList();
// foreach (var groupMail in groupedMails)
// {
// var projectId = groupMail.ProjectId;
// var body = groupMail.MailBody;
// var subject = groupMail.Subject;
// var receivers = groupMail.Recipients;
// if (projectId == Guid.Empty)
// {
// _logger.LogError("Provided empty project ID while fetching project report.");
// return NotFound(ApiResponse<object>.ErrorResponse("Provided empty Project ID.", "Provided empty Project ID.", 400));
// }
// var statisticReport = await _reportHelper.GetDailyProjectReport(projectId, tenantId);
// if (statisticReport == null)
// {
// _logger.LogWarning("User attempted to fetch project progress for project ID {ProjectId} but not found.", projectId);
// return NotFound(ApiResponse<object>.ErrorResponse("Project not found.", "Project not found.", 404));
// }
// var date = statisticReport.Date.ToString("dd-MMM-yyyy", CultureInfo.InvariantCulture);
// // Send Email
// var emailBody = await _emailSender.SendProjectStatisticsEmail(new List<string>(), body, subject, statisticReport);
// var subjectReplacements = new Dictionary<string, string>
// {
// {"DATE", date },
// {"PROJECT_NAME", statisticReport.ProjectName}
// };
// foreach (var item in subjectReplacements)
// {
// subject = subject.Replace($"{{{{{item.Key}}}}}", item.Value);
// }
// string env = _configuration["environment:Title"] ?? string.Empty;
// if (string.IsNullOrWhiteSpace(env))
// {
// subject = $"{subject}";
// }
// else
// {
// subject = $"({env}) {subject}";
// }
// var mail = new ProjectReportEmailMongoDB
// {
// IsSent = false,
// Body = emailBody,
// Receivers = receivers,
// Subject = subject,
// };
// await _cache.AddProjectReportMail(mail);
// }
// return Ok(ApiResponse<object>.SuccessResponse("Project Report Mail is stored in MongoDB", "Project Report Mail is stored in MongoDB", 200));
//}
[HttpPost("add-report-mail")]
public async Task<IActionResult> StoreProjectStatistics()
{
Guid tenantId = _userHelper.GetTenantId();
if (projectId == Guid.Empty)
// 1. Database-Side Grouping (Still the most efficient way to get initial data)
var projectMailGroups = await _context.MailDetails
.AsNoTracking()
.Where(m => m.TenantId == tenantId && m.ProjectId != Guid.Empty)
.GroupBy(m => new { m.ProjectId, m.MailListId })
.Select(g => new
{
g.Key.ProjectId,
Recipients = g.Select(m => m.Recipient).Distinct().ToList(),
MailInfo = g.Select(m => new { Body = m.MailBody != null ? m.MailBody.Body : "", Subject = m.MailBody != null ? m.MailBody.Subject : "" }).FirstOrDefault()
})
.ToListAsync();
if (!projectMailGroups.Any())
{
_logger.LogError("Provided empty project ID while fetching project report.");
return ApiResponse<object>.ErrorResponse("Provided empty Project ID.", "Provided empty Project ID.", 400);
_logger.LogInfo("No project mail details found for tenant {TenantId} to process.", tenantId);
return Ok(ApiResponse<object>.SuccessResponse("No project reports to generate.", "No project reports to generate.", 200));
}
string env = _configuration["environment:Title"] ?? string.Empty;
var statisticReport = await _reportHelper.GetDailyProjectReport(projectId, tenantId);
if (statisticReport == null)
// 2. Process each group concurrently, but with isolated DBContexts.
var processingTasks = projectMailGroups.Select(async group =>
{
_logger.LogWarning("User attempted to fetch project progress for project ID {ProjectId} but not found.", projectId);
return ApiResponse<object>.ErrorResponse("Project not found.", "Project not found.", 404);
}
// SOLUTION: Create a new DI scope for each parallel task.
using (var scope = _serviceScopeFactory.CreateScope())
{
// Resolve services from this new, isolated scope.
// These helpers will get their own fresh DbContext instance.
var reportHelper = scope.ServiceProvider.GetRequiredService<ReportHelper>();
var emailSender = scope.ServiceProvider.GetRequiredService<IEmailSender>();
var cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>(); // e.g., IProjectReportCache
// Send Email
var emailBody = await _emailSender.SendProjectStatisticsEmail(recipientEmails, body, subject, statisticReport);
var employee = await _context.Employees.FirstOrDefaultAsync(e => e.Email != null && recipientEmails.Contains(e.Email)) ?? new Employee();
List<MailLog> mailLogs = new List<MailLog>();
foreach (var recipientEmail in recipientEmails)
{
mailLogs.Add(
new MailLog
// The rest of the logic is the same, but now it's thread-safe.
try
{
ProjectId = projectId,
EmailId = recipientEmail,
Body = emailBody,
EmployeeId = employee.Id,
TimeStamp = DateTime.UtcNow,
TenantId = tenantId
});
var projectId = group.ProjectId;
var statisticReport = await reportHelper.GetDailyProjectReport(projectId, tenantId);
if (statisticReport == null)
{
_logger.LogWarning("Statistic report for project ID {ProjectId} not found. Skipping.", projectId);
return;
}
if (group.MailInfo == null)
{
_logger.LogWarning("MailBody info for project ID {ProjectId} not found. Skipping.", projectId);
return;
}
var date = statisticReport.Date.ToString("dd-MMM-yyyy", CultureInfo.InvariantCulture);
// Assuming the first param to SendProjectStatisticsEmail was just a placeholder
var emailBody = await emailSender.SendProjectStatisticsEmail(new List<string>(), group.MailInfo.Body, string.Empty, statisticReport);
string subject = group.MailInfo.Subject
.Replace("{{DATE}}", date)
.Replace("{{PROJECT_NAME}}", statisticReport.ProjectName);
subject = string.IsNullOrWhiteSpace(env) ? subject : $"({env}) {subject}";
var mail = new ProjectReportEmailMongoDB
{
IsSent = false,
Body = emailBody,
Receivers = group.Recipients,
Subject = subject,
};
await cache.AddProjectReportMail(mail);
}
catch (Exception ex)
{
// It's good practice to log any unexpected errors within a concurrent task.
_logger.LogError("Failed to process project report for ProjectId {ProjectId} : {Error}", group.ProjectId, ex.Message);
}
}
});
// Await all the concurrent, now thread-safe, tasks.
await Task.WhenAll(processingTasks);
return Ok(ApiResponse<object>.SuccessResponse(
$"{projectMailGroups.Count} Project Report Mail(s) are queued for storage.",
"Project Report Mail processing initiated.",
200));
}
[HttpGet("report-mail")]
public async Task<IActionResult> GetProjectStatisticsFromCache()
{
var mailList = await _cache.GetProjectReportMail(false);
if (mailList == null)
{
return NotFound(ApiResponse<object>.ErrorResponse("Not mail found", "Not mail found", 404));
}
_context.MailLogs.AddRange(mailLogs);
await _context.SaveChangesAsync();
return ApiResponse<object>.SuccessResponse(statisticReport, "Email sent successfully", 200);
return Ok(ApiResponse<object>.SuccessResponse(mailList, "Fetched list of mail body successfully", 200));
}
}
}

View File

@ -150,18 +150,24 @@ namespace MarcoBMS.Services.Service
emailBody = emailBody.Replace("{{TEAM_ON_SITE}}", BuildTeamOnSiteHtml(report.TeamOnSite));
emailBody = emailBody.Replace("{{PERFORMED_TASK}}", BuildPerformedTaskHtml(report.PerformedTasks, report.Date));
emailBody = emailBody.Replace("{{PERFORMED_ATTENDANCE}}", BuildPerformedAttendanceHtml(report.PerformedAttendance));
var subjectReplacements = new Dictionary<string, string>
if (!string.IsNullOrWhiteSpace(subject))
{
{"DATE", date },
{"PROJECT_NAME", report.ProjectName}
};
foreach (var item in subjectReplacements)
{
subject = subject.Replace($"{{{{{item.Key}}}}}", item.Value);
var subjectReplacements = new Dictionary<string, string>
{
{"DATE", date },
{"PROJECT_NAME", report.ProjectName}
};
foreach (var item in subjectReplacements)
{
subject = subject.Replace($"{{{{{item.Key}}}}}", item.Value);
}
string env = _configuration["environment:Title"] ?? string.Empty;
subject = CheckSubject(subject);
}
if (toEmails.Count > 0)
{
await SendEmailAsync(toEmails, subject, emailBody);
}
string env = _configuration["environment:Title"] ?? string.Empty;
subject = CheckSubject(subject);
await SendEmailAsync(toEmails, subject, emailBody);
return emailBody;
}
public async Task SendOTP(List<string> toEmails, string emailBody, string name, string otp, string subject)