2 Commits
Author SHA1 Message Date
hermes 8e0c099867 Stable multitenancy 2026-09-24 10:25:07 +02:00
hermes ef134a5b3c Implemented audit logging and automated logging 2026-09-23 05:33:16 +02:00
67 changed files with 9134 additions and 625 deletions
+5 -2
View File
@@ -20,8 +20,11 @@
}, },
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Warning",
"Microsoft.AspNetCore": "Warning" "Quartz": "Error",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore.Database": "Error",
"Microsoft.EntityFrameworkCore.Update": "Error"
} }
}, },
"EmailProcessorCron": "0 */5 * * * ?", "EmailProcessorCron": "0 */5 * * * ?",
@@ -7,12 +7,16 @@ namespace PostFundManagement.Application.Abstractions.Services;
public interface ICommunicationsService public interface ICommunicationsService
{ {
[Log]
[Audit] [Audit]
Task<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
Task<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default);
[Log]
Task<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default); Task<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default);
[Log]
Task<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default); Task<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default);
} }
@@ -7,32 +7,44 @@ namespace PostFundManagement.Application.Abstractions.Services;
public interface IEvidenceService public interface IEvidenceService
{ {
[Log]
[Audit] [Audit]
Task<Result> CreateEvidenceAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default); Task<Result> CreateEvidenceAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default); Task<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default);
[Log]
Task<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default);
[Log]
Task<Result<Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result> CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
Task<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken); Task<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken);
[Log]
Task<Result<Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default); Task<Result<Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default);
[Log]
Task<Result<Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default); Task<Result<Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default); Task<Result<Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> UpdateEvidenceAsync(UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result> UpdateEvidenceAsync(UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
Task<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default); Task<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default);
} }
@@ -7,26 +7,35 @@ namespace PostFundManagement.Application.Abstractions.Services;
public interface IExecutionService public interface IExecutionService
{ {
[Log]
Task<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default);
[Log]
Task<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default); Task<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default);
[Log]
Task<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default); Task<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default); Task<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default); Task<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default);
} }
@@ -7,31 +7,43 @@ namespace PostFundManagement.Application.Abstractions.Services;
public interface IGovernanceService public interface IGovernanceService
{ {
[Log]
Task<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default); Task<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Portfolio[]>> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default); Task<Result<Portfolio[]>> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default);
[Log]
Task<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default); Task<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default); Task<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default); Task<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result<Organisation>> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default); Task<Result<Organisation>> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result> CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result> UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
Task<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default); Task<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default); Task<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
} }
@@ -7,36 +7,49 @@ namespace PostFundManagement.Application.Abstractions.Services;
public interface IGrantsService public interface IGrantsService
{ {
[Log]
Task<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default);
[Log]
Task<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default); Task<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default);
[Log]
Task<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default); Task<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default);
[Log]
Task<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default); Task<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default); Task<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default); Task<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default); Task<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default);
} }
@@ -4,6 +4,7 @@ namespace PostFundManagement.Application.Abstractions.Services;
public interface IIdentityService public interface IIdentityService
{ {
[Log]
[Audit] [Audit]
Task<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default); Task<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default);
} }
@@ -7,48 +7,67 @@ namespace PostFundManagement.Application.Abstractions.Services;
public interface IProjectService public interface IProjectService
{ {
[Log]
[Audit] [Audit]
Task<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
Task<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default);
[Log]
Task<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default); Task<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default); Task<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
Task<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default); Task<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken = default); Task<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default); Task<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
Task<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default); Task<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default); Task<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result> CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result> UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
Task<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default); Task<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default);
[Log]
Task<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default); Task<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default);
} }
@@ -7,13 +7,17 @@ namespace PostFundManagement.Application.Abstractions.Services;
public interface IUserService public interface IUserService
{ {
[Log]
[Audit] [Audit]
Task<Result> SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default); Task<Result> SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default);
[Log]
Task<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default); Task<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default);
[Log]
[Audit] [Audit]
Task<Result> UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default); Task<Result> UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default);
[Log]
Task<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken); Task<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken);
} }
@@ -16,7 +16,7 @@ namespace PostFundManagement.Application.Extensions;
public static class Services public static class Services
{ {
public static IServiceCollection AddAuditedCachedScoped<TInterface, TImplementation, TCacheDecorator>(this IServiceCollection services) public static IServiceCollection AddLoggedAuditedCachedServiceScoped<TInterface, TImplementation, TCacheDecorator>(this IServiceCollection services)
where TInterface : class where TInterface : class
where TImplementation : class, TInterface where TImplementation : class, TInterface
where TCacheDecorator : class, TInterface where TCacheDecorator : class, TInterface
@@ -27,30 +27,16 @@ public static class Services
services.AddScoped(sp => services.AddScoped(sp =>
{ {
var proxyGenerator = sp.GetRequiredService<ProxyGenerator>(); var proxyGenerator = sp.GetRequiredService<ProxyGenerator>();
var interceptor = sp.GetRequiredService<AuditInterceptor>(); var auditInterceptor = sp.GetRequiredService<AuditInterceptor>();
var loggingInterceptor = sp.GetRequiredService<LoggingInterceptor>();
var actual = sp.GetRequiredService<TImplementation>(); var actual = sp.GetRequiredService<TImplementation>();
var cached = ActivatorUtilities.CreateInstance<TCacheDecorator>(sp, actual); var cached = ActivatorUtilities.CreateInstance<TCacheDecorator>(sp, actual);
return proxyGenerator.CreateInterfaceProxyWithTarget<TInterface>(cached, interceptor.ToInterceptor()); return proxyGenerator.CreateInterfaceProxyWithTarget<TInterface>(cached,
}); loggingInterceptor.ToInterceptor(),
auditInterceptor.ToInterceptor());
return services;
}
public static IServiceCollection AddAuditedScoped<TInterface, TImplementation>(this IServiceCollection services)
where TInterface : class
where TImplementation : class, TInterface
{
services.AddScoped<TImplementation>();
services.AddScoped(sp =>
{
var proxyGenerator = sp.GetRequiredService<ProxyGenerator>();
var actual = sp.GetRequiredService<TImplementation>();
var interceptor = sp.GetRequiredService<AuditInterceptor>();
return proxyGenerator.CreateInterfaceProxyWithTarget<TInterface>(actual, interceptor.ToInterceptor());
}); });
return services; return services;
@@ -59,15 +45,16 @@ public static class Services
public static IServiceCollection AddRegisterServicesWithCaching(this IServiceCollection services, IConfiguration configuration) public static IServiceCollection AddRegisterServicesWithCaching(this IServiceCollection services, IConfiguration configuration)
{ {
services.AddScoped<AuditInterceptor>(); services.AddScoped<AuditInterceptor>();
services.AddScoped<LoggingInterceptor>();
services.AddSingleton<ProxyGenerator>(); services.AddSingleton<ProxyGenerator>();
services.AddAuditedCachedScoped<ICommunicationsService, CommunicationsService, CommunicationsCacheService>(); services.AddLoggedAuditedCachedServiceScoped<ICommunicationsService, CommunicationsService, CommunicationsCacheService>();
services.AddAuditedCachedScoped<IEvidenceService, EvidenceService, EvidenceCacheService>(); services.AddLoggedAuditedCachedServiceScoped<IEvidenceService, EvidenceService, EvidenceCacheService>();
services.AddAuditedCachedScoped<IUserService, UserService, UserCacheService>(); services.AddLoggedAuditedCachedServiceScoped<IUserService, UserService, UserCacheService>();
services.AddAuditedCachedScoped<IExecutionService, ExecutionService, ExecutionCacheService>(); services.AddLoggedAuditedCachedServiceScoped<IExecutionService, ExecutionService, ExecutionCacheService>();
services.AddAuditedCachedScoped<IGovernanceService, GovernanceService, GovernanceCacheService>(); services.AddLoggedAuditedCachedServiceScoped<IGovernanceService, GovernanceService, GovernanceCacheService>();
services.AddAuditedCachedScoped<IGrantsService, GrantsService, GrantsCacheService>(); services.AddLoggedAuditedCachedServiceScoped<IGrantsService, GrantsService, GrantsCacheService>();
services.AddAuditedCachedScoped<IProjectService, ProjectService, ProjectCacheService>(); services.AddLoggedAuditedCachedServiceScoped<IProjectService, ProjectService, ProjectCacheService>();
services.AddScoped<IIdentityService, IdentityService>(); services.AddScoped<IIdentityService, IdentityService>();
services.Decorate<IIdentityService, IdentityCacheService>(); services.Decorate<IIdentityService, IdentityCacheService>();
@@ -0,0 +1,136 @@
using PostFundManagement.Domain.Attributes;
namespace PostFundManagement.Application.Interceptors;
public sealed class LoggingInterceptor(ILogger<LoggingInterceptor> logger) : IAsyncInterceptor
{
public void InterceptSynchronous(IInvocation invocation)
{
invocation.Proceed();
CheckAndLogIfFailed(invocation, invocation.ReturnValue);
}
public void InterceptAsynchronous(IInvocation invocation) =>
invocation.ReturnValue = InternalInterceptAsynchronous(invocation);
public void InterceptAsynchronous<TResult>(IInvocation invocation) =>
invocation.ReturnValue = InternalInterceptAsynchronous<TResult>(invocation);
private static async Task InternalInterceptAsynchronous(IInvocation invocation)
{
invocation.Proceed();
var task = (Task)invocation.ReturnValue;
await task.ConfigureAwait(false);
}
private async Task<TResult> InternalInterceptAsynchronous<TResult>(IInvocation invocation)
{
invocation.Proceed();
var task = (Task<TResult>)invocation.ReturnValue;
var resultObj = await task.ConfigureAwait(false);
var method = invocation.MethodInvocationTarget ?? invocation.Method;
var auditAttr = method.GetCustomAttribute<LogAttribute>()
?? invocation.Method.GetCustomAttribute<LogAttribute>();
if (auditAttr == null) return resultObj;
CheckAndLogIfFailed(invocation, resultObj);
return resultObj;
}
private void CheckAndLogIfFailed(IInvocation invocation, object? resultObj)
{
if (resultObj is IResultBase result && result.IsFailed)
{
try
{
var method = invocation.MethodInvocationTarget ?? invocation.Method;
var parameters = BuildPayloadSummary(method.GetParameters(), invocation.Arguments);
var errorMessages = new List<string>();
if (result.Reasons != null)
{
foreach (var reason in result.Reasons)
if (reason is IError error)
errorMessages.Add(error.Message);
}
var errorsString = string.Join("; ", errorMessages);
logger.LogError("Method {MethodName} failed. Errors: {Errors}. Parameters: {@Parameters}",
method.Name, errorsString, parameters);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to write error log for method {MethodName}.", invocation.Method.Name);
}
}
}
private static string[] BuildPayloadSummary(ParameterInfo[] parameters, object[] args)
{
var entries = new List<string>();
for (var i = 0; i < parameters.Length; i++)
{
var param = parameters[i];
var arg = args[i];
if (param.ParameterType == typeof(CancellationToken))
continue;
if (arg == null)
{
entries.Add($"{param.Name}: null");
continue;
}
if (arg is IFormFile file)
{
entries.Add($"{param.Name}: [File] Name='{file.FileName}', Size={file.Length} bytes, Type='{file.ContentType}'");
continue;
}
if (arg is IEnumerable<IFormFile> files)
{
var fileSummaries = new List<string>();
foreach (var f in files)
fileSummaries.Add($"'{f.FileName}' ({f.Length} bytes)");
entries.Add($"{param.Name}: [Files] count={fileSummaries.Count} [{string.Join(", ", fileSummaries)}]");
continue;
}
if (arg is Stream stream)
{
entries.Add($"{param.Name}: [Stream] Length={(stream.CanSeek ? stream.Length : -1)} bytes");
continue;
}
try
{
var json = JsonSerializer.Serialize(arg);
entries.Add($"{param.Name}: {json}");
}
catch
{
entries.Add($"{param.Name}: [Unserializable]");
}
}
return [.. entries];
}
}
@@ -15,7 +15,7 @@ public sealed class AuditService(IDbContextFactory<ApplicationDbContext> context
{ {
EntityId = entityId, EntityId = entityId,
Action = action, Action = action,
Changes = changes, Changes = new Domain.Models.AuditChanges { Logs = changes },
EntityName = entityName, EntityName = entityName,
PerformedBy = performedBy, PerformedBy = performedBy,
Timestamp = DateTime.UtcNow, Timestamp = DateTime.UtcNow,
@@ -85,7 +85,7 @@ public sealed class UserService(IDbContextFactory<ApplicationDbContext> contextF
var user = await context.Users.AsNoTracking() var user = await context.Users.AsNoTracking()
.FirstOrDefaultAsync(f => f.Id == userId, cancellationToken); .FirstOrDefaultAsync(f => f.Id == userId, cancellationToken);
return Result.FailIf(user is null, NotFoundError.New) return Result.FailIf(user == null, NotFoundError.New)
.ToResult(user!.Map()); .ToResult(user?.Map()!);
})); }));
} }
@@ -0,0 +1,6 @@
namespace PostFundManagement.Domain.Abstractions;
public interface ITenantEntity
{
public long TenantId { get; set; }
}
@@ -0,0 +1,4 @@
namespace PostFundManagement.Domain.Attributes;
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
public sealed class LogAttribute : Attribute;
+11 -2
View File
@@ -1,10 +1,12 @@
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants; using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<ActivityConfiguration, Activity>] public class Activity : Models.Activity, ITenantEntity
public class Activity : Models.Activity
{ {
public virtual Tenant? Tenant { get; set; }
public virtual User? Creator { get; set; } public virtual User? Creator { get; set; }
public virtual User? Updator { get; set; } public virtual User? Updator { get; set; }
@@ -26,6 +28,7 @@ public sealed class ActivityConfiguration : IEntityTypeConfiguration<Activity>
builder.HasKey(pk => pk.Id); builder.HasKey(pk => pk.Id);
builder.Property(f => f.ProjectId).IsRequired(); builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.TenantId).IsRequired();
builder.Property(f => f.MilestoneId).IsRequired(); builder.Property(f => f.MilestoneId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.UpdatedBy).IsRequired(false); builder.Property(f => f.UpdatedBy).IsRequired(false);
@@ -40,6 +43,12 @@ public sealed class ActivityConfiguration : IEntityTypeConfiguration<Activity>
builder.Property(f => f.Title).IsRequired().HasMaxLength(LabelLength); builder.Property(f => f.Title).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Description).IsRequired().HasMaxLength(TextLength); builder.Property(f => f.Description).IsRequired().HasMaxLength(TextLength);
builder.HasOne(f => f.Tenant)
.WithMany()
.HasForeignKey(fk => fk.TenantId)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Project) builder.HasOne(f => f.Project)
.WithMany() .WithMany()
.HasForeignKey(fk => fk.ProjectId) .HasForeignKey(fk => fk.ProjectId)
@@ -1,8 +1,8 @@
using PostFundManagement.Domain.Models;
using static PostFundManagement.Domain.Extensions.Constants; using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<AuditLogConfiguration, AuditLog>]
public sealed class AuditLog : Models.AuditLog; public sealed class AuditLog : Models.AuditLog;
public sealed class AuditLogConfiguration : IEntityTypeConfiguration<AuditLog> public sealed class AuditLogConfiguration : IEntityTypeConfiguration<AuditLog>
@@ -16,7 +16,9 @@ public sealed class AuditLogConfiguration : IEntityTypeConfiguration<AuditLog>
builder.Property(f => f.EntityName).IsRequired().HasMaxLength(LabelLength); builder.Property(f => f.EntityName).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.EntityId).IsRequired(); builder.Property(f => f.EntityId).IsRequired();
builder.Property(f => f.Action).IsRequired().HasMaxLength(ShortLabelLength); builder.Property(f => f.Action).IsRequired().HasMaxLength(ShortLabelLength);
builder.Property(f => f.Changes).IsRequired(false).HasColumnType("jsonb"); builder.Property(f => f.Changes).IsRequired(false).HasColumnType("jsonb")
.HasConversion(model => JsonSerializer.Serialize(model, JsonSerializerOptions.Default),
provider => JsonSerializer.Deserialize<AuditChanges>(provider, JsonSerializerOptions.Default));
builder.Property(f => f.PerformedBy).IsRequired(); builder.Property(f => f.PerformedBy).IsRequired();
builder.Property(f => f.Timestamp).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.Timestamp).IsRequired().HasDefaultValueSql("now()");
} }
+7 -6
View File
@@ -1,7 +1,8 @@
using PostFundManagement.Domain.Abstractions;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<AwardConfiguration, Award>] public class Award : Models.Award, ITenantEntity
public class Award : Models.Award
{ {
public virtual User? Creator { get; set; } public virtual User? Creator { get; set; }
@@ -77,9 +78,9 @@ public sealed class AwardConfiguration : IEntityTypeConfiguration<Award>
.OnDelete(DeleteBehavior.NoAction); .OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Approver) builder.HasOne(f => f.Approver)
.WithMany() .WithMany()
.HasForeignKey(fk => fk.ApprovedBy) .HasForeignKey(fk => fk.ApprovedBy)
.IsRequired(false) .IsRequired(false)
.OnDelete(DeleteBehavior.NoAction); .OnDelete(DeleteBehavior.NoAction);
} }
} }
@@ -1,14 +1,18 @@
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants; using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<BeneficiaryConfiguration, Beneficiary>] public class Beneficiary : Models.Beneficiary, ITenantEntity
public class Beneficiary : Models.Beneficiary
{ {
public virtual Tenant? Tenant { get; set; }
public virtual Project? Project { get; set; } public virtual Project? Project { get; set; }
[ForeignKey(nameof(CreatedBy))]
public virtual User? Creator { get; set; } public virtual User? Creator { get; set; }
[ForeignKey(nameof(UserId))]
public virtual User? User { get; set; } public virtual User? User { get; set; }
} }
@@ -19,6 +23,7 @@ public sealed class BeneficiaryConfiguration : IEntityTypeConfiguration<Benefici
builder.ToTable(nameof(Beneficiary).Pluralize()); builder.ToTable(nameof(Beneficiary).Pluralize());
builder.HasKey(pk => pk.Id); builder.HasKey(pk => pk.Id);
builder.Property(f => f.TenantId).IsRequired();
builder.Property(f => f.ProjectId).IsRequired(); builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.UserId).IsRequired(); builder.Property(f => f.UserId).IsRequired();
@@ -29,6 +34,12 @@ public sealed class BeneficiaryConfiguration : IEntityTypeConfiguration<Benefici
builder.Property(f => f.ReachedCount).IsRequired(); builder.Property(f => f.ReachedCount).IsRequired();
builder.Property(f => f.Location).IsRequired(); builder.Property(f => f.Location).IsRequired();
builder.HasOne(f => f.Tenant)
.WithMany()
.HasForeignKey(fk => fk.TenantId)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Project) builder.HasOne(f => f.Project)
.WithMany() .WithMany()
.HasForeignKey(fk => fk.ProjectId) .HasForeignKey(fk => fk.ProjectId)
+11 -2
View File
@@ -1,10 +1,12 @@
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants; using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<BudgetConfiguration, Budget>] public class Budget : Models.Budget, ITenantEntity
public class Budget : Models.Budget
{ {
public virtual Tenant? Tenant { get; set; }
public virtual Project? Project { get; set; } public virtual Project? Project { get; set; }
public virtual User? Creator { get; set; } public virtual User? Creator { get; set; }
@@ -19,6 +21,7 @@ public sealed class BudgetConfiguration : IEntityTypeConfiguration<Budget>
builder.ToTable(nameof(Budget).Pluralize()); builder.ToTable(nameof(Budget).Pluralize());
builder.HasKey(pk => pk.Id); builder.HasKey(pk => pk.Id);
builder.Property(f => f.TenantId).IsRequired();
builder.Property(f => f.ProjectId).IsRequired(); builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.ApprovedBy).IsRequired(false); builder.Property(f => f.ApprovedBy).IsRequired(false);
@@ -27,6 +30,12 @@ public sealed class BudgetConfiguration : IEntityTypeConfiguration<Budget>
builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength); builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2); builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2);
builder.HasOne(f => f.Tenant)
.WithMany()
.HasForeignKey(fk => fk.TenantId)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Project) builder.HasOne(f => f.Project)
.WithMany() .WithMany()
.HasForeignKey(fk => fk.ProjectId) .HasForeignKey(fk => fk.ProjectId)
@@ -1,10 +1,12 @@
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants; using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<ChangeRequestConfiguration, ChangeRequest>] public class ChangeRequest : Models.ChangeRequest, ITenantEntity
public class ChangeRequest : Models.ChangeRequest
{ {
public virtual Tenant? Tenant { get; set; }
public virtual Project? Project { get; set; } public virtual Project? Project { get; set; }
public virtual User? Creator { get; set; } public virtual User? Creator { get; set; }
@@ -19,6 +21,7 @@ public sealed class ChangeRequestConfiguration : IEntityTypeConfiguration<Change
builder.ToTable(nameof(ChangeRequest).Pluralize()); builder.ToTable(nameof(ChangeRequest).Pluralize());
builder.HasKey(pk => pk.Id); builder.HasKey(pk => pk.Id);
builder.Property(f => f.TenantId).IsRequired();
builder.Property(f => f.ProjectId).IsRequired(); builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.ApprovedBy).IsRequired(false); builder.Property(f => f.ApprovedBy).IsRequired(false);
@@ -31,6 +34,12 @@ public sealed class ChangeRequestConfiguration : IEntityTypeConfiguration<Change
builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1") builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1")
.HasSentinel(ApprovalStatus.Pending); .HasSentinel(ApprovalStatus.Pending);
builder.HasOne(f => f.Tenant)
.WithMany()
.HasForeignKey(fk => fk.TenantId)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Project) builder.HasOne(f => f.Project)
.WithMany() .WithMany()
.HasForeignKey(fk => fk.ProjectId) .HasForeignKey(fk => fk.ProjectId)
@@ -1,10 +1,12 @@
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants; using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<ComplianceItemConfiguration, ComplianceItem>] public class ComplianceItem : Models.ComplianceItem, ITenantEntity
public class ComplianceItem : Models.ComplianceItem
{ {
public virtual Tenant? Tenant { get; set; }
public virtual Project? Project { get; set; } public virtual Project? Project { get; set; }
public virtual User? Creator { get; set; } public virtual User? Creator { get; set; }
@@ -19,6 +21,7 @@ public sealed class ComplianceItemConfiguration : IEntityTypeConfiguration<Compl
builder.ToTable(nameof(ComplianceItem).Pluralize()); builder.ToTable(nameof(ComplianceItem).Pluralize());
builder.HasKey(pk => pk.Id); builder.HasKey(pk => pk.Id);
builder.Property(f => f.TenantId).IsRequired();
builder.Property(f => f.ProjectId).IsRequired(); builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.VerifiedBy).IsRequired(false); builder.Property(f => f.VerifiedBy).IsRequired(false);
+12 -3
View File
@@ -1,8 +1,11 @@
using PostFundManagement.Domain.Abstractions;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<ContractConfiguration, Contract>] public class Contract : Models.Contract, ITenantEntity
public class Contract : Models.Contract
{ {
public virtual Tenant? Tenant { get; set; }
public virtual Award? Award { get; set; } public virtual Award? Award { get; set; }
public virtual User? Creator { get; set; } public virtual User? Creator { get; set; }
@@ -15,8 +18,8 @@ public sealed class ContractConfiguration : IEntityTypeConfiguration<Contract>
builder.ToTable(nameof(Contract).Pluralize()); builder.ToTable(nameof(Contract).Pluralize());
builder.HasKey(pk => pk.Id); builder.HasKey(pk => pk.Id);
builder.Property(f => f.TenantId).IsRequired();
builder.HasIndex(f => f.AwardId).IsUnique(); builder.HasIndex(f => f.AwardId).IsUnique();
builder.Property(f => f.AwardId).IsRequired(); builder.Property(f => f.AwardId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
@@ -28,6 +31,12 @@ public sealed class ContractConfiguration : IEntityTypeConfiguration<Contract>
builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1") builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1")
.HasSentinel(ContractStatus.Draft); .HasSentinel(ContractStatus.Draft);
builder.HasOne(f => f.Tenant)
.WithMany()
.HasForeignKey(fk => fk.TenantId)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Award) builder.HasOne(f => f.Award)
.WithOne(f => f.Contract) .WithOne(f => f.Contract)
.HasForeignKey<Domain.Entities.Contract>(fk => fk.AwardId) .HasForeignKey<Domain.Entities.Contract>(fk => fk.AwardId)
@@ -1,8 +1,11 @@
using PostFundManagement.Domain.Abstractions;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<DisbursementConfiguration, Disbursement>] public class Disbursement : Models.Disbursement, ITenantEntity
public class Disbursement : Models.Disbursement
{ {
public virtual Tenant? Tenant { get; set; }
public virtual Project? Project { get; set; } public virtual Project? Project { get; set; }
public virtual Milestone? Milestone { get; set; } public virtual Milestone? Milestone { get; set; }
@@ -19,6 +22,7 @@ public sealed class DisbursementConfiguration : IEntityTypeConfiguration<Disburs
builder.ToTable(nameof(Disbursement).Pluralize()); builder.ToTable(nameof(Disbursement).Pluralize());
builder.HasKey(pk => pk.Id); builder.HasKey(pk => pk.Id);
builder.Property(f => f.TenantId).IsRequired();
builder.Property(f => f.ProjectId).IsRequired(); builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.MilestoneId).IsRequired(false); builder.Property(f => f.MilestoneId).IsRequired(false);
builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired();
@@ -29,6 +33,12 @@ public sealed class DisbursementConfiguration : IEntityTypeConfiguration<Disburs
builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1") builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1")
.HasSentinel(ApprovalStatus.Pending); .HasSentinel(ApprovalStatus.Pending);
builder.HasOne(f => f.Tenant)
.WithMany()
.HasForeignKey(fk => fk.TenantId)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Project) builder.HasOne(f => f.Project)
.WithMany() .WithMany()
.HasForeignKey(fk => fk.ProjectId) .HasForeignKey(fk => fk.ProjectId)
+12 -2
View File
@@ -1,8 +1,11 @@
using PostFundManagement.Domain.Abstractions;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<EvidenceConfiguration, Evidence>] public class Evidence : Models.Evidence, ITenantEntity
public class Evidence : Models.Evidence
{ {
public virtual Tenant? Tenant { get; set; }
public virtual Project? Project { get; set; } public virtual Project? Project { get; set; }
public virtual Milestone? Milestone { get; set; } public virtual Milestone? Milestone { get; set; }
@@ -23,6 +26,7 @@ public sealed class EvidenceConfiguration : IEntityTypeConfiguration<Evidence>
builder.ToTable(nameof(Evidence).Pluralize()); builder.ToTable(nameof(Evidence).Pluralize());
builder.HasKey(f => f.Id); builder.HasKey(f => f.Id);
builder.Property(f => f.TenantId).IsRequired();
builder.Property(f => f.ProjectId).IsRequired(); builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.MilestoneId).IsRequired(); builder.Property(f => f.MilestoneId).IsRequired();
builder.Property(f => f.IndicatorId).IsRequired(); builder.Property(f => f.IndicatorId).IsRequired();
@@ -38,6 +42,12 @@ public sealed class EvidenceConfiguration : IEntityTypeConfiguration<Evidence>
builder.Property(f => f.Type).IsRequired().HasConversion<int>().HasDefaultValueSql("9") builder.Property(f => f.Type).IsRequired().HasConversion<int>().HasDefaultValueSql("9")
.HasSentinel(EvidenceType.ProcurementDocument); .HasSentinel(EvidenceType.ProcurementDocument);
builder.HasOne(f => f.Tenant)
.WithMany()
.HasForeignKey(fk => fk.TenantId)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Project) builder.HasOne(f => f.Project)
.WithMany() .WithMany()
.HasForeignKey(fk => fk.ProjectId) .HasForeignKey(fk => fk.ProjectId)
@@ -1,10 +1,12 @@
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants; using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<IndicatorConfiguration, Indicator>] public class Indicator : Models.Indicator, ITenantEntity
public class Indicator : Models.Indicator
{ {
public virtual Tenant? Tenant { get; set; }
public virtual Project? Project { get; set; } public virtual Project? Project { get; set; }
public virtual User? Creator { get; set; } public virtual User? Creator { get; set; }
@@ -17,6 +19,7 @@ public sealed class IndicatorConfiguration : IEntityTypeConfiguration<Indicator>
builder.ToTable(nameof(Indicator).Pluralize()); builder.ToTable(nameof(Indicator).Pluralize());
builder.HasKey(pk => pk.Id); builder.HasKey(pk => pk.Id);
builder.Property(f => f.TenantId).IsRequired();
builder.Property(f => f.ProjectId).IsRequired(); builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
@@ -26,6 +29,12 @@ public sealed class IndicatorConfiguration : IEntityTypeConfiguration<Indicator>
builder.Property(f => f.TargetAmount).IsRequired().HasPrecision(18, 2); builder.Property(f => f.TargetAmount).IsRequired().HasPrecision(18, 2);
builder.Property(f => f.ActualAmount).IsRequired().HasPrecision(18, 2); builder.Property(f => f.ActualAmount).IsRequired().HasPrecision(18, 2);
builder.HasOne(f => f.Tenant)
.WithMany()
.HasForeignKey(fk => fk.TenantId)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Project) builder.HasOne(f => f.Project)
.WithMany() .WithMany()
.HasForeignKey(fk => fk.ProjectId) .HasForeignKey(fk => fk.ProjectId)
@@ -1,10 +1,12 @@
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants; using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<InstrumentConfiguration, Instrument>] public class Instrument : Models.Instrument, ITenantEntity
public class Instrument : Models.Instrument
{ {
public virtual Tenant? Tenant { get; set; }
public virtual User? Creator { get; set; } public virtual User? Creator { get; set; }
public virtual Programme? Programme { get; set; } public virtual Programme? Programme { get; set; }
@@ -17,6 +19,7 @@ public sealed class InstrumentConfiguration : IEntityTypeConfiguration<Instrumen
builder.ToTable(nameof(Instrument).Pluralize()); builder.ToTable(nameof(Instrument).Pluralize());
builder.HasKey(pk => pk.Id); builder.HasKey(pk => pk.Id);
builder.Property(f => f.TenantId).IsRequired();
builder.Property(f => f.ProgrammeId).IsRequired(); builder.Property(f => f.ProgrammeId).IsRequired();
builder.Property(f => f.Code).IsRequired().HasMaxLength(ShortLabelLength); builder.Property(f => f.Code).IsRequired().HasMaxLength(ShortLabelLength);
builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength); builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength);
@@ -26,6 +29,12 @@ public sealed class InstrumentConfiguration : IEntityTypeConfiguration<Instrumen
builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.HasOne(f => f.Tenant)
.WithMany()
.HasForeignKey(fk => fk.TenantId)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Programme) builder.HasOne(f => f.Programme)
.WithMany(f => f.Instruments) .WithMany(f => f.Instruments)
.HasForeignKey(fk => fk.ProgrammeId) .HasForeignKey(fk => fk.ProgrammeId)
+12 -2
View File
@@ -1,8 +1,11 @@
using PostFundManagement.Domain.Abstractions;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<InviteConfiguration, Invite>] public class Invite : Models.Invite, ITenantEntity
public class Invite : Models.Invite
{ {
public virtual Tenant? Tenant { get; set; }
public virtual Organisation? Organisation { get; set; } public virtual Organisation? Organisation { get; set; }
public virtual Organisation? InvitedOrganisation { get; set; } public virtual Organisation? InvitedOrganisation { get; set; }
@@ -21,6 +24,7 @@ public sealed class InviteConfiguration : IEntityTypeConfiguration<Invite>
builder.ToTable(nameof(Invite).Pluralize()); builder.ToTable(nameof(Invite).Pluralize());
builder.HasKey(pk => pk.Id); builder.HasKey(pk => pk.Id);
builder.Property(f => f.TenantId).IsRequired();
builder.Property(f => f.OrganisationId).IsRequired(); builder.Property(f => f.OrganisationId).IsRequired();
builder.Property(f => f.InvitedOrganisationId).IsRequired(); builder.Property(f => f.InvitedOrganisationId).IsRequired();
builder.Property(f => f.AwardId).IsRequired(); builder.Property(f => f.AwardId).IsRequired();
@@ -31,6 +35,12 @@ public sealed class InviteConfiguration : IEntityTypeConfiguration<Invite>
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).IsRequired(false); builder.Property(f => f.UpdatedAt).IsRequired(false);
builder.HasOne(f => f.Tenant)
.WithMany()
.HasForeignKey(fk => fk.TenantId)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Organisation) builder.HasOne(f => f.Organisation)
.WithMany() .WithMany()
.HasForeignKey(fk => fk.OrganisationId) .HasForeignKey(fk => fk.OrganisationId)
+11 -2
View File
@@ -1,10 +1,12 @@
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants; using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<IssueConfiguration, Issue>] public class Issue : Models.Issue, ITenantEntity
public class Issue : Models.Issue
{ {
public virtual Tenant? Tenant { get; set; }
public virtual Project? Project { get; set; } public virtual Project? Project { get; set; }
public virtual User? Creator { get; set; } public virtual User? Creator { get; set; }
@@ -19,6 +21,7 @@ public sealed class IssueConfiguration : IEntityTypeConfiguration<Issue>
builder.ToTable(nameof(Issue).Pluralize()); builder.ToTable(nameof(Issue).Pluralize());
builder.HasKey(pk => pk.Id); builder.HasKey(pk => pk.Id);
builder.Property(f => f.TenantId).IsRequired();
builder.Property(f => f.ProjectId).IsRequired(); builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
@@ -32,6 +35,12 @@ public sealed class IssueConfiguration : IEntityTypeConfiguration<Issue>
.HasSentinel(ActivityStatus.Active); .HasSentinel(ActivityStatus.Active);
builder.Property(f => f.Resolution).IsRequired(); builder.Property(f => f.Resolution).IsRequired();
builder.HasOne(f => f.Tenant)
.WithMany()
.HasForeignKey(fk => fk.TenantId)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Project) builder.HasOne(f => f.Project)
.WithMany() .WithMany()
.HasForeignKey(fk => fk.ProjectId) .HasForeignKey(fk => fk.ProjectId)
@@ -1,10 +1,12 @@
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants; using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<MilestoneConfiguration, Milestone>] public class Milestone : Models.Milestone, ITenantEntity
public class Milestone : Models.Milestone
{ {
public virtual Tenant? Tenant { get; set; }
public virtual Project? Project { get; set; } public virtual Project? Project { get; set; }
public virtual User? Creator { get; set; } public virtual User? Creator { get; set; }
@@ -19,6 +21,7 @@ public sealed class MilestoneConfiguration : IEntityTypeConfiguration<Milestone>
builder.ToTable(nameof(Milestone).Pluralize()); builder.ToTable(nameof(Milestone).Pluralize());
builder.HasKey(pk => pk.Id); builder.HasKey(pk => pk.Id);
builder.Property(f => f.TenantId).IsRequired();
builder.Property(f => f.ProjectId).IsRequired(); builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.UpdatedBy).IsRequired(false); builder.Property(f => f.UpdatedBy).IsRequired(false);
@@ -31,6 +34,12 @@ public sealed class MilestoneConfiguration : IEntityTypeConfiguration<Milestone>
builder.Property(f => f.Priority).IsRequired().HasConversion<int>().HasDefaultValueSql("1") builder.Property(f => f.Priority).IsRequired().HasConversion<int>().HasDefaultValueSql("1")
.HasSentinel(Priority.Low); .HasSentinel(Priority.Low);
builder.HasOne(f => f.Tenant)
.WithMany()
.HasForeignKey(fk => fk.TenantId)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Project) builder.HasOne(f => f.Project)
.WithMany() .WithMany()
.HasForeignKey(fk => fk.ProjectId) .HasForeignKey(fk => fk.ProjectId)
@@ -1,9 +1,9 @@
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants; using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<NotificationConfiguration, Notification>] public class Notification : Models.Notification, ITenantEntity
public class Notification : Models.Notification
{ {
public virtual Tenant? Tenant { get; set; } public virtual Tenant? Tenant { get; set; }
@@ -2,7 +2,6 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<OrganisationConfiguration, Organisation>]
public class Organisation : Models.Organisation public class Organisation : Models.Organisation
{ {
public virtual User? Creator { get; set; } public virtual User? Creator { get; set; }
@@ -2,7 +2,6 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<OutcomeConfiguration, Outcome>]
public class Outcome : Models.Outcome public class Outcome : Models.Outcome
{ {
public virtual Beneficiary? Beneficiary { get; set; } public virtual Beneficiary? Beneficiary { get; set; }
@@ -1,9 +1,9 @@
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants; using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<PortfolioConfiguration, Portfolio>] public class Portfolio : Models.Portfolio, ITenantEntity
public class Portfolio : Models.Portfolio
{ {
public virtual User? Creator { get; set; } public virtual User? Creator { get; set; }
@@ -1,9 +1,9 @@
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants; using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<ProgrammeConfiguration, Programme>] public class Programme : Models.Programme, ITenantEntity
public class Programme : Models.Programme
{ {
public virtual User? Creator { get; set; } public virtual User? Creator { get; set; }
@@ -1,9 +1,9 @@
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants; using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<ProjectConfiguration, Project>] public class Project : Models.Project, ITenantEntity
public class Project : Models.Project
{ {
public virtual User? Creator { get; set; } public virtual User? Creator { get; set; }
+11 -2
View File
@@ -1,10 +1,12 @@
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants; using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<RiskConfiguration, Risk>] public class Risk : Models.Risk, ITenantEntity
public class Risk : Models.Risk
{ {
public virtual Tenant? Tenant { get; set; }
public virtual Project? Project { get; set; } public virtual Project? Project { get; set; }
public virtual User? Creator { get; set; } public virtual User? Creator { get; set; }
@@ -17,6 +19,7 @@ public sealed class RiskConfiguration : IEntityTypeConfiguration<Risk>
builder.ToTable(nameof(Risk).Pluralize()); builder.ToTable(nameof(Risk).Pluralize());
builder.HasKey(pk => pk.Id); builder.HasKey(pk => pk.Id);
builder.Property(f => f.TenantId).IsRequired();
builder.Property(f => f.ProjectId).IsRequired(); builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
@@ -27,6 +30,12 @@ public sealed class RiskConfiguration : IEntityTypeConfiguration<Risk>
builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength); builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Description).IsRequired().HasMaxLength(TextLength); builder.Property(f => f.Description).IsRequired().HasMaxLength(TextLength);
builder.HasOne(f => f.Tenant)
.WithMany()
.HasForeignKey(fk => fk.TenantId)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Project) builder.HasOne(f => f.Project)
.WithMany() .WithMany()
.HasForeignKey(fk => fk.ProjectId) .HasForeignKey(fk => fk.ProjectId)
+11 -2
View File
@@ -1,10 +1,12 @@
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants; using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<RuleConfiguration, Rule>] public class Rule : Models.Rule, ITenantEntity
public class Rule : Models.Rule
{ {
public virtual Tenant? Tenant { get; set; }
public virtual User? Creator { get; set; } public virtual User? Creator { get; set; }
public virtual User? Updator { get; set; } public virtual User? Updator { get; set; }
@@ -19,6 +21,7 @@ public sealed class RuleConfiguration : IEntityTypeConfiguration<Rule>
builder.ToTable(nameof(Rule).Pluralize()); builder.ToTable(nameof(Rule).Pluralize());
builder.HasKey(fk => fk.Id); builder.HasKey(fk => fk.Id);
builder.Property(f => f.TenantId).IsRequired();
builder.Property(f => f.ProgrammeId).IsRequired(); builder.Property(f => f.ProgrammeId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.UpdatedBy).IsRequired(false); builder.Property(f => f.UpdatedBy).IsRequired(false);
@@ -29,6 +32,12 @@ public sealed class RuleConfiguration : IEntityTypeConfiguration<Rule>
builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("2") builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("2")
.HasSentinel(ActivityStatus.Inactive); .HasSentinel(ActivityStatus.Inactive);
builder.HasOne(f => f.Tenant)
.WithMany()
.HasForeignKey(fk => fk.TenantId)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Programme) builder.HasOne(f => f.Programme)
.WithMany(f => f.Rules) .WithMany(f => f.Rules)
.HasForeignKey(fk => fk.ProgrammeId) .HasForeignKey(fk => fk.ProgrammeId)
@@ -1,11 +1,13 @@
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants; using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<SiteVisitConfiguration, SiteVisit>] public class SiteVisit : Models.SiteVisit, ITenantEntity
public class SiteVisit : Models.SiteVisit
{ {
public virtual Project? Project { get; set; } public virtual Tenant? Tenant { get; set; }
public virtual Project? Project { get; set; }
public virtual User? Creator { get; set; } public virtual User? Creator { get; set; }
} }
@@ -17,6 +19,7 @@ public sealed class SiteVisitConfiguration : IEntityTypeConfiguration<SiteVisit>
builder.ToTable(nameof(SiteVisit).Pluralize()); builder.ToTable(nameof(SiteVisit).Pluralize());
builder.HasKey(pk => pk.Id); builder.HasKey(pk => pk.Id);
builder.Property(f => f.TenantId).IsRequired();
builder.Property(f => f.ProjectId).IsRequired(); builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
@@ -26,6 +29,12 @@ public sealed class SiteVisitConfiguration : IEntityTypeConfiguration<SiteVisit>
builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1") builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1")
.HasSentinel(ApprovalStatus.Pending); .HasSentinel(ApprovalStatus.Pending);
builder.HasOne(f => f.Tenant)
.WithMany()
.HasForeignKey(fk => fk.TenantId)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Project) builder.HasOne(f => f.Project)
.WithMany() .WithMany()
.HasForeignKey(fk => fk.ProjectId) .HasForeignKey(fk => fk.ProjectId)
@@ -2,7 +2,6 @@
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<TenantConfiguration, Tenant>]
public class Tenant : Models.Tenant public class Tenant : Models.Tenant
{ {
public virtual User? Creator { get; set; } public virtual User? Creator { get; set; }
@@ -1,6 +1,5 @@
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<TenantOrganisationConfiguration, TenantOrganisation>]
public class TenantOrganisation : Models.TenantOrganisation public class TenantOrganisation : Models.TenantOrganisation
{ {
public virtual Tenant? Tenant { get; set; } public virtual Tenant? Tenant { get; set; }
@@ -17,6 +16,8 @@ public sealed class TenantOrganisationConfiguration : IEntityTypeConfiguration<T
builder.ToTable(nameof(TenantOrganisation).Pluralize()); builder.ToTable(nameof(TenantOrganisation).Pluralize());
builder.HasKey(pk => pk.Id); builder.HasKey(pk => pk.Id);
builder.HasIndex(to => new { to.TenantId, to.OrganisationId }).IsUnique();
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.TenantId).IsRequired(); builder.Property(f => f.TenantId).IsRequired();
@@ -1,12 +1,13 @@
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<TenantUserConfiguration, TenantUser>]
public class TenantUser : Models.TenantUser public class TenantUser : Models.TenantUser
{ {
public virtual Tenant? Tenant { get; set; } public virtual Tenant? Tenant { get; set; }
[ForeignKey(nameof(CreatedBy))]
public virtual User? Creator { get; set; } public virtual User? Creator { get; set; }
[ForeignKey(nameof(UserId))]
public virtual User? User { get; set; } public virtual User? User { get; set; }
} }
@@ -17,6 +18,8 @@ public sealed class TenantUserConfiguration : IEntityTypeConfiguration<TenantUse
builder.ToTable(nameof(TenantUser).Pluralize()); builder.ToTable(nameof(TenantUser).Pluralize());
builder.HasKey(pk => pk.Id); builder.HasKey(pk => pk.Id);
builder.HasIndex(tu => new { tu.TenantId, tu.UserId }).IsUnique();
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()"); builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.CreatedBy).IsRequired(); builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.UserId).IsRequired(); builder.Property(f => f.UserId).IsRequired();
@@ -2,7 +2,6 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Entities; namespace PostFundManagement.Domain.Entities;
[EntityTypeConfiguration<UserConfiguration, User>]
public sealed class User : Models.User; public sealed class User : Models.User;
public sealed class UserConfiguration : IEntityTypeConfiguration<User> public sealed class UserConfiguration : IEntityTypeConfiguration<User>
@@ -3,6 +3,8 @@ namespace PostFundManagement.Domain.Models;
public class Activity public class Activity
{ {
public long Id { get; set; } public long Id { get; set; }
public long TenantId { get; set; }
public long? ProjectId { get; set; } public long? ProjectId { get; set; }
@@ -0,0 +1,6 @@
namespace PostFundManagement.Domain.Models;
public sealed class AuditChanges
{
public string[]? Logs { get; set; }
}
+2 -1
View File
@@ -12,7 +12,8 @@ public class AuditLog
public string? Action { get; set; } public string? Action { get; set; }
public string[]? Changes { get; set; } [NotMapped]
public AuditChanges? Changes { get; set; }
public Guid PerformedBy { get; set; } public Guid PerformedBy { get; set; }
@@ -4,6 +4,8 @@ public class Beneficiary
{ {
public long Id { get; set; } public long Id { get; set; }
public long TenantId { get; set; }
public long ProjectId { get; set; } public long ProjectId { get; set; }
public Guid UserId { get; set; } public Guid UserId { get; set; }
@@ -4,6 +4,8 @@ public class Budget
{ {
public long Id { get; set; } public long Id { get; set; }
public long TenantId { get; set; }
public long ProjectId { get; set; } public long ProjectId { get; set; }
public Guid CreatedBy { get; set; } public Guid CreatedBy { get; set; }
@@ -4,6 +4,8 @@ public class ChangeRequest
{ {
public long Id { get; set; } public long Id { get; set; }
public long TenantId { get; set; }
public long ProjectId { get; set; } public long ProjectId { get; set; }
public string? Title { get; set; } public string? Title { get; set; }
@@ -4,6 +4,8 @@ public class ComplianceItem
{ {
public long Id { get; set; } public long Id { get; set; }
public long TenantId { get; set; }
public long ProjectId { get; set; } public long ProjectId { get; set; }
public string? Title { get; set; } public string? Title { get; set; }
@@ -4,6 +4,8 @@ public class Contract
{ {
public long Id { get; set; } public long Id { get; set; }
public long TenantId { get; set; }
public long AwardId { get; set; } public long AwardId { get; set; }
public string? ExternalSignatureId { get; set; } public string? ExternalSignatureId { get; set; }
@@ -4,6 +4,8 @@ public class Disbursement
{ {
public long Id { get; set; } public long Id { get; set; }
public long TenantId { get; set; }
public long ProjectId { get; set; } public long ProjectId { get; set; }
public long? MilestoneId { get; set; } public long? MilestoneId { get; set; }
@@ -4,6 +4,8 @@ public class Evidence
{ {
public long Id { get; set; } public long Id { get; set; }
public long TenantId { get; set; }
public long ProjectId { get; set; } public long ProjectId { get; set; }
public long MilestoneId { get; set; } public long MilestoneId { get; set; }
@@ -4,6 +4,8 @@ public class Indicator
{ {
public long Id { get; set; } public long Id { get; set; }
public long TenantId { get; set; }
public long ProjectId { get; set; } public long ProjectId { get; set; }
public Guid CreatedBy { get; set; } public Guid CreatedBy { get; set; }
@@ -4,6 +4,8 @@ public class Instrument
{ {
public long Id { get; set; } public long Id { get; set; }
public long TenantId { get; set; }
public long ProgrammeId { get; set; } public long ProgrammeId { get; set; }
public string? Code { get; set; } public string? Code { get; set; }
@@ -4,6 +4,8 @@ public class Invite
{ {
public long Id { get; set; } public long Id { get; set; }
public long TenantId { get; set; }
public long OrganisationId { get; set; } public long OrganisationId { get; set; }
public long InvitedOrganisationId { get; set; } public long InvitedOrganisationId { get; set; }
@@ -4,6 +4,8 @@ public class Issue
{ {
public long Id { get; set; } public long Id { get; set; }
public long TenantId { get; set; }
public long ProjectId { get; set; } public long ProjectId { get; set; }
public string? Title { get; set; } public string? Title { get; set; }
@@ -4,6 +4,8 @@ public class Milestone
{ {
public long Id { get; set; } public long Id { get; set; }
public long TenantId { get; set; }
public long ProjectId { get; set; } public long ProjectId { get; set; }
public Guid CreatedBy { get; set; } public Guid CreatedBy { get; set; }
+2
View File
@@ -4,6 +4,8 @@ public class Risk
{ {
public long Id { get; set; } public long Id { get; set; }
public long TenantId { get; set; }
public long ProjectId { get; set; } public long ProjectId { get; set; }
public Guid CreatedBy { get; set; } public Guid CreatedBy { get; set; }
+2
View File
@@ -4,6 +4,8 @@ public class Rule
{ {
public long Id { get; set; } public long Id { get; set; }
public long TenantId { get; set; }
public long ProgrammeId { get; set; } public long ProgrammeId { get; set; }
public Guid CreatedBy { get; set; } public Guid CreatedBy { get; set; }
@@ -4,6 +4,8 @@ public class SiteVisit
{ {
public long Id { get; set; } public long Id { get; set; }
public long TenantId { get; set; }
public long ProjectId { get; set; } public long ProjectId { get; set; }
public DateTime VisitedAt { get; set; } public DateTime VisitedAt { get; set; }
@@ -199,6 +199,7 @@
<!-- Shared Usings --> <!-- Shared Usings -->
<ItemGroup> <ItemGroup>
<Using Include="System.ComponentModel.DataAnnotations.Schema" />
<Using Include="Microsoft.AspNetCore.DataProtection" /> <Using Include="Microsoft.AspNetCore.DataProtection" />
<Using Include="System.Security.Cryptography.X509Certificates" /> <Using Include="System.Security.Cryptography.X509Certificates" />
<Using Include="Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage" /> <Using Include="Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage" />
@@ -65,35 +65,60 @@ public sealed class ApplicationDbContext(DbContextOptions<ApplicationDbContext>
public DbSet<User> Users => Set<User>(); public DbSet<User> Users => Set<User>();
private void ApplyGlobalTenantFilter<TEntity>(ModelBuilder modelBuilder)
where TEntity : class, ITenantEntity =>
modelBuilder.Entity<TEntity>().HasQueryFilter(e => BypassFilter || e.TenantId == CurrentTenantId);
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Portfolio>() modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
.HasQueryFilter(e => BypassFilter || e.TenantId == CurrentTenantId);
modelBuilder.Entity<Programme>() foreach (var entityType in modelBuilder.Model.GetEntityTypes())
.HasQueryFilter(e => BypassFilter || e.TenantId == CurrentTenantId); {
if (typeof(ITenantEntity).IsAssignableFrom(entityType.ClrType))
{
var method = typeof(ApplicationDbContext).GetMethod(nameof(ApplyGlobalTenantFilter),
BindingFlags.NonPublic | BindingFlags.Instance)?.MakeGenericMethod(entityType.ClrType);
modelBuilder.Entity<Project>() method?.Invoke(this, [modelBuilder]);
.HasQueryFilter(e => BypassFilter || e.TenantId == CurrentTenantId); }
}
modelBuilder.Entity<Award>()
.HasQueryFilter(e => BypassFilter || e.TenantId == CurrentTenantId);
modelBuilder.Entity<Notification>()
.HasQueryFilter(e => BypassFilter || e.TenantId == CurrentTenantId);
modelBuilder.Entity<TenantOrganisation>()
.HasQueryFilter(e => BypassFilter || e.TenantId == CurrentTenantId);
modelBuilder.Entity<TenantUser>()
.HasQueryFilter(e => BypassFilter || e.TenantId == CurrentTenantId);
modelBuilder.Entity<AuditLog>()
.HasQueryFilter(e => BypassFilter || e.TenantId == CurrentTenantId);
modelBuilder.Entity<Organisation>().HasQueryFilter(o => BypassFilter || modelBuilder.Entity<Organisation>().HasQueryFilter(o => BypassFilter ||
Set<TenantOrganisation>().Any(to => to.TenantId == CurrentTenantId && to.OrganisationId == o.Id)); Set<TenantOrganisation>().Any(to => to.TenantId == CurrentTenantId && to.OrganisationId == o.Id));
modelBuilder.Entity<User>().HasQueryFilter(o => BypassFilter ||
Set<TenantUser>().Any(to => to.TenantId == CurrentTenantId && to.UserId == o.Id));
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
AlterQualifyingTenantEntity();
return base.SaveChangesAsync(cancellationToken);
}
public override int SaveChanges()
{
AlterQualifyingTenantEntity();
return base.SaveChanges();
}
private void AlterQualifyingTenantEntity()
{
var entries = ChangeTracker.Entries<ITenantEntity>()
.Where(e => e.State == EntityState.Added && e.Entity.TenantId == 0)
.ToList();
if (entries.Count == 0) return;
if (CurrentTenantId <= 0 && !BypassFilter)
throw new InvalidOperationException("Cannot save multi-tenant entity: CurrentTenantId is not set in the active execution context.");
foreach (var entry in entries)
entry.Entity.TenantId = CurrentTenantId;
} }
} }
@@ -26,6 +26,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
<Using Include="System.Reflection" />
<Using Include="Microsoft.EntityFrameworkCore" /> <Using Include="Microsoft.EntityFrameworkCore" />
<Using Include="Microsoft.EntityFrameworkCore.Design" /> <Using Include="Microsoft.EntityFrameworkCore.Design" />
</ItemGroup> </ItemGroup>