Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public interface IAttachmentAppService : IApplicationService
{
Task<IList<ApplicationAttachmentDto>> GetApplicationAsync(Guid applicationId);
Task<IList<AssessmentAttachmentDto>> GetAssessmentAsync(Guid assessmentId);
Task<IList<UnityAttachmentDto>> GetApplicantAsync(Guid applicantId);
Task ResyncSubmissionAttachmentsAsync(Guid applicationId);
Task<IList<UnityAttachmentDto>> GetAttachmentsAsync(AttachmentParametersDto attachmentParametersDto);
Task<AttachmentMetadataDto> GetAttachmentMetadataAsync(AttachmentType attachmentType, Guid attachmentId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class AttachmentAppService(
IApplicationAttachmentRepository applicationAttachmentRepository,
IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository,
IAssessmentAttachmentRepository assessmentAttachmentRepository,
IApplicantAttachmentRepository applicantAttachmentRepository,
IIntakeFormSubmissionManager intakeFormSubmissionManager,
IPersonRepository personUserRepository) : ApplicationService, IAttachmentAppService
{
Expand Down Expand Up @@ -54,6 +55,11 @@ public async Task<IList<AssessmentAttachmentDto>> GetAssessmentAsync(Guid assess
}).ToList();
}

public async Task<IList<UnityAttachmentDto>> GetApplicantAsync(Guid applicantId)
{
return await GetAttachmentsAsync(new AttachmentParametersDto(AttachmentType.APPLICANT, applicantId));
}

public async Task<List<ApplicationChefsFileAttachment>> GetApplicationChefsFileAttachmentsAsync(Guid applicationId)
{
return await applicationChefsFileAttachmentRepository.GetListAsync(applicationId);
Expand All @@ -79,6 +85,9 @@ public async Task<IList<UnityAttachmentDto>> GetAttachmentsAsync(AttachmentParam
AttachmentType.ASSESSMENT => await GetAttachmentsInternalAsync(
assessmentAttachmentRepository,
attachment => attachment.AssessmentId == attachmentParametersDto.AttachedResourceId),
AttachmentType.APPLICANT => await GetAttachmentsInternalAsync(
applicantAttachmentRepository,
attachment => attachment.ApplicantId == attachmentParametersDto.AttachedResourceId),
_ => throw new ArgumentException("Attachment type is not supported", nameof(attachmentParametersDto)),
};
}
Expand Down Expand Up @@ -117,6 +126,8 @@ public async Task<AttachmentMetadataDto> GetAttachmentMetadataAsync(AttachmentTy
attachmentId, assessmentAttachmentRepository),
AttachmentType.CHEFS => await GetMetadataInternalAsync(
attachmentId, applicationChefsFileAttachmentRepository),
AttachmentType.APPLICANT => await GetMetadataInternalAsync(
attachmentId, applicantAttachmentRepository),
_ => throw new ArgumentException("Invalid attachment type", nameof(attachmentType)),
};
}
Expand Down Expand Up @@ -152,6 +163,10 @@ public async Task<AttachmentMetadataDto> UpdateAttachmentMetadataAsync(UpdateAtt
updateAttachment,
applicationChefsFileAttachmentRepository,
AttachmentType.CHEFS),
AttachmentType.APPLICANT => await UpdateMetadataInternalAsync(
updateAttachment,
applicantAttachmentRepository,
AttachmentType.APPLICANT),
_ => throw new ArgumentException("Invalid attachment type", nameof(updateAttachment)),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ public partial class S3BlobProvider : BlobProviderBase, ITransientDependency
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IApplicationAttachmentRepository _applicationAttachmentRepository;
private readonly IAssessmentAttachmentRepository _assessmentAttachmentRepository;
private readonly IAssessmentAttachmentRepository _assessmentAttachmentRepository;
private readonly IApplicantAttachmentRepository _applicantAttachmentRepository;
private readonly AmazonS3Client _amazonS3Client;

public S3BlobProvider(IHttpContextAccessor httpContextAccessor, IApplicationAttachmentRepository attachmentRepository, IAssessmentAttachmentRepository assessmentAttachmentRepository, IConfiguration configuration)
public S3BlobProvider(IHttpContextAccessor httpContextAccessor, IApplicationAttachmentRepository attachmentRepository, IAssessmentAttachmentRepository assessmentAttachmentRepository, IApplicantAttachmentRepository applicantAttachmentRepository, IConfiguration configuration)
{
_httpContextAccessor = httpContextAccessor;
_applicationAttachmentRepository = attachmentRepository;
_assessmentAttachmentRepository = assessmentAttachmentRepository;
_applicantAttachmentRepository = applicantAttachmentRepository;

AmazonS3Config s3config = new()
{
Expand Down Expand Up @@ -89,6 +91,19 @@ public override async Task<bool> DeleteAsync(BlobProviderDeleteArgs args)
await _assessmentAttachmentRepository.DeleteAsync(attachment);
}
}
else if (attachmentType == "Applicant")
{
if (attachmentTypeId.IsNullOrEmpty())
{
throw new AbpValidationException("Missing ApplicantId");
}
IQueryable<ApplicantAttachment> queryableAttachment = _applicantAttachmentRepository.GetQueryableAsync().Result;
ApplicantAttachment? attachment = queryableAttachment.FirstOrDefault(a => a.S3ObjectKey.Equals(s3ObjectKey) && a.ApplicantId.Equals(new Guid(attachmentTypeId.ToString())));
if (attachment != null)
{
await _applicantAttachmentRepository.DeleteAsync(attachment);
}
Comment on lines +94 to +105
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new Applicant delete branch uses GetQueryableAsync().Result (sync-over-async) inside an async method. This can cause thread-pool starvation and makes failures harder to diagnose. Await GetQueryableAsync() and keep the query fully async.

Copilot uses AI. Check for mistakes.
}
else
{
throw new AbpValidationException("Wrong AttachmentType:"+attachmentType);
Expand Down Expand Up @@ -146,30 +161,34 @@ public override async Task SaveAsync(BlobProviderSaveArgs args)
var httpContext = _httpContextAccessor.HttpContext ?? throw new InvalidOperationException("No active HttpContext.");
var queryParams = httpContext.Request?.Query ?? throw new InvalidOperationException("No query parameters in the current request.");
var routeData = _httpContextAccessor.HttpContext.GetRouteData();
var assessmentId = routeData.Values["assessmentId"];

var assessmentId = routeData.Values["assessmentId"];
var applicationId = routeData.Values["applicationId"];
var applicantId = routeData.Values["applicantId"];
queryParams.TryGetValue("userId", out StringValues currentUserId);
if (assessmentId != null)
{
queryParams.TryGetValue("userId", out StringValues currentUserId);
#pragma warning disable CS8604 // Possible null reference argument.

#pragma warning disable CS8604 // Possible null reference argument.
await UploadAssessmentAttachment(args, assessmentId.ToString(), currentUserId.ToString());
#pragma warning restore CS8604 // Possible null reference argument.
#pragma warning restore CS8604 // Possible null reference argument.
}
else if(applicationId != null)
{
#pragma warning disable CS8604 // Possible null reference argument.
await UploadApplicationAttachment(args, applicationId.ToString(), currentUserId.ToString());
#pragma warning restore CS8604 // Possible null reference argument.
}
else if (applicantId != null)
{
#pragma warning disable CS8604 // Possible null reference argument.
await UploadApplicantAttachment(args, applicantId.ToString(), currentUserId.ToString());
#pragma warning restore CS8604 // Possible null reference argument.
}
else
{
var applicationId = routeData.Values["applicationId"];
if(applicationId != null)
{
queryParams.TryGetValue("userId", out StringValues currentUserId);
#pragma warning disable CS8604 // Possible null reference argument.
await UploadApplicationAttachment(args, applicationId.ToString(), currentUserId.ToString());
#pragma warning restore CS8604 // Possible null reference argument.
}
else
{
throw new AbpValidationException("Missing parameter: applicationId/assessmentId");
}
}
throw new AbpValidationException("Missing parameter: applicationId/assessmentId/applicantId");
}
}

private async Task UploadAssessmentAttachment(BlobProviderSaveArgs args, string assessmentId, string currentUserId)
Expand Down Expand Up @@ -246,6 +265,43 @@ await _applicationAttachmentRepository.InsertAsync(
}
}

private async Task UploadApplicantAttachment(BlobProviderSaveArgs args, string applicantId, string currentUserId)
{
var config = args.Configuration.GetS3BlobProviderConfiguration();
var bucket = config.Bucket;
var folder = args.Configuration.GetS3BlobProviderConfiguration().ApplicantS3Folder;
if (!folder.EndsWith('/'))
{
folder += "/";
}
folder += applicantId;
var key = folder + "/" + args.BlobName;
var escapedKey = folder + "/" + Uri.EscapeDataString(args.BlobName);
var mimeType = GetMimeType(args.BlobName);
await UploadToS3(args, bucket, escapedKey, mimeType);
IQueryable<ApplicantAttachment> queryableAttachment = _applicantAttachmentRepository.GetQueryableAsync().Result;
ApplicantAttachment? attachment = queryableAttachment.FirstOrDefault(a => a.S3ObjectKey.Equals(key) && a.ApplicantId.Equals(new Guid(applicantId)));
if (attachment == null)
{
await _applicantAttachmentRepository.InsertAsync(
new ApplicantAttachment
{
ApplicantId = new Guid(applicantId),
S3ObjectKey = key,
UserId = new Guid(currentUserId),
FileName = args.BlobName,
Time = DateTime.UtcNow,
});
}
else
{
attachment.UserId = new Guid(currentUserId);
attachment.FileName = args.BlobName;
attachment.Time = DateTime.UtcNow;
await _applicantAttachmentRepository.UpdateAsync(attachment);
}
}
Comment on lines +268 to +303
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UploadApplicantAttachment uses GetQueryableAsync().Result and synchronously queries for an existing row. This is sync-over-async and can impact scalability under load. Await GetQueryableAsync() and keep the DB query async (or use repository methods that avoid pulling an IQueryable synchronously).

Copilot uses AI. Check for mistakes.

public async Task UploadToS3(BlobProviderSaveArgs args, string bucket, string key, string mimeType)
{
byte[] fileBytes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,12 @@ public string AssessmentS3Folder
.GetConfiguration<string>("S3BlobProvider.AssessmentS3Folder");
set => _containerConfiguration
.SetConfiguration("S3BlobProvider.AssessmentS3Folder", value);
}
public string ApplicantS3Folder
{
get => _containerConfiguration
.GetConfiguration<string>("S3BlobProvider.ApplicantS3Folder");
set => _containerConfiguration
.SetConfiguration("S3BlobProvider.ApplicantS3Folder", value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ public override void ConfigureServices(ServiceConfigurationContext context)
provider.SecretAccessKey = configuration["S3:SecretAccessKey"] ?? "";
provider.ApplicationS3Folder = configuration["S3:ApplicationS3Folder"] ?? "";
provider.AssessmentS3Folder = configuration["S3:AssessmentS3Folder"] ?? "";
provider.ApplicantS3Folder = configuration["S3:ApplicantS3Folder"] ?? "";
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ public enum AttachmentType
APPLICATION = 0,
ASSESSMENT = 1,
CHEFS = 2,
EMAIL = 3
EMAIL = 3,
APPLICANT = 4
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
using Unity.GrantManager.Attachments;

namespace Unity.GrantManager.Applications;

public class ApplicantAttachment : AbstractS3Attachment
{
[NotMapped]
public override AttachmentType AttachmentType => AttachmentType.APPLICANT;
public Guid ApplicantId { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;

namespace Unity.GrantManager.Applications
{
public interface IApplicantAttachmentRepository : IRepository<ApplicantAttachment, Guid>
{
Task<List<ApplicantAttachment>> GetListAsync(
int skipCount,
int maxResultCount,
string sorting,
string filter
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public class GrantTenantDbContext : AbpDbContext<GrantTenantDbContext>
public DbSet<ApplicationTags> ApplicationTags { get; set; }
public DbSet<ApplicantAgent> ApplicantAgents { get; set; }
public DbSet<ApplicantComment> ApplicantComments { get; set; }
public DbSet<ApplicantAttachment> ApplicantAttachments { get; set; }
public DbSet<ApplicationAttachment> ApplicationAttachments { get; set; }
public DbSet<ApplicationFormSubmission> ApplicationFormSubmissions { get; set; }
public DbSet<AssessmentAttachment> AssessmentAttachments { get; set; }
Expand Down Expand Up @@ -217,6 +218,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.HasForeignKey(x => x.CommenterId);
});

modelBuilder.Entity<ApplicantAttachment>(b =>
{
b.ToTable(GrantManagerConsts.TenantTablePrefix + "ApplicantAttachments", GrantManagerConsts.DbSchema);

b.ConfigureByConvention();
b.HasOne<Applicant>().WithMany().HasForeignKey(x => x.ApplicantId).IsRequired();
});

modelBuilder.Entity<ApplicationAttachment>(b =>
{
b.ToTable(GrantManagerConsts.TenantTablePrefix + "ApplicationAttachments", GrantManagerConsts.DbSchema);
Expand Down
Loading
Loading