diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj
index 4dbbb2e6..06f20aa7 100644
--- a/src/Application/Application.csproj
+++ b/src/Application/Application.csproj
@@ -74,6 +74,11 @@
True
ExportApprovalDatasQueryHandler.resx
+
+ True
+ True
+ ExportAuditTrailsQuery.resx
+
True
True
@@ -111,6 +116,10 @@
ResXFileCodeGenerator
ExportApprovalDatasQueryHandler.Designer.cs
+
+ ResXFileCodeGenerator
+ ExportAuditTrailsQuery.Designer.cs
+
ResXFileCodeGenerator
ImportKeyValuesCommandHandler.Designer.cs
diff --git a/src/Application/Common/Interfaces/IApplicationDbContext.cs b/src/Application/Common/Interfaces/IApplicationDbContext.cs
index 1e38c63f..17605ae9 100644
--- a/src/Application/Common/Interfaces/IApplicationDbContext.cs
+++ b/src/Application/Common/Interfaces/IApplicationDbContext.cs
@@ -1,6 +1,7 @@
using System.Threading;
using System.Threading.Tasks;
using CleanArchitecture.Razor.Domain.Entities;
+using CleanArchitecture.Razor.Domain.Entities.Audit;
using CleanArchitecture.Razor.Domain.Entities.Worflow;
using Microsoft.EntityFrameworkCore;
@@ -8,6 +9,7 @@ namespace CleanArchitecture.Razor.Application.Common.Interfaces
{
public interface IApplicationDbContext
{
+ DbSet AuditTrails { get; set; }
DbSet Customers { get; set; }
DbSet DocumentTypes { get; set; }
DbSet Documents { get; set; }
diff --git a/src/Application/Features/AuditTrails/DTOs/AuditTrailDto.cs b/src/Application/Features/AuditTrails/DTOs/AuditTrailDto.cs
new file mode 100644
index 00000000..4f981e84
--- /dev/null
+++ b/src/Application/Features/AuditTrails/DTOs/AuditTrailDto.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using AutoMapper;
+using CleanArchitecture.Razor.Application.Common.Mappings;
+using CleanArchitecture.Razor.Domain.Entities.Audit;
+
+namespace CleanArchitecture.Razor.Application.Features.AuditTrails.DTOs
+{
+ public class AuditTrailDto : IMapFrom
+ {
+ public void Mapping(Profile profile)
+ {
+ profile.CreateMap()
+ .ForMember(x => x.AuditType, s => s.MapFrom(y => y.AuditType.ToString()))
+ .ForMember(x => x.OldValues, s => s.MapFrom(y => JsonSerializer.Serialize(y.OldValues, null)))
+ .ForMember(x => x.NewValues, s => s.MapFrom(y => JsonSerializer.Serialize(y.NewValues, null)))
+ .ForMember(x => x.PrimaryKey, s => s.MapFrom(y => JsonSerializer.Serialize(y.PrimaryKey, null)))
+ .ForMember(x => x.AffectedColumns, s => s.MapFrom(y => JsonSerializer.Serialize(y.AffectedColumns, null)))
+ ;
+
+ }
+ public int Id { get; set; }
+ public string UserId { get; set; }
+ public string AuditType { get; set; }
+ public string TableName { get; set; }
+ public DateTime DateTime { get; set; }
+ public string OldValues { get; set; }
+ public string NewValues { get; set; }
+ public string AffectedColumns { get; set; }
+ public string PrimaryKey { get; set; }
+ }
+}
diff --git a/src/Application/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.cs b/src/Application/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.cs
new file mode 100644
index 00000000..9d212c4f
--- /dev/null
+++ b/src/Application/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AutoMapper;
+using CleanArchitecture.Razor.Application.Common.Extensions;
+using CleanArchitecture.Razor.Application.Common.Interfaces;
+using CleanArchitecture.Razor.Domain.Entities;
+using System.Linq.Dynamic.Core;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+using AutoMapper.QueryableExtensions;
+using Microsoft.Extensions.Localization;
+using CleanArchitecture.Razor.Application.Features.AuditTrails.DTOs;
+using CleanArchitecture.Razor.Domain.Entities.Worflow;
+using CleanArchitecture.Razor.Domain.Entities.Audit;
+
+namespace CleanArchitecture.Razor.Application.Features.AuditTrails.Queries.Export
+{
+ public class ExportAuditTrailsQuery : IRequest
+ {
+ public string filterRules { get; set; }
+ public string sort { get; set; } = "Id";
+ public string order { get; set; } = "desc";
+ }
+
+ public class ExportAuditTrailsQueryHandler :
+ IRequestHandler
+ {
+ private readonly IApplicationDbContext _context;
+ private readonly IMapper _mapper;
+ private readonly IExcelService _excelService;
+ private readonly IStringLocalizer _localizer;
+
+ public ExportAuditTrailsQueryHandler(
+ IApplicationDbContext context,
+ IMapper mapper,
+ IExcelService excelService,
+ IStringLocalizer localizer
+ )
+ {
+ _context = context;
+ _mapper = mapper;
+ _excelService = excelService;
+ _localizer = localizer;
+ }
+
+ public async Task Handle(ExportAuditTrailsQuery request, CancellationToken cancellationToken)
+ {
+ var filters = PredicateBuilder.FromFilter(request.filterRules);
+ var data = await _context.AuditTrails
+ .Where(filters)
+ .OrderBy($"{request.sort} {request.order}")
+ .ProjectTo(_mapper.ConfigurationProvider)
+ .ToListAsync(cancellationToken);
+ var result = await _excelService.ExportAsync(data,
+ new Dictionary>()
+ {
+ //{ _localizer["Id"], item => item.Id },
+ { _localizer["Date Time"], item => item.DateTime.ToString("yyyy-MM-dd HH:mm:ss") },
+ { _localizer["Table Name"], item => item.TableName },
+ { _localizer["Audit Type"], item => item.AuditType },
+ { _localizer["Old Values"], item => item.OldValues },
+ { _localizer["New Values"], item => item.NewValues },
+ { _localizer["Primary Key"], item => item.PrimaryKey },
+ }, _localizer["AuditTrails"]
+ );
+ return result;
+ }
+ }
+}
+
diff --git a/src/Application/Features/AuditTrails/Queries/PaginationQuery/AuditTrailsWithPaginationQuery.cs b/src/Application/Features/AuditTrails/Queries/PaginationQuery/AuditTrailsWithPaginationQuery.cs
new file mode 100644
index 00000000..daba2915
--- /dev/null
+++ b/src/Application/Features/AuditTrails/Queries/PaginationQuery/AuditTrailsWithPaginationQuery.cs
@@ -0,0 +1,62 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using AutoMapper;
+using CleanArchitecture.Razor.Application.Common.Extensions;
+using CleanArchitecture.Razor.Application.Common.Interfaces;
+using CleanArchitecture.Razor.Application.Common.Models;
+using CleanArchitecture.Razor.Application.Models;
+using CleanArchitecture.Razor.Domain.Entities;
+using System.Linq.Dynamic.Core;
+using MediatR;
+using CleanArchitecture.Razor.Application.Common.Mappings;
+using AutoMapper.QueryableExtensions;
+using CleanArchitecture.Razor.Application.Common.Specification;
+using CleanArchitecture.Razor.Application.Features.AuditTrails.DTOs;
+using CleanArchitecture.Razor.Domain.Entities.Audit;
+
+namespace CleanArchitecture.Razor.Application.AuditTrails.Queries.PaginationQuery
+{
+ public class AuditTrailsWithPaginationQuery : PaginationRequest, IRequest>
+ {
+
+
+ }
+ public class AuditTrailsQueryHandler : IRequestHandler>
+ {
+ private readonly ICurrentUserService _currentUserService;
+ private readonly IApplicationDbContext _context;
+ private readonly IMapper _mapper;
+
+ public AuditTrailsQueryHandler(
+ ICurrentUserService currentUserService,
+ IApplicationDbContext context,
+ IMapper mapper
+ )
+ {
+ _currentUserService = currentUserService;
+ _context = context;
+ _mapper = mapper;
+ }
+ public async Task> Handle(AuditTrailsWithPaginationQuery request, CancellationToken cancellationToken)
+ {
+ var filters = PredicateBuilder.FromFilter(request.FilterRules);
+
+ var data = await _context.AuditTrails
+ .Where(filters)
+ .OrderBy($"{request.Sort} {request.Order}")
+ .ProjectTo(_mapper.ConfigurationProvider)
+ .PaginatedDataAsync(request.Page, request.Rows);
+
+ return data;
+ }
+
+
+ }
+}
diff --git a/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.Designer.cs b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.Designer.cs
new file mode 100644
index 00000000..b1ca1cfa
--- /dev/null
+++ b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.Designer.cs
@@ -0,0 +1,118 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace CleanArchitecture.Razor.Application.Resources.Features.AuditTrails.Queries.Export {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class ExportAuditTrailsQuery {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal ExportAuditTrailsQuery() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("CleanArchitecture.Razor.Application.Resources.Features.AuditTrails.Queries.Export" +
+ ".ExportAuditTrailsQuery", typeof(ExportAuditTrailsQuery).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to AuditTrails.
+ ///
+ internal static string AuditTrails {
+ get {
+ return ResourceManager.GetString("AuditTrails", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Date Time.
+ ///
+ internal static string Date_Time {
+ get {
+ return ResourceManager.GetString("Date Time", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to New Values.
+ ///
+ internal static string New_Values {
+ get {
+ return ResourceManager.GetString("New Values", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Old Values.
+ ///
+ internal static string Old_Values {
+ get {
+ return ResourceManager.GetString("Old Values", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Primary Key.
+ ///
+ internal static string Primary_Key {
+ get {
+ return ResourceManager.GetString("Primary Key", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Table Name.
+ ///
+ internal static string Table_Name {
+ get {
+ return ResourceManager.GetString("Table Name", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.de-DE.resx b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.de-DE.resx
new file mode 100644
index 00000000..2b12c9d4
--- /dev/null
+++ b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.de-DE.resx
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Buchungsprotokolle
+
+
+ Terminzeit
+
+
+ Neue Werte
+
+
+ Alte Werte
+
+
+ Primärschlüssel
+
+
+ Tabellenname
+
+
\ No newline at end of file
diff --git a/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.en.resx b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.en.resx
new file mode 100644
index 00000000..d5f7260c
--- /dev/null
+++ b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.en.resx
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ AuditTrails
+
+
+ Date Time
+
+
+ New Values
+
+
+ Old Values
+
+
+ Primary Key
+
+
+ Table Name
+
+
\ No newline at end of file
diff --git a/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.ja-JP.resx b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.ja-JP.resx
new file mode 100644
index 00000000..9965bed5
--- /dev/null
+++ b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.ja-JP.resx
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 監査証跡
+
+
+ 日付時刻
+
+
+ 新しい値
+
+
+ 古い値
+
+
+ 主キー
+
+
+ テーブル名
+
+
\ No newline at end of file
diff --git a/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.resx b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.resx
new file mode 100644
index 00000000..d5f7260c
--- /dev/null
+++ b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.resx
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ AuditTrails
+
+
+ Date Time
+
+
+ New Values
+
+
+ Old Values
+
+
+ Primary Key
+
+
+ Table Name
+
+
\ No newline at end of file
diff --git a/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.ru.resx b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.ru.resx
new file mode 100644
index 00000000..3d18b495
--- /dev/null
+++ b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.ru.resx
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ AuditTrails
+
+
+ Дата Время
+
+
+ Новые ценности
+
+
+ Старые ценности
+
+
+ Основной ключ
+
+
+ Имя таблицы
+
+
\ No newline at end of file
diff --git a/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.zh-CN.resx b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.zh-CN.resx
new file mode 100644
index 00000000..14ce030f
--- /dev/null
+++ b/src/Application/Resources/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.zh-CN.resx
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 审计跟踪
+
+
+ 记录时间
+
+
+ 新值
+
+
+ 旧值
+
+
+ 主键
+
+
+ 表名
+
+
\ No newline at end of file
diff --git a/src/Domain/Common/IAuditTrial.cs b/src/Domain/Common/IAuditTrial.cs
new file mode 100644
index 00000000..c3109178
--- /dev/null
+++ b/src/Domain/Common/IAuditTrial.cs
@@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CleanArchitecture.Razor.Domain.Common
+{
+ public interface IAuditTrial
+ {
+ }
+}
diff --git a/src/Domain/Domain.csproj b/src/Domain/Domain.csproj
index 23098dbf..82714c00 100644
--- a/src/Domain/Domain.csproj
+++ b/src/Domain/Domain.csproj
@@ -6,4 +6,8 @@
CleanArchitecture.Razor.Domain
+
+
+
+
diff --git a/src/Domain/Entities/Audit/AuditTrail.cs b/src/Domain/Entities/Audit/AuditTrail.cs
new file mode 100644
index 00000000..7c269ee2
--- /dev/null
+++ b/src/Domain/Entities/Audit/AuditTrail.cs
@@ -0,0 +1,30 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CleanArchitecture.Razor.Domain.Common;
+using CleanArchitecture.Razor.Domain.Enums;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+
+namespace CleanArchitecture.Razor.Domain.Entities.Audit
+{
+ public class AuditTrail: IEntity
+ {
+ public int Id { get; set; }
+ public string UserId { get; set; }
+ public AuditType AuditType { get; set; }
+ public string TableName { get; set; }
+ public DateTime DateTime { get; set; }
+ public Dictionary OldValues { get; set; } = new();
+ public Dictionary NewValues { get; set; } = new();
+ public ICollection AffectedColumns { get; set; }
+ public Dictionary PrimaryKey { get; set; } = new();
+
+ public List TemporaryProperties { get; } = new();
+ public bool HasTemporaryProperties => TemporaryProperties.Any();
+ }
+}
diff --git a/src/Domain/Entities/Customer.cs b/src/Domain/Entities/Customer.cs
index 94e094ba..14041e13 100644
--- a/src/Domain/Entities/Customer.cs
+++ b/src/Domain/Entities/Customer.cs
@@ -12,7 +12,7 @@
namespace CleanArchitecture.Razor.Domain.Entities
{
- public partial class Customer : AuditableEntity, IHasDomainEvent
+ public partial class Customer : AuditableEntity, IHasDomainEvent,IAuditTrial
{
public int Id { get; set; }
public string Name { get; set; }
diff --git a/src/Domain/Enums/AuditType.cs b/src/Domain/Enums/AuditType.cs
new file mode 100644
index 00000000..fc565262
--- /dev/null
+++ b/src/Domain/Enums/AuditType.cs
@@ -0,0 +1,19 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CleanArchitecture.Razor.Domain.Enums
+{
+ public enum AuditType
+ {
+ None,
+ Create,
+ Update,
+ Delete
+ }
+}
diff --git a/src/Infrastructure/Constants/Permission/Permissions.cs b/src/Infrastructure/Constants/Permission/Permissions.cs
index 61538999..4f72e626 100644
--- a/src/Infrastructure/Constants/Permission/Permissions.cs
+++ b/src/Infrastructure/Constants/Permission/Permissions.cs
@@ -7,6 +7,14 @@ namespace CleanArchitecture.Razor.Infrastructure.Constants.Permission
{
public static class Permissions
{
+ [DisplayName("AuditTrails")]
+ [Description("AuditTrails Permissions")]
+ public static class AuditTrails
+ {
+ public const string View = "Permissions.AuditTrails.View";
+ public const string Search = "Permissions.AuditTrails.Search";
+ public const string Export = "Permissions.AuditTrails.Export";
+ }
[DisplayName("Workflow")]
[Description("Approval Permissions")]
public static class Approval
diff --git a/src/Infrastructure/Persistence/ApplicationDbContext.cs b/src/Infrastructure/Persistence/ApplicationDbContext.cs
index ac901ff0..505c70d7 100644
--- a/src/Infrastructure/Persistence/ApplicationDbContext.cs
+++ b/src/Infrastructure/Persistence/ApplicationDbContext.cs
@@ -1,11 +1,14 @@
using CleanArchitecture.Razor.Application.Common.Interfaces;
using CleanArchitecture.Razor.Domain.Common;
using CleanArchitecture.Razor.Domain.Entities;
+using CleanArchitecture.Razor.Domain.Entities.Audit;
using CleanArchitecture.Razor.Domain.Entities.Worflow;
+using CleanArchitecture.Razor.Domain.Enums;
using CleanArchitecture.Razor.Infrastructure.Identity;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
+using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
@@ -32,7 +35,7 @@ public ApplicationDbContext(
_domainEventService = domainEventService;
_dateTime = dateTime;
}
-
+ public DbSet AuditTrails { get; set; }
public DbSet Customers { get; set; }
public DbSet DocumentTypes { get; set; }
public DbSet Documents { get; set; }
@@ -42,6 +45,8 @@ public ApplicationDbContext(
public override async Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
{
+ var auditEntries = OnBeforeSaveChanges(_currentUserService.UserId);
+
foreach (var entry in ChangeTracker.Entries())
{
switch (entry.State)
@@ -69,7 +74,7 @@ public ApplicationDbContext(
var result = await base.SaveChangesAsync(cancellationToken);
await DispatchEvents();
-
+ await OnAfterSaveChanges(auditEntries, cancellationToken);
return result;
}
@@ -95,5 +100,95 @@ private async Task DispatchEvents()
await _domainEventService.Publish(domainEventEntity);
}
}
+
+
+ private List OnBeforeSaveChanges(string userId)
+ {
+ ChangeTracker.DetectChanges();
+ var auditEntries = new List();
+ foreach (var entry in ChangeTracker.Entries())
+ {
+ if (entry.Entity is AuditTrail ||
+ entry.State == EntityState.Detached ||
+ entry.State == EntityState.Unchanged)
+ continue;
+
+ var auditEntry = new AuditTrail()
+ {
+ DateTime = _dateTime.Now,
+ TableName = entry.Entity.GetType().Name,
+ UserId = userId,
+ AffectedColumns = new List()
+ };
+ auditEntries.Add(auditEntry);
+ foreach (var property in entry.Properties)
+ {
+
+ if (property.IsTemporary)
+ {
+ auditEntry.TemporaryProperties.Add(property);
+ continue;
+ }
+ string propertyName = property.Metadata.Name;
+ if (property.Metadata.IsPrimaryKey())
+ {
+ auditEntry.PrimaryKey[propertyName] = property.CurrentValue;
+ continue;
+ }
+
+ switch (entry.State)
+ {
+ case EntityState.Added:
+ auditEntry.AuditType = AuditType.Create;
+ auditEntry.NewValues[propertyName] = property.CurrentValue;
+ break;
+
+ case EntityState.Deleted:
+ auditEntry.AuditType = AuditType.Delete;
+ auditEntry.OldValues[propertyName] = property.OriginalValue;
+ break;
+
+ case EntityState.Modified:
+ if (property.IsModified && property.OriginalValue?.Equals(property.CurrentValue) == false)
+ {
+ auditEntry.AffectedColumns.Add(propertyName);
+ auditEntry.AuditType = AuditType.Update;
+ auditEntry.OldValues[propertyName] = property.OriginalValue;
+ auditEntry.NewValues[propertyName] = property.CurrentValue;
+ }
+ break;
+ }
+ }
+ }
+
+ foreach (var auditEntry in auditEntries.Where(_ => !_.HasTemporaryProperties))
+ {
+ AuditTrails.Add(auditEntry);
+ }
+ return auditEntries.Where(_ => _.HasTemporaryProperties).ToList();
+ }
+
+ private Task OnAfterSaveChanges(List auditEntries, CancellationToken cancellationToken = new())
+ {
+ if (auditEntries == null || auditEntries.Count == 0)
+ return Task.CompletedTask;
+
+ foreach (var auditEntry in auditEntries)
+ {
+ foreach (var prop in auditEntry.TemporaryProperties)
+ {
+ if (prop.Metadata.IsPrimaryKey())
+ {
+ auditEntry.PrimaryKey[prop.Metadata.Name] = prop.CurrentValue;
+ }
+ else
+ {
+ auditEntry.NewValues[prop.Metadata.Name] = prop.CurrentValue;
+ }
+ }
+ AuditTrails.Add(auditEntry);
+ }
+ return SaveChangesAsync(cancellationToken);
+ }
}
}
diff --git a/src/Infrastructure/Persistence/Configurations/AuditTrailConfiguration.cs b/src/Infrastructure/Persistence/Configurations/AuditTrailConfiguration.cs
new file mode 100644
index 00000000..a4850d1c
--- /dev/null
+++ b/src/Infrastructure/Persistence/Configurations/AuditTrailConfiguration.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using CleanArchitecture.Razor.Domain.Entities;
+using CleanArchitecture.Razor.Domain.Entities.Audit;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace CleanArchitecture.Infrastructure.Persistence.Configurations
+{
+ public class AuditTrailConfiguration : IEntityTypeConfiguration
+ {
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.Property(t => t.AuditType)
+ .HasConversion();
+ builder.Property(e => e.AffectedColumns)
+ .HasConversion(
+ v => JsonSerializer.Serialize(v, null),
+ v => JsonSerializer.Deserialize>(v, null),
+ new ValueComparer>(
+ (c1, c2) => c1.SequenceEqual(c2),
+ c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
+ c => (ICollection)c.ToList()));
+
+ builder.Property(u => u.OldValues)
+ .HasConversion(
+ d => JsonSerializer.Serialize(d, null),
+ s => JsonSerializer.Deserialize>(s, null)
+ );
+ builder.Property(u => u.NewValues)
+ .HasConversion(
+ d => JsonSerializer.Serialize(d, null),
+ s => JsonSerializer.Deserialize>(s, null)
+ );
+ builder.Property(u => u.PrimaryKey)
+ .HasConversion(
+ d => JsonSerializer.Serialize(d, null),
+ s => JsonSerializer.Deserialize>(s, null)
+ );
+
+ builder.Ignore(x => x.TemporaryProperties);
+ builder.Ignore(x => x.HasTemporaryProperties);
+ }
+ }
+}
diff --git a/src/SmartAdmin.WebUI/Pages/Approval/Histories.cshtml b/src/SmartAdmin.WebUI/Pages/Approval/Histories.cshtml
index 59fdfa75..6a52b82b 100644
--- a/src/SmartAdmin.WebUI/Pages/Approval/Histories.cshtml
+++ b/src/SmartAdmin.WebUI/Pages/Approval/Histories.cshtml
@@ -90,23 +90,6 @@
sortOrder: 'desc',
pageSize: 15,
pageList: [10, 15, 30, 50, 100, 1000],
- onBeforeLoad: function () {
- $('#approvalbutton').prop('disabled', true);
- },
- onCheckAll: function (rows) {
- const checked = $(this).datagrid('getChecked').length > 0;
- $('#approvalbutton').prop('disabled', !checked);
- },
- onUncheckAll: function () {
- $('#approvalbutton').prop('disabled', true);
- },
- onCheck: function () {
- $('#approvalbutton').prop('disabled', false);
- },
- onUncheck: function () {
- const checked = $(this).datagrid('getChecked').length > 0;
- $('#approvalbutton').prop('disabled', !checked);
- },
columns: [[
{ field: 'ck', checkbox: true },
{ field: 'WorkflowName', title: '@_localizer["Workflow Name"]', sortable: true, width: 200 },
diff --git a/src/SmartAdmin.WebUI/Pages/AuditTrails/Index.cshtml b/src/SmartAdmin.WebUI/Pages/AuditTrails/Index.cshtml
new file mode 100644
index 00000000..3234e6cc
--- /dev/null
+++ b/src/SmartAdmin.WebUI/Pages/AuditTrails/Index.cshtml
@@ -0,0 +1,168 @@
+@page
+@using CleanArchitecture.Razor.Infrastructure.Constants.Permission
+@model SmartAdmin.WebUI.Pages.AuditTrails.IndexModel
+@inject Microsoft.Extensions.Localization.IStringLocalizer _localizer
+@inject Microsoft.AspNetCore.Authorization.IAuthorizationService _authorizationService
+@{
+ ViewData["Title"] = _localizer["Audit Trails"].Value;
+ ViewData["PageName"] = "audittrails_index";
+ ViewData["Category1"] = _localizer["Authorization"].Value;
+ ViewData["Heading"] = _localizer["Audit Trails"].Value;
+ ViewData["PageDescription"] = _localizer["See all available options"].Value;
+ ViewData["PreemptiveClass"] = "Default";
+ var _canSearch = await _authorizationService.AuthorizeAsync(User, null, Permissions.AuditTrails.Search);
+ var _canExport = await _authorizationService.AuthorizeAsync(User, null, Permissions.AuditTrails.Export);
+
+}
+@section HeadBlock {
+
+
+
+
+
+}
+
+
+
+@section ScriptsBlock {
+
+
+
+
+
+
+
+}
diff --git a/src/SmartAdmin.WebUI/Pages/AuditTrails/Index.cshtml.cs b/src/SmartAdmin.WebUI/Pages/AuditTrails/Index.cshtml.cs
new file mode 100644
index 00000000..384517df
--- /dev/null
+++ b/src/SmartAdmin.WebUI/Pages/AuditTrails/Index.cshtml.cs
@@ -0,0 +1,55 @@
+using System.Threading.Tasks;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.Extensions.Localization;
+using Microsoft.AspNetCore.Authorization;
+using CleanArchitecture.Razor.Application.Features.ApprovalDatas.Queries.Pagination;
+using CleanArchitecture.Razor.Infrastructure.Constants.Permission;
+using CleanArchitecture.Razor.Application.Features.ApprovalDatas.Queries.Export;
+using CleanArchitecture.Razor.Application.AuditTrails.Queries.PaginationQuery;
+using CleanArchitecture.Razor.Application.Features.AuditTrails.Queries.Export;
+
+namespace SmartAdmin.WebUI.Pages.AuditTrails
+{
+ [Authorize(policy: Permissions.AuditTrails.View)]
+ public class IndexModel : PageModel
+ {
+ private readonly ISender _mediator;
+ private readonly IStringLocalizer _localizer;
+ [BindProperty]
+ public string WorkflowId { get; set; }
+ [BindProperty]
+ public string Comments { get; set; }
+ [BindProperty]
+ public string Outcome { get; set; }
+ [BindProperty]
+ public string Approver { get; set; }
+ public IndexModel(
+ ISender mediator,
+ IStringLocalizer localizer
+ )
+ {
+ _mediator = mediator;
+ _localizer = localizer;
+ }
+ public async Task OnGetAsync()
+ {
+
+ }
+ public async Task OnGetDataAsync([FromQuery] AuditTrailsWithPaginationQuery command)
+ {
+ var result = await _mediator.Send(command);
+ return new JsonResult(result);
+ }
+ public async Task OnPostExportAsync([FromBody] ExportAuditTrailsQuery command)
+ {
+ var result = await _mediator.Send(command);
+ return File(result, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", _localizer["ApprovalHistories"] + ".xlsx");
+ }
+
+
+
+
+ }
+}
diff --git a/src/SmartAdmin.WebUI/Program.cs b/src/SmartAdmin.WebUI/Program.cs
index 89698ca6..37e351a4 100644
--- a/src/SmartAdmin.WebUI/Program.cs
+++ b/src/SmartAdmin.WebUI/Program.cs
@@ -15,7 +15,7 @@ namespace SmartAdmin.WebUI
{
public class Program
{
- public async static Task Main(string[] args)
+ public static async Task Main(string[] args)
{
var filePath = Path.Combine(Directory.GetCurrentDirectory(), "Files");
if (!Directory.Exists(filePath))
diff --git a/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.de-DE.resx b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.de-DE.resx
new file mode 100644
index 00000000..8fa717f3
--- /dev/null
+++ b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.de-DE.resx
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Buchungsprotokolle
+
+
+ Audit-Typ
+
+
+ Genehmigung
+
+
+ Terminzeit
+
+
+ Excel exportieren
+
+
+ Exportieren fehlgeschlagen
+
+
+ Exporterfolg
+
+
+ Neue Werte
+
+
+ Alte Werte
+
+
+ Primärschlüssel
+
+
+ Suche
+
+
+ Alle verfügbaren Optionen anzeigen
+
+
+ Tabellenname
+
+
\ No newline at end of file
diff --git a/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.en.resx b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.en.resx
new file mode 100644
index 00000000..f239779b
--- /dev/null
+++ b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.en.resx
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Audit Trails
+
+
+ Audit Type
+
+
+ Authorization
+
+
+ Date Time
+
+
+ Export Excel
+
+
+ Export fail
+
+
+ Export Success
+
+
+ New Values
+
+
+ Old Values
+
+
+ Primary Key
+
+
+ Search
+
+
+ See all available options
+
+
+ Table Name
+
+
\ No newline at end of file
diff --git a/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.ja-JP.resx b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.ja-JP.resx
new file mode 100644
index 00000000..b7159ec9
--- /dev/null
+++ b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.ja-JP.resx
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 監査証跡
+
+
+ 監査タイプ
+
+
+ 承認
+
+
+ 日付時刻
+
+
+ Excelをエクスポートする
+
+
+ エクスポートに失敗する
+
+
+ エクスポートの成功
+
+
+ 新しい値
+
+
+ 古い値
+
+
+ 主キー
+
+
+ 検索
+
+
+ 利用可能なすべてのオプションを表示
+
+
+ テーブル名
+
+
\ No newline at end of file
diff --git a/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.resx b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.resx
new file mode 100644
index 00000000..f239779b
--- /dev/null
+++ b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.resx
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Audit Trails
+
+
+ Audit Type
+
+
+ Authorization
+
+
+ Date Time
+
+
+ Export Excel
+
+
+ Export fail
+
+
+ Export Success
+
+
+ New Values
+
+
+ Old Values
+
+
+ Primary Key
+
+
+ Search
+
+
+ See all available options
+
+
+ Table Name
+
+
\ No newline at end of file
diff --git a/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.ru.resx b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.ru.resx
new file mode 100644
index 00000000..fcf190aa
--- /dev/null
+++ b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.ru.resx
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Журналы аудита
+
+
+ Тип аудита
+
+
+ Авторизация
+
+
+ Дата Время
+
+
+ Экспорт в Excel
+
+
+ Ошибка экспорта
+
+
+ Успех экспорта
+
+
+ Новые ценности
+
+
+ Старые ценности
+
+
+ Основной ключ
+
+
+ Поиск
+
+
+ Посмотреть все доступные варианты
+
+
+ Имя таблицы
+
+
\ No newline at end of file
diff --git a/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.zh-CN.resx b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.zh-CN.resx
new file mode 100644
index 00000000..db5ed90b
--- /dev/null
+++ b/src/SmartAdmin.WebUI/Resources/Pages/AuditTrails/IndexModel.zh-CN.resx
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 审计跟踪
+
+
+ 审核类型
+
+
+ 授权
+
+
+ 记录时间
+
+
+ 导出 Excel
+
+
+ 导出失败
+
+
+ 出口成功
+
+
+ 新值
+
+
+ 旧值
+
+
+ 主键
+
+
+ 搜索
+
+
+ 查看所有可用选项
+
+
+ 表名
+
+
\ No newline at end of file
diff --git a/src/SmartAdmin.WebUI/nav.json b/src/SmartAdmin.WebUI/nav.json
index de780228..8ce18d13 100644
--- a/src/SmartAdmin.WebUI/nav.json
+++ b/src/SmartAdmin.WebUI/nav.json
@@ -81,6 +81,11 @@
"title": "Roles",
"href": "authorization_roles.html",
"roles": []
+ },
+ {
+ "title": "Audit Trails",
+ "href": "audittrails_index.html",
+ "roles": []
}
]
},