Refactor project report APIs to improve performance and readability
This commit is contained in:
parent
852b079428
commit
d27cdee72d
45
Marco.Pms.CacheHelper/ReportCache.cs
Normal file
45
Marco.Pms.CacheHelper/ReportCache.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
Marco.Pms.Model/MongoDBModels/ProjectReportEmailMongoDB.cs
Normal file
16
Marco.Pms.Model/MongoDBModels/ProjectReportEmailMongoDB.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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.Dtos.Mail;
|
||||||
using Marco.Pms.Model.Employees;
|
|
||||||
using Marco.Pms.Model.Mail;
|
using Marco.Pms.Model.Mail;
|
||||||
|
using Marco.Pms.Model.MongoDBModels;
|
||||||
using Marco.Pms.Model.Utilities;
|
using Marco.Pms.Model.Utilities;
|
||||||
using Marco.Pms.Services.Helpers;
|
using Marco.Pms.Services.Helpers;
|
||||||
using MarcoBMS.Services.Helpers;
|
using MarcoBMS.Services.Helpers;
|
||||||
using MarcoBMS.Services.Service;
|
using MarcoBMS.Services.Service;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
|
using System.Data;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Net.Mail;
|
||||||
|
|
||||||
namespace Marco.Pms.Services.Controllers
|
namespace Marco.Pms.Services.Controllers
|
||||||
{
|
{
|
||||||
@ -25,7 +28,11 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
private readonly UserHelper _userHelper;
|
private readonly UserHelper _userHelper;
|
||||||
private readonly IWebHostEnvironment _env;
|
private readonly IWebHostEnvironment _env;
|
||||||
private readonly ReportHelper _reportHelper;
|
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;
|
_context = context;
|
||||||
_emailSender = emailSender;
|
_emailSender = emailSender;
|
||||||
@ -33,27 +40,122 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
_userHelper = userHelper;
|
_userHelper = userHelper;
|
||||||
_env = env;
|
_env = env;
|
||||||
_reportHelper = reportHelper;
|
_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)
|
public async Task<IActionResult> AddMailDetails([FromBody] MailDetailsDto mailDetailsDto)
|
||||||
{
|
{
|
||||||
|
// 1. Get Tenant ID and Basic Authorization Check
|
||||||
Guid tenantId = _userHelper.GetTenantId();
|
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,
|
ProjectId = mailDetailsDto.ProjectId,
|
||||||
Recipient = mailDetailsDto.Recipient,
|
Recipient = mailDetailsDto.Recipient,
|
||||||
Schedule = mailDetailsDto.Schedule,
|
Schedule = mailDetailsDto.Schedule,
|
||||||
MailListId = mailDetailsDto.MailListId,
|
MailListId = mailDetailsDto.MailListId,
|
||||||
TenantId = tenantId
|
TenantId = tenantId,
|
||||||
};
|
};
|
||||||
_context.MailDetails.Add(mailDetails);
|
|
||||||
await _context.SaveChangesAsync();
|
try
|
||||||
return Ok("Success");
|
{
|
||||||
|
_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")]
|
[HttpPost("mail-template1")]
|
||||||
public async Task<IActionResult> AddMailTemplate([FromBody] MailTemeplateDto mailTemeplateDto)
|
public async Task<IActionResult> AddMailTemplate1([FromBody] MailTemeplateDto mailTemeplateDto)
|
||||||
{
|
{
|
||||||
Guid tenantId = _userHelper.GetTenantId();
|
Guid tenantId = _userHelper.GetTenantId();
|
||||||
if (string.IsNullOrWhiteSpace(mailTemeplateDto.Body) && string.IsNullOrWhiteSpace(mailTemeplateDto.Title))
|
if (string.IsNullOrWhiteSpace(mailTemeplateDto.Body) && string.IsNullOrWhiteSpace(mailTemeplateDto.Title))
|
||||||
@ -80,116 +182,376 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
return Ok("Success");
|
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")]
|
[HttpGet("project-statistics")]
|
||||||
public async Task<IActionResult> SendProjectReport()
|
public async Task<IActionResult> SendProjectReport()
|
||||||
{
|
{
|
||||||
Guid tenantId = _userHelper.GetTenantId();
|
Guid tenantId = _userHelper.GetTenantId();
|
||||||
|
|
||||||
// Use AsNoTracking() for read-only queries to improve performance
|
// 1. OPTIMIZATION: Perform grouping and projection on the database server.
|
||||||
List<MailDetails> mailDetails = await _context.MailDetails
|
// This is far more efficient than loading all entities into memory.
|
||||||
|
var projectMailGroups = await _context.MailDetails
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(m => m.MailBody)
|
|
||||||
.Where(m => m.TenantId == tenantId)
|
.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 })
|
.GroupBy(m => new { m.ProjectId, m.MailListId })
|
||||||
.Select(g => new
|
.Select(g => new
|
||||||
{
|
{
|
||||||
ProjectId = g.Key.ProjectId,
|
ProjectId = g.Key.ProjectId,
|
||||||
MailListId = g.Key.MailListId,
|
|
||||||
Recipients = g.Select(m => m.Recipient).Distinct().ToList(),
|
Recipients = g.Select(m => m.Recipient).Distinct().ToList(),
|
||||||
MailBody = g.FirstOrDefault()?.MailBody?.Body ?? "",
|
// Project the mail body and subject from the first record in the group
|
||||||
Subject = g.FirstOrDefault()?.MailBody?.Subject ?? string.Empty,
|
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);
|
if (!projectMailGroups.Any())
|
||||||
|
|
||||||
// Using Task.WhenAll to send reports concurrently for better performance
|
|
||||||
var sendTasks = groupedMails.Select(async mailDetail =>
|
|
||||||
{
|
{
|
||||||
await semaphore.WaitAsync();
|
return Ok(ApiResponse<object>.SuccessResponse(new { }, "No projects found to send reports for.", 200));
|
||||||
try
|
}
|
||||||
|
|
||||||
|
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);
|
// Resolve a new instance of the helper from this isolated scope.
|
||||||
if (response.StatusCode == 200)
|
// This ensures each task gets its own thread-safe DbContext.
|
||||||
Interlocked.Increment(ref successCount);
|
var reportHelper = scope.ServiceProvider.GetRequiredService<ReportHelper>();
|
||||||
else if (response.StatusCode == 404)
|
|
||||||
Interlocked.Increment(ref notFoundCount);
|
try
|
||||||
else if (response.StatusCode == 400)
|
{
|
||||||
Interlocked.Increment(ref invalidIdCount);
|
// Ensure MailInfo and ProjectId are valid before proceeding
|
||||||
}
|
if (mailGroup.MailInfo == null || mailGroup.ProjectId == Guid.Empty)
|
||||||
finally
|
{
|
||||||
{
|
Interlocked.Increment(ref invalidIdCount);
|
||||||
semaphore.Release();
|
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();
|
}).ToList();
|
||||||
|
|
||||||
await Task.WhenAll(sendTasks);
|
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(
|
_logger.LogInfo(
|
||||||
"Emails of project reports sent for tenant {TenantId}. Successfully sent: {SuccessCount}, Projects not found: {NotFoundCount}, Invalid IDs: {InvalidIdsCount}",
|
"Project report sending complete for tenant {TenantId}. Success: {SuccessCount}, Not Found: {NotFoundCount}, Invalid ID: {InvalidIdCount}, Failures: {FailureCount}",
|
||||||
tenantId, successCount, notFoundCount, invalidIdCount);
|
tenantId, successCount, notFoundCount, invalidIdCount, failureCount);
|
||||||
|
|
||||||
return Ok(ApiResponse<object>.SuccessResponse(
|
return Ok(ApiResponse<object>.SuccessResponse(
|
||||||
new { },
|
new { successCount, notFoundCount, invalidIdCount, failureCount },
|
||||||
$"Reports sent successfully: {successCount}. Projects not found: {notFoundCount}. Invalid IDs: {invalidIdCount}.",
|
summaryMessage,
|
||||||
200));
|
200));
|
||||||
}
|
}
|
||||||
/// <summary>
|
|
||||||
/// Retrieves project statistics for a given project ID and sends an email report.
|
//[HttpPost("add-report-mail1")]
|
||||||
/// </summary>
|
//public async Task<IActionResult> StoreProjectStatistics1()
|
||||||
/// <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>
|
// Guid tenantId = _userHelper.GetTenantId();
|
||||||
private async Task<ApiResponse<object>> GetProjectStatistics(Guid projectId, List<string> recipientEmails, string body, string subject, Guid tenantId)
|
|
||||||
|
// // 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.");
|
_logger.LogInfo("No project mail details found for tenant {TenantId} to process.", tenantId);
|
||||||
return ApiResponse<object>.ErrorResponse("Provided empty Project ID.", "Provided empty Project ID.", 400);
|
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);
|
// 2. Process each group concurrently, but with isolated DBContexts.
|
||||||
|
var processingTasks = projectMailGroups.Select(async group =>
|
||||||
if (statisticReport == null)
|
|
||||||
{
|
{
|
||||||
_logger.LogWarning("User attempted to fetch project progress for project ID {ProjectId} but not found.", projectId);
|
// SOLUTION: Create a new DI scope for each parallel task.
|
||||||
return ApiResponse<object>.ErrorResponse("Project not found.", "Project not found.", 404);
|
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
|
// The rest of the logic is the same, but now it's thread-safe.
|
||||||
var emailBody = await _emailSender.SendProjectStatisticsEmail(recipientEmails, body, subject, statisticReport);
|
try
|
||||||
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
|
|
||||||
{
|
{
|
||||||
ProjectId = projectId,
|
var projectId = group.ProjectId;
|
||||||
EmailId = recipientEmail,
|
var statisticReport = await reportHelper.GetDailyProjectReport(projectId, tenantId);
|
||||||
Body = emailBody,
|
|
||||||
EmployeeId = employee.Id,
|
if (statisticReport == null)
|
||||||
TimeStamp = DateTime.UtcNow,
|
{
|
||||||
TenantId = tenantId
|
_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);
|
return Ok(ApiResponse<object>.SuccessResponse(mailList, "Fetched list of mail body successfully", 200));
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
return ApiResponse<object>.SuccessResponse(statisticReport, "Email sent successfully", 200);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -150,18 +150,24 @@ namespace MarcoBMS.Services.Service
|
|||||||
emailBody = emailBody.Replace("{{TEAM_ON_SITE}}", BuildTeamOnSiteHtml(report.TeamOnSite));
|
emailBody = emailBody.Replace("{{TEAM_ON_SITE}}", BuildTeamOnSiteHtml(report.TeamOnSite));
|
||||||
emailBody = emailBody.Replace("{{PERFORMED_TASK}}", BuildPerformedTaskHtml(report.PerformedTasks, report.Date));
|
emailBody = emailBody.Replace("{{PERFORMED_TASK}}", BuildPerformedTaskHtml(report.PerformedTasks, report.Date));
|
||||||
emailBody = emailBody.Replace("{{PERFORMED_ATTENDANCE}}", BuildPerformedAttendanceHtml(report.PerformedAttendance));
|
emailBody = emailBody.Replace("{{PERFORMED_ATTENDANCE}}", BuildPerformedAttendanceHtml(report.PerformedAttendance));
|
||||||
var subjectReplacements = new Dictionary<string, string>
|
if (!string.IsNullOrWhiteSpace(subject))
|
||||||
{
|
{
|
||||||
{"DATE", date },
|
var subjectReplacements = new Dictionary<string, string>
|
||||||
{"PROJECT_NAME", report.ProjectName}
|
{
|
||||||
};
|
{"DATE", date },
|
||||||
foreach (var item in subjectReplacements)
|
{"PROJECT_NAME", report.ProjectName}
|
||||||
{
|
};
|
||||||
subject = subject.Replace($"{{{{{item.Key}}}}}", item.Value);
|
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;
|
return emailBody;
|
||||||
}
|
}
|
||||||
public async Task SendOTP(List<string> toEmails, string emailBody, string name, string otp, string subject)
|
public async Task SendOTP(List<string> toEmails, string emailBody, string name, string otp, string subject)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user