From ef134a5b3c7754d374b6db700bb27004eb367950 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 23 Sep 2026 05:33:16 +0200 Subject: [PATCH] Implemented audit logging and automated logging --- PostFundManagement.Api/appsettings.json | 7 +- .../Services/ICommunicationsService.cs | 4 + .../Abstractions/Services/IEvidenceService.cs | 12 ++ .../Services/IExecutionService.cs | 9 ++ .../Services/IGovernanceService.cs | 16 ++- .../Abstractions/Services/IGrantsService.cs | 13 ++ .../Abstractions/Services/IIdentityService.cs | 1 + .../Abstractions/Services/IProjectService.cs | 21 ++- .../Abstractions/Services/IUserService.cs | 4 + .../Extensions/Services.cs | 41 ++---- .../Interceptors/LoggingInterceptor.cs | 136 ++++++++++++++++++ .../Services/Users/UserService.cs | 4 +- .../Attributes/LogAttribute.cs | 4 + PostFundManagement.Domain/Entities/Award.cs | 2 +- .../Entities/Portfolio.cs | 2 +- .../Entities/Programme.cs | 2 +- PostFundManagement.Domain/Entities/Project.cs | 2 +- PostFundManagement.Domain/Entities/Tenant.cs | 8 ++ 18 files changed, 250 insertions(+), 38 deletions(-) create mode 100644 PostFundManagement.Application/Interceptors/LoggingInterceptor.cs create mode 100644 PostFundManagement.Domain/Attributes/LogAttribute.cs diff --git a/PostFundManagement.Api/appsettings.json b/PostFundManagement.Api/appsettings.json index b325942..b201569 100644 --- a/PostFundManagement.Api/appsettings.json +++ b/PostFundManagement.Api/appsettings.json @@ -20,8 +20,11 @@ }, "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Default": "Warning", + "Quartz": "Error", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore.Database": "Error", + "Microsoft.EntityFrameworkCore.Update": "Error" } }, "EmailProcessorCron": "0 */5 * * * ?", diff --git a/PostFundManagement.Application/Abstractions/Services/ICommunicationsService.cs b/PostFundManagement.Application/Abstractions/Services/ICommunicationsService.cs index 2be4ff7..68adcc9 100644 --- a/PostFundManagement.Application/Abstractions/Services/ICommunicationsService.cs +++ b/PostFundManagement.Application/Abstractions/Services/ICommunicationsService.cs @@ -7,12 +7,16 @@ namespace PostFundManagement.Application.Abstractions.Services; public interface ICommunicationsService { + [Log] [Audit] Task> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default); + [Log] Task> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default); + [Log] Task> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default); + [Log] Task> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default); } diff --git a/PostFundManagement.Application/Abstractions/Services/IEvidenceService.cs b/PostFundManagement.Application/Abstractions/Services/IEvidenceService.cs index 7684c48..4c6cf7b 100644 --- a/PostFundManagement.Application/Abstractions/Services/IEvidenceService.cs +++ b/PostFundManagement.Application/Abstractions/Services/IEvidenceService.cs @@ -7,32 +7,44 @@ namespace PostFundManagement.Application.Abstractions.Services; public interface IEvidenceService { + [Log] [Audit] Task CreateEvidenceAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default); + [Log] [Audit] Task RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default); + [Log] Task> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default); + [Log] Task> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default); + [Log] [Audit] Task CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default); + [Log] [Audit] Task CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default); + [Log] Task> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken); + [Log] Task> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default); + [Log] Task> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default); + [Log] Task> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default); + [Log] [Audit] Task UpdateEvidenceAsync(UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default); + [Log] Task> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default); } diff --git a/PostFundManagement.Application/Abstractions/Services/IExecutionService.cs b/PostFundManagement.Application/Abstractions/Services/IExecutionService.cs index 0e02687..e87d256 100644 --- a/PostFundManagement.Application/Abstractions/Services/IExecutionService.cs +++ b/PostFundManagement.Application/Abstractions/Services/IExecutionService.cs @@ -7,26 +7,35 @@ namespace PostFundManagement.Application.Abstractions.Services; public interface IExecutionService { + [Log] Task> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default); + [Log] Task> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default); + [Log] [Audit] Task CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default); + [Log] [Audit] Task CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default); + [Log] [Audit] Task ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default); + [Log] Task> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default); + [Log] Task> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default); + [Log] [Audit] Task UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default); + [Log] [Audit] Task UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default); } diff --git a/PostFundManagement.Application/Abstractions/Services/IGovernanceService.cs b/PostFundManagement.Application/Abstractions/Services/IGovernanceService.cs index 29bd9c9..4ea0399 100644 --- a/PostFundManagement.Application/Abstractions/Services/IGovernanceService.cs +++ b/PostFundManagement.Application/Abstractions/Services/IGovernanceService.cs @@ -7,31 +7,43 @@ namespace PostFundManagement.Application.Abstractions.Services; public interface IGovernanceService { + [Log] Task> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default); + [Log] Task> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default); + [Log] Task> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default); + [Log] Task> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default); + [Log] Task> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default); + [Log] Task> GetDashboardAsync(CancellationToken cancellationToken = default); + [Log] [Audit] Task CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default); + [Log] [Audit] Task> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default); + [Log] [Audit] Task CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default); + [Log] [Audit] Task UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default); - + + [Log] Task> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default); - + + [Log] Task> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default); } diff --git a/PostFundManagement.Application/Abstractions/Services/IGrantsService.cs b/PostFundManagement.Application/Abstractions/Services/IGrantsService.cs index b33c0d2..730873b 100644 --- a/PostFundManagement.Application/Abstractions/Services/IGrantsService.cs +++ b/PostFundManagement.Application/Abstractions/Services/IGrantsService.cs @@ -7,36 +7,49 @@ namespace PostFundManagement.Application.Abstractions.Services; public interface IGrantsService { + [Log] Task> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default); + [Log] Task> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default); + [Log] Task> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default); + [Log] Task> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default); + [Log] Task> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default); + [Log] Task> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default); + [Log] [Audit] Task ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default); + [Log] [Audit] Task CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default); + [Log] [Audit] Task CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default); + [Log] [Audit] Task CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default); + [Log] [Audit] Task> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default); + [Log] [Audit] Task UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default); + [Log] [Audit] Task UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default); } diff --git a/PostFundManagement.Application/Abstractions/Services/IIdentityService.cs b/PostFundManagement.Application/Abstractions/Services/IIdentityService.cs index d4efd5a..b3f033e 100644 --- a/PostFundManagement.Application/Abstractions/Services/IIdentityService.cs +++ b/PostFundManagement.Application/Abstractions/Services/IIdentityService.cs @@ -4,6 +4,7 @@ namespace PostFundManagement.Application.Abstractions.Services; public interface IIdentityService { + [Log] [Audit] Task UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default); } diff --git a/PostFundManagement.Application/Abstractions/Services/IProjectService.cs b/PostFundManagement.Application/Abstractions/Services/IProjectService.cs index aa84ea8..3e7846d 100644 --- a/PostFundManagement.Application/Abstractions/Services/IProjectService.cs +++ b/PostFundManagement.Application/Abstractions/Services/IProjectService.cs @@ -7,48 +7,67 @@ namespace PostFundManagement.Application.Abstractions.Services; public interface IProjectService { + [Log] [Audit] Task UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default); + [Log] Task> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default); + [Log] Task> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default); + [Log] Task> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default); + [Log] [Audit] Task CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default); + [Log] [Audit] Task CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default); + [Log] Task> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default); + [Log] Task> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken = default); + [Log] Task> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default); + [Log] Task> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default); + [Log] [Audit] Task UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default); + [Log] [Audit] Task UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default); + [Log] Task> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default); + [Log] Task> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default); + [Log] [Audit] Task CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default); + [Log] [Audit] Task UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default); - + + [Log] Task> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default); + [Log] Task> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default); + [Log] Task> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default); } diff --git a/PostFundManagement.Application/Abstractions/Services/IUserService.cs b/PostFundManagement.Application/Abstractions/Services/IUserService.cs index 585330b..c0caddc 100644 --- a/PostFundManagement.Application/Abstractions/Services/IUserService.cs +++ b/PostFundManagement.Application/Abstractions/Services/IUserService.cs @@ -7,13 +7,17 @@ namespace PostFundManagement.Application.Abstractions.Services; public interface IUserService { + [Log] [Audit] Task SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default); + [Log] Task> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default); + [Log] [Audit] Task UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default); + [Log] Task> GetUserAsync(Guid userId, CancellationToken cancellationToken); } diff --git a/PostFundManagement.Application/Extensions/Services.cs b/PostFundManagement.Application/Extensions/Services.cs index df53b93..ff98689 100644 --- a/PostFundManagement.Application/Extensions/Services.cs +++ b/PostFundManagement.Application/Extensions/Services.cs @@ -16,7 +16,7 @@ namespace PostFundManagement.Application.Extensions; public static class Services { - public static IServiceCollection AddAuditedCachedScoped(this IServiceCollection services) + public static IServiceCollection AddLoggedAuditedCachedServiceScoped(this IServiceCollection services) where TInterface : class where TImplementation : class, TInterface where TCacheDecorator : class, TInterface @@ -27,30 +27,16 @@ public static class Services services.AddScoped(sp => { var proxyGenerator = sp.GetRequiredService(); - var interceptor = sp.GetRequiredService(); + var auditInterceptor = sp.GetRequiredService(); + var loggingInterceptor = sp.GetRequiredService(); var actual = sp.GetRequiredService(); var cached = ActivatorUtilities.CreateInstance(sp, actual); - return proxyGenerator.CreateInterfaceProxyWithTarget(cached, interceptor.ToInterceptor()); - }); - - return services; - } - - public static IServiceCollection AddAuditedScoped(this IServiceCollection services) - where TInterface : class - where TImplementation : class, TInterface - { - services.AddScoped(); - services.AddScoped(sp => - { - var proxyGenerator = sp.GetRequiredService(); - var actual = sp.GetRequiredService(); - var interceptor = sp.GetRequiredService(); - - return proxyGenerator.CreateInterfaceProxyWithTarget(actual, interceptor.ToInterceptor()); + return proxyGenerator.CreateInterfaceProxyWithTarget(cached, + loggingInterceptor.ToInterceptor(), + auditInterceptor.ToInterceptor()); }); return services; @@ -59,15 +45,16 @@ public static class Services public static IServiceCollection AddRegisterServicesWithCaching(this IServiceCollection services, IConfiguration configuration) { services.AddScoped(); + services.AddScoped(); services.AddSingleton(); - services.AddAuditedCachedScoped(); - services.AddAuditedCachedScoped(); - services.AddAuditedCachedScoped(); - services.AddAuditedCachedScoped(); - services.AddAuditedCachedScoped(); - services.AddAuditedCachedScoped(); - services.AddAuditedCachedScoped(); + services.AddLoggedAuditedCachedServiceScoped(); + services.AddLoggedAuditedCachedServiceScoped(); + services.AddLoggedAuditedCachedServiceScoped(); + services.AddLoggedAuditedCachedServiceScoped(); + services.AddLoggedAuditedCachedServiceScoped(); + services.AddLoggedAuditedCachedServiceScoped(); + services.AddLoggedAuditedCachedServiceScoped(); services.AddScoped(); services.Decorate(); diff --git a/PostFundManagement.Application/Interceptors/LoggingInterceptor.cs b/PostFundManagement.Application/Interceptors/LoggingInterceptor.cs new file mode 100644 index 0000000..f36df0b --- /dev/null +++ b/PostFundManagement.Application/Interceptors/LoggingInterceptor.cs @@ -0,0 +1,136 @@ +using PostFundManagement.Domain.Attributes; + +namespace PostFundManagement.Application.Interceptors; + +public sealed class LoggingInterceptor(ILogger 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(IInvocation invocation) => + invocation.ReturnValue = InternalInterceptAsynchronous(invocation); + + private static async Task InternalInterceptAsynchronous(IInvocation invocation) + { + invocation.Proceed(); + + var task = (Task)invocation.ReturnValue; + + await task.ConfigureAwait(false); + } + + private async Task InternalInterceptAsynchronous(IInvocation invocation) + { + invocation.Proceed(); + + var task = (Task)invocation.ReturnValue; + var resultObj = await task.ConfigureAwait(false); + + var method = invocation.MethodInvocationTarget ?? invocation.Method; + var auditAttr = method.GetCustomAttribute() + ?? invocation.Method.GetCustomAttribute(); + + 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(); + + 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(); + + 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 files) + { + var fileSummaries = new List(); + + 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]; + } +} \ No newline at end of file diff --git a/PostFundManagement.Application/Services/Users/UserService.cs b/PostFundManagement.Application/Services/Users/UserService.cs index a156c37..e76caf9 100644 --- a/PostFundManagement.Application/Services/Users/UserService.cs +++ b/PostFundManagement.Application/Services/Users/UserService.cs @@ -85,7 +85,7 @@ public sealed class UserService(IDbContextFactory contextF var user = await context.Users.AsNoTracking() .FirstOrDefaultAsync(f => f.Id == userId, cancellationToken); - return Result.FailIf(user is null, NotFoundError.New) - .ToResult(user!.Map()); + return Result.FailIf(user == null, NotFoundError.New) + .ToResult(user?.Map()!); })); } diff --git a/PostFundManagement.Domain/Attributes/LogAttribute.cs b/PostFundManagement.Domain/Attributes/LogAttribute.cs new file mode 100644 index 0000000..7478845 --- /dev/null +++ b/PostFundManagement.Domain/Attributes/LogAttribute.cs @@ -0,0 +1,4 @@ +namespace PostFundManagement.Domain.Attributes; + +[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +public sealed class LogAttribute : Attribute; diff --git a/PostFundManagement.Domain/Entities/Award.cs b/PostFundManagement.Domain/Entities/Award.cs index cfaf78c..8f1aa9c 100644 --- a/PostFundManagement.Domain/Entities/Award.cs +++ b/PostFundManagement.Domain/Entities/Award.cs @@ -41,7 +41,7 @@ public sealed class AwardConfiguration : IEntityTypeConfiguration builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2); builder.HasOne(f => f.Tenant) - .WithMany() + .WithMany(f => f.Awards) .HasForeignKey(fk => fk.TenantId) .IsRequired() .OnDelete(DeleteBehavior.Restrict); diff --git a/PostFundManagement.Domain/Entities/Portfolio.cs b/PostFundManagement.Domain/Entities/Portfolio.cs index 7c2ff85..00f9215 100644 --- a/PostFundManagement.Domain/Entities/Portfolio.cs +++ b/PostFundManagement.Domain/Entities/Portfolio.cs @@ -32,7 +32,7 @@ public sealed class PortfolioConfiguration : IEntityTypeConfiguration builder.Property(f => f.Description).IsRequired(false).HasMaxLength(TextLength); builder.HasOne(f => f.Tenant) - .WithMany() + .WithMany(f => f.Portfolios) .HasForeignKey(fk => fk.TenantId) .IsRequired() .OnDelete(DeleteBehavior.Restrict); diff --git a/PostFundManagement.Domain/Entities/Programme.cs b/PostFundManagement.Domain/Entities/Programme.cs index 4ce3583..fde66d7 100644 --- a/PostFundManagement.Domain/Entities/Programme.cs +++ b/PostFundManagement.Domain/Entities/Programme.cs @@ -41,7 +41,7 @@ public sealed class ProgrammeConfiguration : IEntityTypeConfiguration .HasSentinel(ApprovalStatus.Pending); builder.HasOne(f => f.Tenant) - .WithMany() + .WithMany(f => f.Programmes) .HasForeignKey(fk => fk.TenantId) .IsRequired() .OnDelete(DeleteBehavior.NoAction); diff --git a/PostFundManagement.Domain/Entities/Project.cs b/PostFundManagement.Domain/Entities/Project.cs index 37be0b8..d3bde29 100644 --- a/PostFundManagement.Domain/Entities/Project.cs +++ b/PostFundManagement.Domain/Entities/Project.cs @@ -41,7 +41,7 @@ public sealed class ProjectConfiguration : IEntityTypeConfiguration .HasSentinel(ApprovalStatus.Pending); builder.HasOne(f => f.Tenant) - .WithMany() + .WithMany(f => f.Projects) .HasForeignKey(fk => fk.TenantId) .IsRequired() .OnDelete(DeleteBehavior.Restrict); diff --git a/PostFundManagement.Domain/Entities/Tenant.cs b/PostFundManagement.Domain/Entities/Tenant.cs index 6bab5aa..a318768 100644 --- a/PostFundManagement.Domain/Entities/Tenant.cs +++ b/PostFundManagement.Domain/Entities/Tenant.cs @@ -8,6 +8,14 @@ public class Tenant : Models.Tenant public virtual User? Creator { get; set; } public virtual User? Updator { get; set; } + + public ICollection Awards { get; set; } = []; + + public ICollection Projects { get; set; } = []; + + public ICollection Programmes { get; set; } = []; + + public ICollection Portfolios { get; set; } = []; } public sealed class TenantConfiguration : IEntityTypeConfiguration