From 94bf317fbacb9cb73f8a9e5761b002d346eefe40 Mon Sep 17 00:00:00 2001 From: Zhu Date: Thu, 13 Mar 2025 11:15:57 +0800 Subject: [PATCH 1/4] commit --- src/Application/Application.csproj | 2 +- .../Common/Extensions/QueryableExtensions.cs | 30 ++++++ .../Common/Security/UserProfile.cs | 5 - .../Security/UserProfileStateService.cs | 11 +- src/Application/DependencyInjection.cs | 1 + .../AuditTrails/DTOs/AuditTrailDto.cs | 13 +++ .../AuditTrails/Mappers/AuditTrailMapper.cs | 19 ---- .../Queries/Export/ExportAuditTrailsQuery.cs | 7 +- .../AuditTrailsWithPaginationQuery.cs | 10 +- .../Commands/AddEdit/AddEditContactCommand.cs | 48 +++++---- .../Commands/Create/CreateContactCommand.cs | 67 +++++++----- .../Commands/Import/ImportContactsCommand.cs | 10 +- .../Commands/Update/UpdateContactCommand.cs | 19 +++- .../Features/Contacts/DTOs/ContactDto.cs | 26 +++-- .../Contacts/Mappers/ContactMapper.cs | 41 ------- .../Queries/Export/ExportContactsQuery.cs | 11 +- .../Queries/GetAll/GetAllContactsQuery.cs | 6 +- .../Queries/GetById/GetContactByIdQuery.cs | 12 ++- .../Pagination/ContactsPaginationQuery.cs | 38 +++---- .../AddEdit/AddEditDocumentCommand.cs | 16 ++- .../Features/Documents/DTOs/DocumentDto.cs | 14 ++- .../Documents/Mappers/DocumentMapper.cs | 25 ----- .../DocumentsWithPaginationQuery.cs | 8 +- .../Identity/DTOs/ApplicationRoleDto.cs | 11 +- .../Identity/DTOs/ApplicationUserDto.cs | 57 +++++++++- .../Identity/Mappers/ApplicationRoleMapper.cs | 14 --- .../Identity/Mappers/ApplicationUserMapper.cs | 58 ---------- .../AddEdit/AddEditPicklistSetCommand.cs | 18 +++- .../PicklistSets/DTOs/PicklistSetDto.cs | 8 ++ .../PicklistSets/Mappers/PicklistMapper.cs | 16 --- .../Queries/ByName/PicklistSetsQueryByName.cs | 9 +- .../Queries/Export/ExportPicklistSetsQuery.cs | 7 +- .../Queries/GetAll/GetAllPicklistSetsQuery.cs | 8 +- .../PicklistSetsWithPaginationQuery.cs | 8 +- .../Commands/AddEdit/AddEditProductCommand.cs | 17 ++- .../Commands/Import/ImportProductsCommand.cs | 11 +- .../Features/Products/DTOs/ProductDto.cs | 7 ++ .../Products/Mappers/ProductMapper.cs | 18 ---- .../Queries/Export/ExportProductsQuery.cs | 8 +- .../Queries/GetAll/GetAllProductsQuery.cs | 9 +- .../Pagination/ProductsPaginationQuery.cs | 8 +- .../Features/SystemLogs/DTOs/SystemLogDto.cs | 10 ++ .../SystemLogs/Mappers/SystemLogMapper.cs | 9 -- .../Queries/Export/ExportSystemLogsQuery.cs | 7 +- .../SystemLogsWithPaginationQuery.cs | 9 +- .../Commands/AddEdit/AddEditTenantCommand.cs | 18 +++- .../Features/Tenants/DTOs/TenantDto.cs | 7 ++ .../Features/Tenants/Mappers/TenantMapper.cs | 16 --- .../Queries/GetAll/GetAllTenantsQuery.cs | 9 +- .../Pagination/TenantsPaginationQuery.cs | 8 +- src/Application/_Imports.cs | 4 +- .../Services/Identity/IdentityService.cs | 14 ++- .../Services/Identity/UserService.cs | 10 +- .../Services/MultiTenant/TenantService.cs | 15 +-- .../Services/PicklistService.cs | 12 ++- src/Infrastructure/_Imports.cs | 4 +- src/Server.UI/Pages/Contacts/Contacts.razor | 66 ++++++------ .../Pages/Contacts/EditContact.razor | 3 +- src/Server.UI/Pages/Documents/Documents.razor | 3 +- .../Pages/Identity/Roles/Roles.razor | 3 +- .../Pages/Identity/Users/Profile.razor | 2 +- .../Pages/Identity/Users/Users.razor | 5 +- .../Pages/PicklistSets/PicklistSets.razor | 3 +- src/Server.UI/Pages/Products/Products.razor | 85 ++++++++------- src/Server.UI/Pages/Tenants/Tenants.razor | 3 +- src/Server.UI/Server.UI.csproj | 2 +- src/Server.UI/_Imports.cs | 1 + src/Server.UI/_Imports.razor | 5 +- .../Mappings/ApplicationUserMapperTests.cs | 102 ------------------ .../Common/Mappings/MappingTests.cs | 70 ++++++++++++ 70 files changed, 655 insertions(+), 581 deletions(-) delete mode 100644 src/Application/Features/AuditTrails/Mappers/AuditTrailMapper.cs delete mode 100644 src/Application/Features/Contacts/Mappers/ContactMapper.cs delete mode 100644 src/Application/Features/Documents/Mappers/DocumentMapper.cs delete mode 100644 src/Application/Features/Identity/Mappers/ApplicationRoleMapper.cs delete mode 100644 src/Application/Features/Identity/Mappers/ApplicationUserMapper.cs delete mode 100644 src/Application/Features/PicklistSets/Mappers/PicklistMapper.cs delete mode 100644 src/Application/Features/Products/Mappers/ProductMapper.cs delete mode 100644 src/Application/Features/SystemLogs/Mappers/SystemLogMapper.cs delete mode 100644 src/Application/Features/Tenants/Mappers/TenantMapper.cs delete mode 100644 tests/Application.UnitTests/Common/Mappings/ApplicationUserMapperTests.cs create mode 100644 tests/Application.UnitTests/Common/Mappings/MappingTests.cs diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index 74f1bd5a9..85b495ebc 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -8,6 +8,7 @@ default + @@ -20,7 +21,6 @@ - diff --git a/src/Application/Common/Extensions/QueryableExtensions.cs b/src/Application/Common/Extensions/QueryableExtensions.cs index 2b29b2fb2..550a72b0e 100644 --- a/src/Application/Common/Extensions/QueryableExtensions.cs +++ b/src/Application/Common/Extensions/QueryableExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Ardalis.Specification.EntityFrameworkCore; +using AutoMapper.QueryableExtensions; using CleanArchitecture.Blazor.Domain.Common.Entities; namespace CleanArchitecture.Blazor.Application.Common.Extensions; @@ -29,6 +30,35 @@ public static IQueryable ApplySpecification(this IQueryable query, ISpe return SpecificationEvaluator.Default.GetQuery(query, spec, evaluateCriteriaOnly); } + /// + /// Extension method to provided ordered queryable data to a paginated result set. + /// + /// + /// This method will apply the given specification to the query, paginate the results, and project them to the desired + /// result type. + /// + /// Source type of the entities in the query + /// Destination type to which the entities should be projected + /// The original ordered query to project and paginate + /// The specification to apply to the query before projection and pagination + /// The desired page number of the paginated results + /// The number of items per page in the paginated results + /// Configuration for the projection + /// Optional cancellation token to cancel the operation + /// The paginated and projected data + public static async Task> ProjectToPaginatedDataAsync( + this IOrderedQueryable query, ISpecification spec, int pageNumber, int pageSize, + IConfigurationProvider configuration, CancellationToken cancellationToken = default) where T : class, IEntity + { + var specificationEvaluator = SpecificationEvaluator.Default; + var count = await specificationEvaluator.GetQuery(query.AsNoTracking(), spec).CountAsync(); + var data = await specificationEvaluator.GetQuery(query.AsNoTracking(), spec).Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ProjectTo(configuration) + .ToListAsync(cancellationToken); + return new PaginatedData(data, count, pageNumber, pageSize); + } + /// /// Projects the query to a paginated data asynchronously. /// diff --git a/src/Application/Common/Security/UserProfile.cs b/src/Application/Common/Security/UserProfile.cs index abfc28d6b..1b3fc6fde 100644 --- a/src/Application/Common/Security/UserProfile.cs +++ b/src/Application/Common/Security/UserProfile.cs @@ -24,8 +24,3 @@ public class UserProfile : TimeZoneInfo.FindSystemTimeZoneById(TimeZoneId).BaseUtcOffset; } -[Mapper] -public static partial class UserProfileMapper -{ - public static partial ChangeUserProfileModel ToChangeUserProfileModel(UserProfile entity); -} diff --git a/src/Application/Common/Security/UserProfileStateService.cs b/src/Application/Common/Security/UserProfileStateService.cs index 5cee8807e..80bd80e76 100644 --- a/src/Application/Common/Security/UserProfileStateService.cs +++ b/src/Application/Common/Security/UserProfileStateService.cs @@ -1,4 +1,5 @@ -using CleanArchitecture.Blazor.Application.Features.Identity.Mappers; +using AutoMapper.QueryableExtensions; +using CleanArchitecture.Blazor.Application.Features.Identity.DTOs; using CleanArchitecture.Blazor.Domain.Identity; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; @@ -11,13 +12,17 @@ public class UserProfileStateService private TimeSpan RefreshInterval => TimeSpan.FromSeconds(60); private UserProfile _userProfile = new UserProfile() { Email="", UserId="", UserName="" }; private readonly UserManager _userManager; + private readonly IMapper _mapper; private readonly IFusionCache _fusionCache; - public UserProfileStateService(IServiceScopeFactory scopeFactory, + public UserProfileStateService( + IMapper mapper, + IServiceScopeFactory scopeFactory, IFusionCache fusionCache) { var scope = scopeFactory.CreateScope(); _userManager = scope.ServiceProvider.GetRequiredService>(); + _mapper = mapper; _fusionCache = fusionCache; } @@ -26,7 +31,7 @@ public async Task InitializeAsync(string userName) var key = GetApplicationUserCacheKey(userName); var result = await _fusionCache.GetOrSetAsync(key, _ => _userManager.Users.Where(x => x.UserName == userName).Include(x => x.UserRoles) - .ThenInclude(x => x.Role).ProjectTo() + .ThenInclude(x => x.Role).ProjectTo(_mapper.ConfigurationProvider) .FirstOrDefaultAsync(), RefreshInterval); if(result is not null) { diff --git a/src/Application/DependencyInjection.cs b/src/Application/DependencyInjection.cs index 08f43232d..986f45b53 100644 --- a/src/Application/DependencyInjection.cs +++ b/src/Application/DependencyInjection.cs @@ -13,6 +13,7 @@ public static class DependencyInjection { public static IServiceCollection AddApplication(this IServiceCollection services) { + services.AddAutoMapper(config => { config.AddMaps(Assembly.GetExecutingAssembly()); }); services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); services.AddTransient(typeof(IRequestExceptionHandler<,,>), typeof(DbExceptionHandler<,,>)); services.AddMediatR(config => diff --git a/src/Application/Features/AuditTrails/DTOs/AuditTrailDto.cs b/src/Application/Features/AuditTrails/DTOs/AuditTrailDto.cs index 430a9d47f..8814968a9 100644 --- a/src/Application/Features/AuditTrails/DTOs/AuditTrailDto.cs +++ b/src/Application/Features/AuditTrails/DTOs/AuditTrailDto.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. + + using CleanArchitecture.Blazor.Application.Features.Identity.DTOs; namespace CleanArchitecture.Blazor.Application.Features.AuditTrails.DTOs; @@ -26,4 +28,15 @@ public class AuditTrailDto [Description("Is Successful")] public bool IsSuccessful=> string.IsNullOrEmpty(ErrorMessage); + + private class Mapping : Profile + { + public Mapping() + { + CreateMap(MemberList.None) + .ForMember(x => x.PrimaryKey, + s => s.MapFrom(y => JsonSerializer.Serialize(y.PrimaryKey, DefaultJsonSerializerOptions.Options))); + } + } + } \ No newline at end of file diff --git a/src/Application/Features/AuditTrails/Mappers/AuditTrailMapper.cs b/src/Application/Features/AuditTrails/Mappers/AuditTrailMapper.cs deleted file mode 100644 index 322f4d507..000000000 --- a/src/Application/Features/AuditTrails/Mappers/AuditTrailMapper.cs +++ /dev/null @@ -1,19 +0,0 @@ -using CleanArchitecture.Blazor.Application.Features.AuditTrails.DTOs; -using CleanArchitecture.Blazor.Application.Features.Identity.Mappers; - -namespace CleanArchitecture.Blazor.Application.Features.AuditTrails.Mappers; -#pragma warning disable RMG020 -#pragma warning disable RMG012 -[Mapper] -[UseStaticMapper(typeof(ApplicationUserMapper))] -public static partial class AuditTrailMapper -{ - public static partial IQueryable ProjectTo(this IQueryable q); - [MapPropertyFromSource(nameof(AuditTrail.PrimaryKey), Use = nameof(MapPrimaryKey))] - public static partial AuditTrailDto ToDto(AuditTrail auditTrail); - - private static string MapPrimaryKey(AuditTrail source) - { - return JsonSerializer.Serialize(source.PrimaryKey, new JsonSerializerOptions()); - } -} diff --git a/src/Application/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.cs b/src/Application/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.cs index fbe9496cf..2a421d583 100644 --- a/src/Application/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.cs +++ b/src/Application/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using AutoMapper.QueryableExtensions; using CleanArchitecture.Blazor.Application.Features.AuditTrails.DTOs; -using CleanArchitecture.Blazor.Application.Features.AuditTrails.Mappers; namespace CleanArchitecture.Blazor.Application.Features.AuditTrails.Queries.Export; @@ -18,16 +18,19 @@ public class ExportAuditTrailsQueryHandler : { private readonly IApplicationDbContext _context; private readonly IExcelService _excelService; + private readonly IMapper _mapper; private readonly IStringLocalizer _localizer; public ExportAuditTrailsQueryHandler( IApplicationDbContext context, IExcelService excelService, + IMapper mapper, IStringLocalizer localizer ) { _context = context; _excelService = excelService; + _mapper = mapper; _localizer = localizer; } @@ -36,7 +39,7 @@ public async Task Handle(ExportAuditTrailsQuery request, CancellationTok var data = await _context.AuditTrails .Where(x => x.TableName!.Contains(request.Keyword)) .OrderBy($"{request.OrderBy} {request.SortDirection}") - .ProjectTo() + .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(cancellationToken); var result = await _excelService.ExportAsync(data, new Dictionary> diff --git a/src/Application/Features/AuditTrails/Queries/PaginationQuery/AuditTrailsWithPaginationQuery.cs b/src/Application/Features/AuditTrails/Queries/PaginationQuery/AuditTrailsWithPaginationQuery.cs index c3abcd8d4..e46ea127c 100644 --- a/src/Application/Features/AuditTrails/Queries/PaginationQuery/AuditTrailsWithPaginationQuery.cs +++ b/src/Application/Features/AuditTrails/Queries/PaginationQuery/AuditTrailsWithPaginationQuery.cs @@ -3,7 +3,6 @@ using CleanArchitecture.Blazor.Application.Features.AuditTrails.Caching; using CleanArchitecture.Blazor.Application.Features.AuditTrails.DTOs; -using CleanArchitecture.Blazor.Application.Features.AuditTrails.Mappers; using CleanArchitecture.Blazor.Application.Features.AuditTrails.Specifications; namespace CleanArchitecture.Blazor.Application.Features.AuditTrails.Queries.PaginationQuery; @@ -24,20 +23,23 @@ public override string ToString() public class AuditTrailsQueryHandler : IRequestHandler> { private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; public AuditTrailsQueryHandler( - IApplicationDbContext context + IApplicationDbContext context, + IMapper mapper ) { _context = context; + _mapper = mapper; } public async Task> Handle(AuditTrailsWithPaginationQuery request, CancellationToken cancellationToken) { var data = await _context.AuditTrails.OrderBy($"{request.OrderBy} {request.SortDirection}") - .ProjectToPaginatedDataAsync(request.Specification, request.PageNumber, - request.PageSize, AuditTrailMapper.ToDto, cancellationToken); + .ProjectToPaginatedDataAsync(request.Specification, request.PageNumber, + request.PageSize, _mapper.ConfigurationProvider, cancellationToken); return data; } diff --git a/src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs b/src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs index e6699365d..6a06d9c82 100644 --- a/src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs +++ b/src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs @@ -24,36 +24,48 @@ using CleanArchitecture.Blazor.Application.Features.Contacts.Caching; -using CleanArchitecture.Blazor.Application.Features.Contacts.Mappers; +using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs; namespace CleanArchitecture.Blazor.Application.Features.Contacts.Commands.AddEdit; -public class AddEditContactCommand: ICacheInvalidatorRequest> +public class AddEditContactCommand : ICacheInvalidatorRequest> { - [Description("Id")] - public int Id { get; set; } - [Description("Name")] - public string Name {get;set;} + [Description("Id")] + public int Id { get; set; } + [Description("Name")] + public string Name { get; set; } [Description("Description")] - public string? Description {get;set;} + public string? Description { get; set; } [Description("Email")] - public string? Email {get;set;} + public string? Email { get; set; } [Description("Phone number")] - public string? PhoneNumber {get;set;} + public string? PhoneNumber { get; set; } [Description("Country")] - public string? Country {get;set;} + public string? Country { get; set; } - public string CacheKey => ContactCacheKey.GetAllCacheKey; - public IEnumerable? Tags => ContactCacheKey.Tags; + public string CacheKey => ContactCacheKey.GetAllCacheKey; + public IEnumerable? Tags => ContactCacheKey.Tags; + + private class Mapping : Profile + { + public Mapping() + { + CreateMap(MemberList.None); + CreateMap(MemberList.None); + } + } } public class AddEditContactCommandHandler : IRequestHandler> { + private readonly IMapper _mapper; private readonly IApplicationDbContext _context; public AddEditContactCommandHandler( + IMapper mapper, IApplicationDbContext context) { + _mapper = mapper; _context = context; } public async Task> Handle(AddEditContactCommand request, CancellationToken cancellationToken) @@ -65,22 +77,22 @@ public async Task> Handle(AddEditContactCommand request, Cancellatio { return await Result.FailureAsync($"Contact with id: [{request.Id}] not found."); } - ContactMapper.ApplyChangesFrom(request,item); - // raise a update domain event - item.AddDomainEvent(new ContactUpdatedEvent(item)); + item = _mapper.Map(request, item); + // raise a update domain event + item.AddDomainEvent(new ContactUpdatedEvent(item)); await _context.SaveChangesAsync(cancellationToken); return await Result.SuccessAsync(item.Id); } else { - var item = ContactMapper.FromEditCommand(request); + var item = _mapper.Map(request); // raise a create domain event - item.AddDomainEvent(new ContactCreatedEvent(item)); + item.AddDomainEvent(new ContactCreatedEvent(item)); _context.Contacts.Add(item); await _context.SaveChangesAsync(cancellationToken); return await Result.SuccessAsync(item.Id); } - + } } diff --git a/src/Application/Features/Contacts/Commands/Create/CreateContactCommand.cs b/src/Application/Features/Contacts/Commands/Create/CreateContactCommand.cs index 848a7828c..a4dc1125c 100644 --- a/src/Application/Features/Contacts/Commands/Create/CreateContactCommand.cs +++ b/src/Application/Features/Contacts/Commands/Create/CreateContactCommand.cs @@ -26,45 +26,56 @@ // events for integration with other bounded contexts in the application. using CleanArchitecture.Blazor.Application.Features.Contacts.Caching; -using CleanArchitecture.Blazor.Application.Features.Contacts.Mappers; +using CleanArchitecture.Blazor.Application.Features.Contacts.Commands.AddEdit; +using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs; namespace CleanArchitecture.Blazor.Application.Features.Contacts.Commands.Create; -public class CreateContactCommand: ICacheInvalidatorRequest> +public class CreateContactCommand : ICacheInvalidatorRequest> { - [Description("Id")] - public int Id { get; set; } - [Description("Name")] - public string Name {get;set;} + [Description("Id")] + public int Id { get; set; } + [Description("Name")] + public string Name { get; set; } [Description("Description")] - public string? Description {get;set;} + public string? Description { get; set; } [Description("Email")] - public string? Email {get;set;} + public string? Email { get; set; } [Description("Phone number")] - public string? PhoneNumber {get;set;} + public string? PhoneNumber { get; set; } [Description("Country")] - public string? Country {get;set;} + public string? Country { get; set; } - public string CacheKey => ContactCacheKey.GetAllCacheKey; - public IEnumerable? Tags => ContactCacheKey.Tags; -} - - public class CreateContactCommandHandler : IRequestHandler> + public string CacheKey => ContactCacheKey.GetAllCacheKey; + public IEnumerable? Tags => ContactCacheKey.Tags; + private class Mapping : Profile { - private readonly IApplicationDbContext _context; - public CreateContactCommandHandler( - IApplicationDbContext context) - { - _context = context; - } - public async Task> Handle(CreateContactCommand request, CancellationToken cancellationToken) + public Mapping() { - var item = ContactMapper.FromCreateCommand(request); - // raise a create domain event - item.AddDomainEvent(new ContactCreatedEvent(item)); - _context.Contacts.Add(item); - await _context.SaveChangesAsync(cancellationToken); - return await Result.SuccessAsync(item.Id); + CreateMap(MemberList.None); } } +} + +public class CreateContactCommandHandler : IRequestHandler> +{ + private readonly IMapper _mapper; + private readonly IApplicationDbContext _context; + public CreateContactCommandHandler( + IMapper mapper, + IApplicationDbContext context) + { + _mapper = mapper; + _context = context; + } + public async Task> Handle(CreateContactCommand request, CancellationToken cancellationToken) + { + var item = _mapper.Map(request); + // raise a create domain event + item.AddDomainEvent(new ContactCreatedEvent(item)); + _context.Contacts.Add(item); + await _context.SaveChangesAsync(cancellationToken); + return await Result.SuccessAsync(item.Id); + } +} diff --git a/src/Application/Features/Contacts/Commands/Import/ImportContactsCommand.cs b/src/Application/Features/Contacts/Commands/Import/ImportContactsCommand.cs index 468e53327..277bb15c5 100644 --- a/src/Application/Features/Contacts/Commands/Import/ImportContactsCommand.cs +++ b/src/Application/Features/Contacts/Commands/Import/ImportContactsCommand.cs @@ -28,7 +28,6 @@ using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs; using CleanArchitecture.Blazor.Application.Features.Contacts.Caching; -using CleanArchitecture.Blazor.Application.Features.Contacts.Mappers; namespace CleanArchitecture.Blazor.Application.Features.Contacts.Commands.Import; @@ -56,17 +55,20 @@ public class ImportContactsCommandHandler : private readonly IApplicationDbContext _context; private readonly IStringLocalizer _localizer; private readonly IExcelService _excelService; - private readonly ContactDto _dto = new(); + private readonly IMapper _mapper; + private readonly ContactDto _dto = new(); public ImportContactsCommandHandler( IApplicationDbContext context, IExcelService excelService, + IMapper mapper, IStringLocalizer localizer) { _context = context; _localizer = localizer; _excelService = excelService; - } + _mapper = mapper; + } #nullable disable warnings public async Task> Handle(ImportContactsCommand request, CancellationToken cancellationToken) { @@ -87,7 +89,7 @@ public async Task> Handle(ImportContactsCommand request, Cancellatio var exists = await _context.Contacts.AnyAsync(x => x.Name == dto.Name, cancellationToken); if (!exists) { - var item = ContactMapper.FromDto(dto); + var item = _mapper.Map(dto); // add create domain events if this entity implement the IHasDomainEvent interface // item.AddDomainEvent(new ContactCreatedEvent(item)); await _context.Contacts.AddAsync(item, cancellationToken); diff --git a/src/Application/Features/Contacts/Commands/Update/UpdateContactCommand.cs b/src/Application/Features/Contacts/Commands/Update/UpdateContactCommand.cs index 17492f55c..9cc1e3002 100644 --- a/src/Application/Features/Contacts/Commands/Update/UpdateContactCommand.cs +++ b/src/Application/Features/Contacts/Commands/Update/UpdateContactCommand.cs @@ -26,7 +26,7 @@ // invalidated to keep the contact list consistent. using CleanArchitecture.Blazor.Application.Features.Contacts.Caching; -using CleanArchitecture.Blazor.Application.Features.Contacts.Mappers; +using CleanArchitecture.Blazor.Application.Features.Contacts.Commands.Create; namespace CleanArchitecture.Blazor.Application.Features.Contacts.Commands.Update; @@ -47,15 +47,24 @@ public class UpdateContactCommand: ICacheInvalidatorRequest> public string CacheKey => ContactCacheKey.GetAllCacheKey; public IEnumerable? Tags => ContactCacheKey.Tags; - + private class Mapping : Profile + { + public Mapping() + { + CreateMap(MemberList.None); + } + } } public class UpdateContactCommandHandler : IRequestHandler> { + private readonly Mapper _mapper; private readonly IApplicationDbContext _context; public UpdateContactCommandHandler( + Mapper mapper, IApplicationDbContext context) { + _mapper = mapper; _context = context; } public async Task> Handle(UpdateContactCommand request, CancellationToken cancellationToken) @@ -66,9 +75,9 @@ public async Task> Handle(UpdateContactCommand request, Cancellation { return await Result.FailureAsync($"Contact with id: [{request.Id}] not found."); } - ContactMapper.ApplyChangesFrom(request, item); - // raise a update domain event - item.AddDomainEvent(new ContactUpdatedEvent(item)); + item = _mapper.Map(request, item); + // raise a update domain event + item.AddDomainEvent(new ContactUpdatedEvent(item)); await _context.SaveChangesAsync(cancellationToken); return await Result.SuccessAsync(item.Id); } diff --git a/src/Application/Features/Contacts/DTOs/ContactDto.cs b/src/Application/Features/Contacts/DTOs/ContactDto.cs index 0acfc47e7..4ce4ec3d5 100644 --- a/src/Application/Features/Contacts/DTOs/ContactDto.cs +++ b/src/Application/Features/Contacts/DTOs/ContactDto.cs @@ -33,17 +33,29 @@ public class ContactDto { [Description("Id")] public int Id { get; set; } - [Description("Name")] - public string Name {get;set;} + [Description("Name")] + public string Name { get; set; } [Description("Description")] - public string? Description {get;set;} + public string? Description { get; set; } [Description("Email")] - public string? Email {get;set;} + public string? Email { get; set; } [Description("Phone number")] - public string? PhoneNumber {get;set;} + public string? PhoneNumber { get; set; } [Description("Country")] - public string? Country {get;set;} - + public string? Country { get; set; } + private class Mapping : Profile + { + public Mapping() + { + CreateMap(); + CreateMap() + .ForMember(dest => dest.Created, opt => opt.Ignore()) + .ForMember(dest => dest.CreatedBy, opt => opt.Ignore()) + .ForMember(dest => dest.LastModified, opt => opt.Ignore()) + .ForMember(dest => dest.LastModifiedBy, opt => opt.Ignore()) + .ForMember(dest => dest.DomainEvents, opt => opt.Ignore()); + } + } } diff --git a/src/Application/Features/Contacts/Mappers/ContactMapper.cs b/src/Application/Features/Contacts/Mappers/ContactMapper.cs deleted file mode 100644 index 894d34d24..000000000 --- a/src/Application/Features/Contacts/Mappers/ContactMapper.cs +++ /dev/null @@ -1,41 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This file is part of the CleanArchitecture.Blazor project. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -// -// Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 -// Description: -// Defines mapping methods between `Contact` entities and related DTOs/commands -// within the CleanArchitecture.Blazor application. This mapper facilitates -// conversions to support different operations, such as creating, updating, -// and projecting contact data. -// -//------------------------------------------------------------------------------ - -using CleanArchitecture.Blazor.Application.Features.Contacts.Commands.AddEdit; -using CleanArchitecture.Blazor.Application.Features.Contacts.Commands.Create; -using CleanArchitecture.Blazor.Application.Features.Contacts.Commands.Update; -using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs; - -namespace CleanArchitecture.Blazor.Application.Features.Contacts.Mappers; - -#pragma warning disable RMG020 -#pragma warning disable RMG012 -[Mapper] -public static partial class ContactMapper -{ - public static partial ContactDto ToDto(Contact source); - public static partial Contact FromDto(ContactDto dto); - public static partial Contact FromEditCommand(AddEditContactCommand command); - public static partial Contact FromCreateCommand(CreateContactCommand command); - public static partial UpdateContactCommand ToUpdateCommand(ContactDto dto); - public static partial AddEditContactCommand CloneFromDto(ContactDto dto); - public static partial void ApplyChangesFrom(UpdateContactCommand source, Contact target); - public static partial void ApplyChangesFrom(AddEditContactCommand source, Contact target); - public static partial IQueryable ProjectTo(this IQueryable q); -} - diff --git a/src/Application/Features/Contacts/Queries/Export/ExportContactsQuery.cs b/src/Application/Features/Contacts/Queries/Export/ExportContactsQuery.cs index 399e6be87..828ba0372 100644 --- a/src/Application/Features/Contacts/Queries/Export/ExportContactsQuery.cs +++ b/src/Application/Features/Contacts/Queries/Export/ExportContactsQuery.cs @@ -14,9 +14,9 @@ // //------------------------------------------------------------------------------ +using AutoMapper.QueryableExtensions; using CleanArchitecture.Blazor.Application.Features.Contacts.Caching; using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs; -using CleanArchitecture.Blazor.Application.Features.Contacts.Mappers; using CleanArchitecture.Blazor.Application.Features.Contacts.Specifications; namespace CleanArchitecture.Blazor.Application.Features.Contacts.Queries.Export; @@ -35,17 +35,20 @@ public override string ToString() public class ExportContactsQueryHandler : IRequestHandler> { - private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + private readonly IApplicationDbContext _context; private readonly IExcelService _excelService; private readonly IStringLocalizer _localizer; private readonly ContactDto _dto = new(); public ExportContactsQueryHandler( + IMapper mapper, IApplicationDbContext context, IExcelService excelService, IStringLocalizer localizer ) { - _context = context; + _mapper = mapper; + _context = context; _excelService = excelService; _localizer = localizer; } @@ -54,7 +57,7 @@ public async Task> Handle(ExportContactsQuery request, Cancellati { var data = await _context.Contacts.ApplySpecification(request.Specification) .OrderBy($"{request.OrderBy} {request.SortDirection}") - .ProjectTo() + .ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking() .ToListAsync(cancellationToken); var result = await _excelService.ExportAsync(data, diff --git a/src/Application/Features/Contacts/Queries/GetAll/GetAllContactsQuery.cs b/src/Application/Features/Contacts/Queries/GetAll/GetAllContactsQuery.cs index a2b393188..0158e9e7b 100644 --- a/src/Application/Features/Contacts/Queries/GetAll/GetAllContactsQuery.cs +++ b/src/Application/Features/Contacts/Queries/GetAll/GetAllContactsQuery.cs @@ -15,7 +15,6 @@ //------------------------------------------------------------------------------ using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs; -using CleanArchitecture.Blazor.Application.Features.Contacts.Mappers; using CleanArchitecture.Blazor.Application.Features.Contacts.Caching; namespace CleanArchitecture.Blazor.Application.Features.Contacts.Queries.GetAll; @@ -29,17 +28,20 @@ public class GetAllContactsQuery : ICacheableRequest> public class GetAllContactsQueryHandler : IRequestHandler> { + private readonly IMapper _mapper; private readonly IApplicationDbContext _context; public GetAllContactsQueryHandler( + IMapper mapper, IApplicationDbContext context) { + _mapper = mapper; _context = context; } public async Task> Handle(GetAllContactsQuery request, CancellationToken cancellationToken) { - var data = await _context.Contacts.ProjectTo() + var data = await _context.Contacts.ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking() .ToListAsync(cancellationToken); return data; diff --git a/src/Application/Features/Contacts/Queries/GetById/GetContactByIdQuery.cs b/src/Application/Features/Contacts/Queries/GetById/GetContactByIdQuery.cs index f482a939e..c316591c8 100644 --- a/src/Application/Features/Contacts/Queries/GetById/GetContactByIdQuery.cs +++ b/src/Application/Features/Contacts/Queries/GetById/GetContactByIdQuery.cs @@ -13,9 +13,9 @@ // //------------------------------------------------------------------------------ -using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs; +using AutoMapper.QueryableExtensions; using CleanArchitecture.Blazor.Application.Features.Contacts.Caching; -using CleanArchitecture.Blazor.Application.Features.Contacts.Mappers; +using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs; using CleanArchitecture.Blazor.Application.Features.Contacts.Specifications; namespace CleanArchitecture.Blazor.Application.Features.Contacts.Queries.GetById; @@ -30,19 +30,23 @@ public class GetContactByIdQuery : ICacheableRequest> public class GetContactByIdQueryHandler : IRequestHandler> { + private readonly IMapper _mapper; private readonly IApplicationDbContext _context; public GetContactByIdQueryHandler( + IMapper mapper, IApplicationDbContext context) { + _mapper = mapper; _context = context; } public async Task> Handle(GetContactByIdQuery request, CancellationToken cancellationToken) { var data = await _context.Contacts.ApplySpecification(new ContactByIdSpecification(request.Id)) - .ProjectTo() - .FirstAsync(cancellationToken); + .ProjectTo(_mapper.ConfigurationProvider) + .FirstAsync(cancellationToken) ?? + throw new NotFoundException($"Contact with id: [{request.Id}] not found."); ; return await Result.SuccessAsync(data); } } diff --git a/src/Application/Features/Contacts/Queries/Pagination/ContactsPaginationQuery.cs b/src/Application/Features/Contacts/Queries/Pagination/ContactsPaginationQuery.cs index a06611554..c8f41efc0 100644 --- a/src/Application/Features/Contacts/Queries/Pagination/ContactsPaginationQuery.cs +++ b/src/Application/Features/Contacts/Queries/Pagination/ContactsPaginationQuery.cs @@ -15,7 +15,6 @@ using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs; using CleanArchitecture.Blazor.Application.Features.Contacts.Caching; -using CleanArchitecture.Blazor.Application.Features.Contacts.Mappers; using CleanArchitecture.Blazor.Application.Features.Contacts.Specifications; namespace CleanArchitecture.Blazor.Application.Features.Contacts.Queries.Pagination; @@ -30,26 +29,29 @@ public override string ToString() public IEnumerable? Tags => ContactCacheKey.Tags; public ContactAdvancedSpecification Specification => new ContactAdvancedSpecification(this); } - + public class ContactsWithPaginationQueryHandler : IRequestHandler> { - private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + private readonly IApplicationDbContext _context; - public ContactsWithPaginationQueryHandler( - IApplicationDbContext context) - { - _context = context; - } + public ContactsWithPaginationQueryHandler( + IMapper mapper, + IApplicationDbContext context) + { + _mapper = mapper; + _context = context; + } - public async Task> Handle(ContactsWithPaginationQuery request, CancellationToken cancellationToken) - { - var data = await _context.Contacts.OrderBy($"{request.OrderBy} {request.SortDirection}") - .ProjectToPaginatedDataAsync(request.Specification, - request.PageNumber, - request.PageSize, - ContactMapper.ToDto, - cancellationToken); - return data; - } + public async Task> Handle(ContactsWithPaginationQuery request, CancellationToken cancellationToken) + { + var data = await _context.Contacts.OrderBy($"{request.OrderBy} {request.SortDirection}") + .ProjectToPaginatedDataAsync(request.Specification, + request.PageNumber, + request.PageSize, + _mapper.ConfigurationProvider, + cancellationToken); + return data; + } } \ No newline at end of file diff --git a/src/Application/Features/Documents/Commands/AddEdit/AddEditDocumentCommand.cs b/src/Application/Features/Documents/Commands/AddEdit/AddEditDocumentCommand.cs index 365584afb..46f910df8 100644 --- a/src/Application/Features/Documents/Commands/AddEdit/AddEditDocumentCommand.cs +++ b/src/Application/Features/Documents/Commands/AddEdit/AddEditDocumentCommand.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.Documents.Caching; -using CleanArchitecture.Blazor.Application.Features.Documents.Mappers; +using CleanArchitecture.Blazor.Application.Features.Documents.DTOs; namespace CleanArchitecture.Blazor.Application.Features.Documents.Commands.AddEdit; @@ -20,19 +20,29 @@ public class AddEditDocumentCommand : ICacheInvalidatorRequest> [Description("Content")] public string? Content { get; set; } public UploadRequest? UploadRequest { get; set; } public IEnumerable? Tags => DocumentCacheKey.Tags; - + private class Mapping : Profile + { + public Mapping() + { + CreateMap(MemberList.None); + CreateMap(MemberList.None); + } + } } public class AddEditDocumentCommandHandler : IRequestHandler> { + private readonly IMapper _mapper; private readonly IApplicationDbContext _context; private readonly IUploadService _uploadService; public AddEditDocumentCommandHandler( + IMapper mapper, IApplicationDbContext context, IUploadService uploadService ) { + _mapper = mapper; _context = context; _uploadService = uploadService; } @@ -57,7 +67,7 @@ public async Task> Handle(AddEditDocumentCommand request, Cancellati } else { - var document = DocumentMapper.FromEditCommand(request); + var document = _mapper.Map(request); if (request.UploadRequest != null) document.URL = await _uploadService.UploadAsync(request.UploadRequest); document.AddDomainEvent(new CreatedEvent(document)); _context.Documents.Add(document); diff --git a/src/Application/Features/Documents/DTOs/DocumentDto.cs b/src/Application/Features/Documents/DTOs/DocumentDto.cs index 142841274..fc948ef7f 100644 --- a/src/Application/Features/Documents/DTOs/DocumentDto.cs +++ b/src/Application/Features/Documents/DTOs/DocumentDto.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs; using CleanArchitecture.Blazor.Application.Features.Identity.DTOs; namespace CleanArchitecture.Blazor.Application.Features.Documents.DTOs; @@ -19,5 +20,16 @@ public class DocumentDto [Description("Status")] public JobStatus Status { get; set; } = JobStatus.NotStart; [Description("Content")] public string? Content { get; set; } [Description("Created By User")] public ApplicationUserDto? CreatedByUser { get; set; } - + private class Mapping : Profile + { + public Mapping() + { + CreateMap(MemberList.None) + .ForMember(x => x.TenantName, s => s.MapFrom(y => y.Tenant!.Name)); + CreateMap(MemberList.None) + .ForMember(x => x.Tenant, s => s.Ignore()) + .ForMember(x => x.CreatedByUser, s => s.Ignore()) + .ForMember(x => x.LastModifiedByUser, s => s.Ignore()); + } + } } \ No newline at end of file diff --git a/src/Application/Features/Documents/Mappers/DocumentMapper.cs b/src/Application/Features/Documents/Mappers/DocumentMapper.cs deleted file mode 100644 index 8fc8ea6e8..000000000 --- a/src/Application/Features/Documents/Mappers/DocumentMapper.cs +++ /dev/null @@ -1,25 +0,0 @@ -using CleanArchitecture.Blazor.Application.Features.Documents.Commands.AddEdit; -using CleanArchitecture.Blazor.Application.Features.Documents.DTOs; -using CleanArchitecture.Blazor.Application.Features.Identity.Mappers; - -namespace CleanArchitecture.Blazor.Application.Features.Documents.Mappers; -#pragma warning disable RMG020 -#pragma warning disable RMG012 -[Mapper] -[UseStaticMapper(typeof(ApplicationUserMapper))] -public static partial class DocumentMapper -{ - [MapProperty("Tenant.Name", "TenantName")] - public static partial DocumentDto ToDto(Document document); - - [MapperIgnoreSource(nameof(DocumentDto.CreatedByUser))] - public static partial Document FromDto(DocumentDto dto); - [MapperIgnoreSource(nameof(DocumentDto.CreatedByUser))] - public static partial AddEditDocumentCommand ToEditCommand(DocumentDto dto); - public static partial Document FromEditCommand(AddEditDocumentCommand command); - public static partial void ApplyChangesFrom(AddEditDocumentCommand command, Document document); - - public static partial IQueryable ProjectTo(this IQueryable q); - - -} diff --git a/src/Application/Features/Documents/Queries/PaginationQuery/DocumentsWithPaginationQuery.cs b/src/Application/Features/Documents/Queries/PaginationQuery/DocumentsWithPaginationQuery.cs index 22aaf828d..ac0481616 100644 --- a/src/Application/Features/Documents/Queries/PaginationQuery/DocumentsWithPaginationQuery.cs +++ b/src/Application/Features/Documents/Queries/PaginationQuery/DocumentsWithPaginationQuery.cs @@ -1,9 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. + using CleanArchitecture.Blazor.Application.Features.Documents.Caching; using CleanArchitecture.Blazor.Application.Features.Documents.DTOs; -using CleanArchitecture.Blazor.Application.Features.Documents.Mappers; using CleanArchitecture.Blazor.Application.Features.Documents.Specifications; namespace CleanArchitecture.Blazor.Application.Features.Documents.Queries.PaginationQuery; @@ -24,12 +24,15 @@ public override string ToString() public class DocumentsQueryHandler : IRequestHandler> { + private readonly IMapper _mapper; private readonly IApplicationDbContext _context; public DocumentsQueryHandler( + IMapper mapper, IApplicationDbContext context ) { + _mapper = mapper; _context = context; } @@ -37,8 +40,7 @@ public async Task> Handle(DocumentsWithPaginationQuer CancellationToken cancellationToken) { var data = await _context.Documents.OrderBy($"{request.OrderBy} {request.SortDirection}") - .ProjectToPaginatedDataAsync(request.Specification, request.PageNumber, - request.PageSize,DocumentMapper.ToDto, cancellationToken); + .ProjectToPaginatedDataAsync(request.Specification, request.PageNumber, request.PageSize, _mapper.ConfigurationProvider, cancellationToken); return data; } diff --git a/src/Application/Features/Identity/DTOs/ApplicationRoleDto.cs b/src/Application/Features/Identity/DTOs/ApplicationRoleDto.cs index 07db9c44c..b00f69558 100644 --- a/src/Application/Features/Identity/DTOs/ApplicationRoleDto.cs +++ b/src/Application/Features/Identity/DTOs/ApplicationRoleDto.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using CleanArchitecture.Blazor.Domain.Identity; + namespace CleanArchitecture.Blazor.Application.Features.Identity.DTOs; [Description("Roles")] @@ -12,5 +14,12 @@ public class ApplicationRoleDto [Description("Tenant Name")] public string? TenantName { get; set; } [Description("Normalized Name")] public string? NormalizedName { get; set; } [Description("Description")] public string? Description { get; set; } - + private class Mapping : Profile + { + public Mapping() + { + CreateMap(MemberList.None) + .ForMember(x => x.TenantName, s => s.MapFrom(y => y.Tenant!.Name)); + } + } } \ No newline at end of file diff --git a/src/Application/Features/Identity/DTOs/ApplicationUserDto.cs b/src/Application/Features/Identity/DTOs/ApplicationUserDto.cs index e4a8a5c1b..abeed6833 100644 --- a/src/Application/Features/Identity/DTOs/ApplicationUserDto.cs +++ b/src/Application/Features/Identity/DTOs/ApplicationUserDto.cs @@ -1,4 +1,5 @@ using CleanArchitecture.Blazor.Application.Features.Tenants.DTOs; +using CleanArchitecture.Blazor.Domain.Identity; namespace CleanArchitecture.Blazor.Application.Features.Identity.DTOs; @@ -88,7 +89,61 @@ public bool IsInRole(string role) return AssignedRoles?.Contains(role) ?? false; } - +#pragma warning disable CS8619 +#pragma warning disable CS8601 + private class Mapping : Profile + { + public Mapping() + { + CreateMap(MemberList.None) + .ForMember(x => x.LocalTimeOffset, s => s.Ignore()) + .ForMember(x => x.EmailConfirmed, s => s.MapFrom(y => y.EmailConfirmed)) + .ForMember(x => x.AssignedRoles, s => s.MapFrom(y => y.UserRoles.Select(r => r.Role.Name))) + .ForMember(x => x.Superior, s => s.MapFrom(y => y.Superior != null ? new ApplicationUserDto() + { + Id = y.Superior.Id, + UserName = y.Superior.UserName, + DisplayName = y.Superior.DisplayName, + Email = y.Superior.Email, + PhoneNumber = y.Superior.PhoneNumber, + ProfilePictureDataUrl = y.Superior.ProfilePictureDataUrl, + IsActive = y.Superior.IsActive, + TenantId = y.Superior.TenantId, + AssignedRoles = y.Superior.UserRoles.Select(r => r.Role.Name).ToArray(), + TimeZoneId = y.Superior.TimeZoneId, + LanguageCode = y.Superior.LanguageCode + } : null)) + .ForMember(x => x.CreatedByUser, s => s.MapFrom(y => y.CreatedByUser != null ? new ApplicationUserDto() + { + Id = y.CreatedByUser.Id, + UserName = y.CreatedByUser.UserName, + DisplayName = y.CreatedByUser.DisplayName, + Email = y.CreatedByUser.Email, + PhoneNumber = y.CreatedByUser.PhoneNumber, + ProfilePictureDataUrl = y.CreatedByUser.ProfilePictureDataUrl, + IsActive = y.CreatedByUser.IsActive, + TenantId = y.CreatedByUser.TenantId, + AssignedRoles = y.CreatedByUser.UserRoles.Select(r => r.Role.Name).ToArray(), + TimeZoneId = y.CreatedByUser.TimeZoneId, + LanguageCode = y.CreatedByUser.LanguageCode + } : null)) + .ForMember(x => x.LastModifiedByUser, s => s.MapFrom(y => y.LastModifiedByUser != null ? new ApplicationUserDto() + { + Id = y.LastModifiedByUser.Id, + UserName = y.LastModifiedByUser.UserName, + DisplayName = y.LastModifiedByUser.DisplayName, + Email = y.LastModifiedByUser.Email, + PhoneNumber = y.LastModifiedByUser.PhoneNumber, + ProfilePictureDataUrl = y.LastModifiedByUser.ProfilePictureDataUrl, + IsActive = y.LastModifiedByUser.IsActive, + TenantId = y.LastModifiedByUser.TenantId, + AssignedRoles = y.LastModifiedByUser.UserRoles.Select(r => r.Role.Name).ToArray(), + TimeZoneId = y.LastModifiedByUser.TimeZoneId, + LanguageCode = y.LastModifiedByUser.LanguageCode + } : null)); + + } + } } public class ApplicationUserDtoValidator : AbstractValidator diff --git a/src/Application/Features/Identity/Mappers/ApplicationRoleMapper.cs b/src/Application/Features/Identity/Mappers/ApplicationRoleMapper.cs deleted file mode 100644 index d1c4bf792..000000000 --- a/src/Application/Features/Identity/Mappers/ApplicationRoleMapper.cs +++ /dev/null @@ -1,14 +0,0 @@ -using CleanArchitecture.Blazor.Application.Features.Identity.DTOs; -using CleanArchitecture.Blazor.Domain.Identity; - -namespace CleanArchitecture.Blazor.Application.Features.Identity.Mappers; -#pragma warning disable RMG020 -#pragma warning disable RMG012 -[Mapper] -public static partial class ApplicationRoleMapper -{ - [MapProperty("Tenant.Name", "TenantName")] - public static partial ApplicationRoleDto ToApplicationRoleDto(ApplicationRole role); - public static partial IQueryable ProjectTo(this IQueryable q); - -} diff --git a/src/Application/Features/Identity/Mappers/ApplicationUserMapper.cs b/src/Application/Features/Identity/Mappers/ApplicationUserMapper.cs deleted file mode 100644 index ccd6c6501..000000000 --- a/src/Application/Features/Identity/Mappers/ApplicationUserMapper.cs +++ /dev/null @@ -1,58 +0,0 @@ -using CleanArchitecture.Blazor.Application.Features.Identity.DTOs; -using CleanArchitecture.Blazor.Application.Features.Tenants.Mappers; -using CleanArchitecture.Blazor.Domain.Identity; - -namespace CleanArchitecture.Blazor.Application.Features.Identity.Mappers; -#pragma warning disable RMG020 -#pragma warning disable RMG012 -[Mapper] -[UseStaticMapper(typeof(TenantMapper))] -public static partial class ApplicationUserMapper -{ - [MapPropertyFromSource(nameof(ApplicationUser.CreatedByUser), Use = nameof(MapWithoutRelatedProperties))] - [MapPropertyFromSource(nameof(ApplicationUser.LastModifiedByUser), Use = nameof(MapWithoutRelatedProperties))] - [MapPropertyFromSource(nameof(ApplicationUser.Superior), Use = nameof(MapWithoutRelatedProperties))] - [MapProperty(nameof(ApplicationUser.UserRoles), nameof(ApplicationUserDto.AssignedRoles), Use = nameof(MapAssignedRoles))] - public static partial ApplicationUserDto ToApplicationUserDto(ApplicationUser user); - - [MapperIgnoreSource(nameof(ApplicationUser.CreatedByUser))] - [MapperIgnoreSource(nameof(ApplicationUser.LastModifiedByUser))] - [MapperIgnoreSource(nameof(ApplicationUser.Superior))] - [MapperIgnoreSource(nameof(ApplicationUser.Tenant))] - private static partial ApplicationUserDto MapWithoutRelatedProperties(ApplicationUser user); - public static IQueryable ProjectTo(this IQueryable q) - { - return q.Select(x => new ApplicationUserDto - { - Id = x.Id, - UserName = x.UserName ?? "", - DisplayName = x.DisplayName, - Provider = x.Provider, - TenantId = x.TenantId, - Tenant = x.Tenant != null ? TenantMapper.ToDto(x.Tenant) : default, - ProfilePictureDataUrl = x.ProfilePictureDataUrl, - Email = x.Email ?? "", - PhoneNumber = x.PhoneNumber, - Superior = x.Superior != null ? MapWithoutRelatedProperties(x.Superior) : null, - CreatedByUser = x.CreatedByUser != null ? MapWithoutRelatedProperties(x.CreatedByUser) : null, - LastModifiedByUser = x.LastModifiedByUser != null ? MapWithoutRelatedProperties(x.LastModifiedByUser) : null, - AssignedRoles = x.UserRoles.Select(r => r.Role.Name!).ToArray(), - IsActive = x.IsActive, - IsLive = x.IsLive, - EmailConfirmed = x.EmailConfirmed, - LockoutEnd = x.LockoutEnd, - TimeZoneId = x.TimeZoneId, - LanguageCode = x.LanguageCode, - LastModified = x.LastModified, - LastModifiedBy = x.LastModifiedBy, - Created = x.Created, - CreatedBy = x.CreatedBy - }); - } - - - private static string[] MapAssignedRoles(ICollection roles) - { - return roles.Select(r => r.Role.Name!).ToArray(); - } -} diff --git a/src/Application/Features/PicklistSets/Commands/AddEdit/AddEditPicklistSetCommand.cs b/src/Application/Features/PicklistSets/Commands/AddEdit/AddEditPicklistSetCommand.cs index c735e5939..4b68a0f6a 100644 --- a/src/Application/Features/PicklistSets/Commands/AddEdit/AddEditPicklistSetCommand.cs +++ b/src/Application/Features/PicklistSets/Commands/AddEdit/AddEditPicklistSetCommand.cs @@ -2,8 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.PicklistSets.Caching; -using CleanArchitecture.Blazor.Application.Features.PicklistSets.Mappers; - +using CleanArchitecture.Blazor.Application.Features.PicklistSets.DTOs; namespace CleanArchitecture.Blazor.Application.Features.PicklistSets.Commands.AddEdit; public class AddEditPicklistSetCommand : ICacheInvalidatorRequest> @@ -16,15 +15,26 @@ public class AddEditPicklistSetCommand : ICacheInvalidatorRequest> public TrackingState TrackingState { get; set; } = TrackingState.Unchanged; public string CacheKey => PicklistSetCacheKey.GetAllCacheKey; public IEnumerable? Tags => PicklistSetCacheKey.Tags; + private class Mapping : Profile + { + public Mapping() + { + CreateMap(MemberList.None); + CreateMap(MemberList.None); + } + } } public class AddEditPicklistSetCommandHandler : IRequestHandler> { + private readonly IMapper _mapper; private readonly IApplicationDbContext _context; public AddEditPicklistSetCommandHandler( + IMapper mapper, IApplicationDbContext context) { + _mapper = mapper; _context = context; } @@ -37,14 +47,14 @@ public async Task> Handle(AddEditPicklistSetCommand request, Cancell { return await Result.FailureAsync($"Picklist with id: [{request.Id}] not found."); } - PicklistMapper.ApplyChangesFrom(request, item); + item = _mapper.Map(request, item); item.AddDomainEvent(new UpdatedEvent(item)); await _context.SaveChangesAsync(cancellationToken); return await Result.SuccessAsync(item.Id); } else { - var keyValue = PicklistMapper.FromEditCommand(request); + var keyValue = _mapper.Map(request); keyValue.AddDomainEvent(new UpdatedEvent(keyValue)); _context.PicklistSets.Add(keyValue); await _context.SaveChangesAsync(cancellationToken); diff --git a/src/Application/Features/PicklistSets/DTOs/PicklistSetDto.cs b/src/Application/Features/PicklistSets/DTOs/PicklistSetDto.cs index a47322adc..aad22ca88 100644 --- a/src/Application/Features/PicklistSets/DTOs/PicklistSetDto.cs +++ b/src/Application/Features/PicklistSets/DTOs/PicklistSetDto.cs @@ -12,4 +12,12 @@ public class PicklistSetDto [Description("Text")] public string? Text { get; set; } [Description("Description")] public string? Description { get; set; } public TrackingState TrackingState { get; set; } = TrackingState.Unchanged; + + private class Mapping : Profile + { + public Mapping() + { + CreateMap(MemberList.None).ReverseMap(); + } + } } \ No newline at end of file diff --git a/src/Application/Features/PicklistSets/Mappers/PicklistMapper.cs b/src/Application/Features/PicklistSets/Mappers/PicklistMapper.cs deleted file mode 100644 index 603b22e8c..000000000 --- a/src/Application/Features/PicklistSets/Mappers/PicklistMapper.cs +++ /dev/null @@ -1,16 +0,0 @@ -using CleanArchitecture.Blazor.Application.Features.PicklistSets.Commands.AddEdit; -using CleanArchitecture.Blazor.Application.Features.PicklistSets.DTOs; - -namespace CleanArchitecture.Blazor.Application.Features.PicklistSets.Mappers; -#pragma warning disable RMG020 -#pragma warning disable RMG012 -[Mapper] -public static partial class PicklistMapper -{ - public static partial PicklistSetDto ToDto(PicklistSet picklistSet); - public static partial PicklistSet FromDto(PicklistSetDto dto); - public static partial AddEditPicklistSetCommand ToEditCommand(PicklistSetDto dto); - public static partial PicklistSet FromEditCommand(AddEditPicklistSetCommand command); - public static partial void ApplyChangesFrom(AddEditPicklistSetCommand command, PicklistSet contact); - public static partial IQueryable ProjectTo(this IQueryable q); -} diff --git a/src/Application/Features/PicklistSets/Queries/ByName/PicklistSetsQueryByName.cs b/src/Application/Features/PicklistSets/Queries/ByName/PicklistSetsQueryByName.cs index e6ba3024c..0842ccea3 100644 --- a/src/Application/Features/PicklistSets/Queries/ByName/PicklistSetsQueryByName.cs +++ b/src/Application/Features/PicklistSets/Queries/ByName/PicklistSetsQueryByName.cs @@ -1,9 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using AutoMapper.QueryableExtensions; using CleanArchitecture.Blazor.Application.Features.PicklistSets.Caching; using CleanArchitecture.Blazor.Application.Features.PicklistSets.DTOs; -using CleanArchitecture.Blazor.Application.Features.PicklistSets.Mappers; + namespace CleanArchitecture.Blazor.Application.Features.PicklistSets.Queries.ByName; @@ -20,10 +21,13 @@ public PicklistSetsQueryByName(Picklist name) public class PicklistSetsQueryByNameHandler : IRequestHandler> { + private readonly IMapper _mapper; private readonly IApplicationDbContext _context; public PicklistSetsQueryByNameHandler( + IMapper mapper, IApplicationDbContext context) { + _mapper = mapper; _context = context; } @@ -32,7 +36,8 @@ public async Task> Handle(PicklistSetsQueryByName re { var data = await _context.PicklistSets.Where(x => x.Name == request.Name) .OrderBy(x => x.Text) - .ProjectTo() + .ProjectTo(_mapper.ConfigurationProvider) + .AsNoTracking() .ToListAsync(cancellationToken); return data; } diff --git a/src/Application/Features/PicklistSets/Queries/Export/ExportPicklistSetsQuery.cs b/src/Application/Features/PicklistSets/Queries/Export/ExportPicklistSetsQuery.cs index 29330c1ec..6cf433598 100644 --- a/src/Application/Features/PicklistSets/Queries/Export/ExportPicklistSetsQuery.cs +++ b/src/Application/Features/PicklistSets/Queries/Export/ExportPicklistSetsQuery.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using AutoMapper.QueryableExtensions; using CleanArchitecture.Blazor.Application.Features.PicklistSets.DTOs; -using CleanArchitecture.Blazor.Application.Features.PicklistSets.Mappers; namespace CleanArchitecture.Blazor.Application.Features.PicklistSets.Queries.Export; @@ -17,16 +17,19 @@ public class ExportPicklistSetsQueryHandler : IRequestHandler { private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; private readonly IExcelService _excelService; private readonly IStringLocalizer _localizer; public ExportPicklistSetsQueryHandler( IApplicationDbContext context, + IMapper mapper, IExcelService excelService, IStringLocalizer localizer ) { _context = context; + _mapper = mapper; _excelService = excelService; _localizer = localizer; } @@ -38,7 +41,7 @@ public async Task Handle(ExportPicklistSetsQuery request, CancellationTo x.Description.Contains(request.Keyword) || x.Value.Contains(request.Keyword) || x.Text.Contains(request.Keyword)) .OrderBy($"{request.OrderBy} {request.SortDirection}") - .ProjectTo() + .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(cancellationToken); var result = await _excelService.ExportAsync(data, new Dictionary> diff --git a/src/Application/Features/PicklistSets/Queries/GetAll/GetAllPicklistSetsQuery.cs b/src/Application/Features/PicklistSets/Queries/GetAll/GetAllPicklistSetsQuery.cs index c99b5ca9d..29fc74918 100644 --- a/src/Application/Features/PicklistSets/Queries/GetAll/GetAllPicklistSetsQuery.cs +++ b/src/Application/Features/PicklistSets/Queries/GetAll/GetAllPicklistSetsQuery.cs @@ -1,9 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using AutoMapper.QueryableExtensions; using CleanArchitecture.Blazor.Application.Features.PicklistSets.Caching; using CleanArchitecture.Blazor.Application.Features.PicklistSets.DTOs; -using CleanArchitecture.Blazor.Application.Features.PicklistSets.Mappers; + namespace CleanArchitecture.Blazor.Application.Features.PicklistSets.Queries.GetAll; @@ -15,11 +16,14 @@ public class GetAllPicklistSetsQuery : ICacheableRequest> { + private readonly IMapper _mapper; private readonly IApplicationDbContext _context; public GetAllPicklistSetsQueryHandler( + IMapper mapper, IApplicationDbContext context) { + _mapper = mapper; _context = context; } @@ -27,7 +31,7 @@ public async Task> Handle(GetAllPicklistSetsQuery re CancellationToken cancellationToken) { var data = await _context.PicklistSets.OrderBy(x => x.Name).ThenBy(x => x.Value) - .ProjectTo() + .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(cancellationToken); return data; } diff --git a/src/Application/Features/PicklistSets/Queries/PaginationQuery/PicklistSetsWithPaginationQuery.cs b/src/Application/Features/PicklistSets/Queries/PaginationQuery/PicklistSetsWithPaginationQuery.cs index c2ead2fe1..ba1fd4d05 100644 --- a/src/Application/Features/PicklistSets/Queries/PaginationQuery/PicklistSetsWithPaginationQuery.cs +++ b/src/Application/Features/PicklistSets/Queries/PaginationQuery/PicklistSetsWithPaginationQuery.cs @@ -3,7 +3,6 @@ using CleanArchitecture.Blazor.Application.Features.PicklistSets.Caching; using CleanArchitecture.Blazor.Application.Features.PicklistSets.DTOs; -using CleanArchitecture.Blazor.Application.Features.PicklistSets.Mappers; using CleanArchitecture.Blazor.Application.Features.PicklistSets.Specifications; namespace CleanArchitecture.Blazor.Application.Features.PicklistSets.Queries.PaginationQuery; @@ -22,11 +21,14 @@ public override string ToString() public class PicklistSetsQueryHandler : IRequestHandler> { + private readonly IMapper _mapper; private readonly IApplicationDbContext _context; public PicklistSetsQueryHandler( + IMapper mapper, IApplicationDbContext context) { + _mapper = mapper; _context = context; } @@ -34,8 +36,8 @@ public async Task> Handle(PicklistSetsWithPaginati CancellationToken cancellationToken) { var data = await _context.PicklistSets.OrderBy($"{request.OrderBy} {request.SortDirection}") - .ProjectToPaginatedDataAsync(request.Specification, request.PageNumber, - request.PageSize, PicklistMapper.ToDto, cancellationToken); + .ProjectToPaginatedDataAsync(request.Specification, request.PageNumber, + request.PageSize, _mapper.ConfigurationProvider, cancellationToken); return data; } diff --git a/src/Application/Features/Products/Commands/AddEdit/AddEditProductCommand.cs b/src/Application/Features/Products/Commands/AddEdit/AddEditProductCommand.cs index 0bef48a9e..6f7489d5e 100644 --- a/src/Application/Features/Products/Commands/AddEdit/AddEditProductCommand.cs +++ b/src/Application/Features/Products/Commands/AddEdit/AddEditProductCommand.cs @@ -3,7 +3,7 @@ using CleanArchitecture.Blazor.Application.Features.Products.Caching; -using CleanArchitecture.Blazor.Application.Features.Products.Mappers; +using CleanArchitecture.Blazor.Application.Features.Products.DTOs; using Microsoft.AspNetCore.Components.Forms; namespace CleanArchitecture.Blazor.Application.Features.Products.Commands.AddEdit; @@ -21,16 +21,27 @@ public class AddEditProductCommand : ICacheInvalidatorRequest> public IReadOnlyList? UploadPictures { get; set; } public string CacheKey => ProductCacheKey.GetAllCacheKey; public IEnumerable? Tags => ProductCacheKey.Tags; + private class Mapping : Profile + { + public Mapping() + { + CreateMap(MemberList.None); + CreateMap(MemberList.None); + } + } } public class AddEditProductCommandHandler : IRequestHandler> { + private readonly IMapper _mapper; private readonly IApplicationDbContext _context; public AddEditProductCommandHandler( + IMapper mapper, IApplicationDbContext context ) { + _mapper = mapper; _context = context; } @@ -43,14 +54,14 @@ public async Task> Handle(AddEditProductCommand request, Cancellatio { return await Result.FailureAsync($"Prduct with id: [{request.Id}] not found."); } - ProductMapper.ApplyChangesFrom(request, item); + item = _mapper.Map(request, item); item.AddDomainEvent(new UpdatedEvent(item)); await _context.SaveChangesAsync(cancellationToken); return await Result.SuccessAsync(item.Id); } else { - var item = ProductMapper.FromEditCommand(request); + var item = _mapper.Map(request); item.AddDomainEvent(new CreatedEvent(item)); _context.Products.Add(item); await _context.SaveChangesAsync(cancellationToken); diff --git a/src/Application/Features/Products/Commands/Import/ImportProductsCommand.cs b/src/Application/Features/Products/Commands/Import/ImportProductsCommand.cs index 09900cebe..061da4d08 100644 --- a/src/Application/Features/Products/Commands/Import/ImportProductsCommand.cs +++ b/src/Application/Features/Products/Commands/Import/ImportProductsCommand.cs @@ -1,11 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - -using CleanArchitecture.Blazor.Application.Common.Interfaces.Serialization; using CleanArchitecture.Blazor.Application.Features.Products.Caching; using CleanArchitecture.Blazor.Application.Features.Products.DTOs; -using CleanArchitecture.Blazor.Application.Features.Products.Mappers; + namespace CleanArchitecture.Blazor.Application.Features.Products.Commands.Import; @@ -33,17 +31,20 @@ public class ImportProductsCommandHandler : { private readonly IApplicationDbContext _context; private readonly IExcelService _excelService; + private readonly IMapper _mapper; private readonly IStringLocalizer _localizer; public ImportProductsCommandHandler( IApplicationDbContext context, - IExcelService excelService, + IExcelService excelService, + IMapper mapper, IStringLocalizer localizer ) { _context = context; _localizer = localizer; _excelService = excelService; + _mapper = mapper; } public async Task> Handle(CreateProductsTemplateCommand request, CancellationToken cancellationToken) @@ -88,7 +89,7 @@ public async Task> Handle(ImportProductsCommand request, Cancellatio { foreach (var dto in result.Data!) { - var item = ProductMapper.FromDto(dto); + var item = _mapper.Map(dto); await _context.Products.AddAsync(item, cancellationToken); } diff --git a/src/Application/Features/Products/DTOs/ProductDto.cs b/src/Application/Features/Products/DTOs/ProductDto.cs index a1279029f..7d8c238b7 100644 --- a/src/Application/Features/Products/DTOs/ProductDto.cs +++ b/src/Application/Features/Products/DTOs/ProductDto.cs @@ -19,4 +19,11 @@ public class ProductDto [Description("Price")] public decimal Price { get; set; } [Description("Pictures")] public List? Pictures { get; set; } + private class Mapping : Profile + { + public Mapping() + { + CreateMap().ReverseMap(); + } + } } \ No newline at end of file diff --git a/src/Application/Features/Products/Mappers/ProductMapper.cs b/src/Application/Features/Products/Mappers/ProductMapper.cs deleted file mode 100644 index 732c5be76..000000000 --- a/src/Application/Features/Products/Mappers/ProductMapper.cs +++ /dev/null @@ -1,18 +0,0 @@ -using CleanArchitecture.Blazor.Application.Features.Products.Commands.AddEdit; -using CleanArchitecture.Blazor.Application.Features.Products.DTOs; - -namespace CleanArchitecture.Blazor.Application.Features.Products.Mappers; -#pragma warning disable RMG020 -#pragma warning disable RMG012 -[Mapper] -public static partial class ProductMapper -{ - public static partial ProductDto ToDto(Product product); - public static partial Product FromDto(ProductDto dto); - public static partial Product FromEditCommand(AddEditProductCommand command); - public static partial void ApplyChangesFrom(AddEditProductCommand command, Product product); - [MapperIgnoreSource(nameof(ProductDto.Id))] - public static partial AddEditProductCommand CloneFromDto(ProductDto dto); - public static partial AddEditProductCommand ToEditCommand(ProductDto dto); - public static partial IQueryable ProjectTo(this IQueryable q); -} diff --git a/src/Application/Features/Products/Queries/Export/ExportProductsQuery.cs b/src/Application/Features/Products/Queries/Export/ExportProductsQuery.cs index 3d84cb396..511a854ce 100644 --- a/src/Application/Features/Products/Queries/Export/ExportProductsQuery.cs +++ b/src/Application/Features/Products/Queries/Export/ExportProductsQuery.cs @@ -2,9 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. -using CleanArchitecture.Blazor.Application.Common.Interfaces.Serialization; +using AutoMapper.QueryableExtensions; using CleanArchitecture.Blazor.Application.Features.Products.DTOs; -using CleanArchitecture.Blazor.Application.Features.Products.Mappers; using CleanArchitecture.Blazor.Application.Features.Products.Specifications; namespace CleanArchitecture.Blazor.Application.Features.Products.Queries.Export; @@ -20,18 +19,21 @@ public class ExportProductsQueryHandler : { private readonly IApplicationDbContext _context; private readonly IExcelService _excelService; + private readonly IMapper _mapper; private readonly IStringLocalizer _localizer; private readonly IPDFService _pdfService; public ExportProductsQueryHandler( IApplicationDbContext context, IExcelService excelService, + IMapper mapper, IPDFService pdfService, IStringLocalizer localizer ) { _context = context; _excelService = excelService; + _mapper = mapper; _pdfService = pdfService; _localizer = localizer; } @@ -40,7 +42,7 @@ public async Task> Handle(ExportProductsQuery request, Cancellati { var data = await _context.Products.ApplySpecification(request.Specification) .AsNoTracking() - .ProjectTo() + .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(cancellationToken); diff --git a/src/Application/Features/Products/Queries/GetAll/GetAllProductsQuery.cs b/src/Application/Features/Products/Queries/GetAll/GetAllProductsQuery.cs index 63d7e9810..6f35cc71c 100644 --- a/src/Application/Features/Products/Queries/GetAll/GetAllProductsQuery.cs +++ b/src/Application/Features/Products/Queries/GetAll/GetAllProductsQuery.cs @@ -1,9 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using AutoMapper.QueryableExtensions; using CleanArchitecture.Blazor.Application.Features.Products.Caching; using CleanArchitecture.Blazor.Application.Features.Products.DTOs; -using CleanArchitecture.Blazor.Application.Features.Products.Mappers; namespace CleanArchitecture.Blazor.Application.Features.Products.Queries.GetAll; @@ -26,19 +26,22 @@ public class GetAllProductsQueryHandler : IRequestHandler { + private readonly IMapper _mapper; private readonly IApplicationDbContext _context; public GetAllProductsQueryHandler( + IMapper mapper, IApplicationDbContext context ) { + _mapper = mapper; _context = context; } public async Task> Handle(GetAllProductsQuery request, CancellationToken cancellationToken) { var data = await _context.Products - .ProjectTo() + .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(cancellationToken); return data; } @@ -46,7 +49,7 @@ public async Task> Handle(GetAllProductsQuery request, C public async Task Handle(GetProductQuery request, CancellationToken cancellationToken) { var data = await _context.Products.Where(x => x.Id == request.Id) - .ProjectTo() + .ProjectTo(_mapper.ConfigurationProvider) .FirstOrDefaultAsync(cancellationToken); return data; } diff --git a/src/Application/Features/Products/Queries/Pagination/ProductsPaginationQuery.cs b/src/Application/Features/Products/Queries/Pagination/ProductsPaginationQuery.cs index 8cda3e670..07df75999 100644 --- a/src/Application/Features/Products/Queries/Pagination/ProductsPaginationQuery.cs +++ b/src/Application/Features/Products/Queries/Pagination/ProductsPaginationQuery.cs @@ -4,7 +4,6 @@ using CleanArchitecture.Blazor.Application.Features.Products.Caching; using CleanArchitecture.Blazor.Application.Features.Products.DTOs; -using CleanArchitecture.Blazor.Application.Features.Products.Mappers; using CleanArchitecture.Blazor.Application.Features.Products.Specifications; namespace CleanArchitecture.Blazor.Application.Features.Products.Queries.Pagination; @@ -27,11 +26,14 @@ public override string ToString() public class ProductsWithPaginationQueryHandler : IRequestHandler> { + private readonly IMapper _mapper; private readonly IApplicationDbContext _context; public ProductsWithPaginationQueryHandler( + IMapper mapper, IApplicationDbContext context ) { + _mapper = mapper; _context = context; } @@ -39,8 +41,8 @@ public async Task> Handle(ProductsWithPaginationQuery CancellationToken cancellationToken) { var data = await _context.Products.OrderBy($"{request.OrderBy} {request.SortDirection}") - .ProjectToPaginatedDataAsync(request.Specification, request.PageNumber, - request.PageSize, ProductMapper.ToDto, cancellationToken); + .ProjectToPaginatedDataAsync(request.Specification, request.PageNumber, + request.PageSize, _mapper.ConfigurationProvider, cancellationToken); return data; } } \ No newline at end of file diff --git a/src/Application/Features/SystemLogs/DTOs/SystemLogDto.cs b/src/Application/Features/SystemLogs/DTOs/SystemLogDto.cs index c1252e8c3..7a6ea644a 100644 --- a/src/Application/Features/SystemLogs/DTOs/SystemLogDto.cs +++ b/src/Application/Features/SystemLogs/DTOs/SystemLogDto.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Logging; + namespace CleanArchitecture.Blazor.Application.Features.SystemLogs.DTOs; public class SystemLogDto @@ -26,4 +28,12 @@ public class SystemLogDto [Description("Properties")] public string? Properties { get; set; } [Description("Log Event")] public string? LogEvent { get; set; } + + private class Mapping : Profile + { + public Mapping() + { + CreateMap().ReverseMap(); + } + } } \ No newline at end of file diff --git a/src/Application/Features/SystemLogs/Mappers/SystemLogMapper.cs b/src/Application/Features/SystemLogs/Mappers/SystemLogMapper.cs deleted file mode 100644 index b0ab4fd91..000000000 --- a/src/Application/Features/SystemLogs/Mappers/SystemLogMapper.cs +++ /dev/null @@ -1,9 +0,0 @@ -using CleanArchitecture.Blazor.Application.Features.SystemLogs.DTOs; - -namespace CleanArchitecture.Blazor.Application.Features.SystemLogs.Mappers; -[Mapper] -public static partial class SystemLogMapper -{ - public static partial SystemLogDto ToDto(SystemLog logger); - public static partial IQueryable ProjectTo(this IQueryable q); -} diff --git a/src/Application/Features/SystemLogs/Queries/Export/ExportSystemLogsQuery.cs b/src/Application/Features/SystemLogs/Queries/Export/ExportSystemLogsQuery.cs index 4f1e977f0..158d96552 100644 --- a/src/Application/Features/SystemLogs/Queries/Export/ExportSystemLogsQuery.cs +++ b/src/Application/Features/SystemLogs/Queries/Export/ExportSystemLogsQuery.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using AutoMapper.QueryableExtensions; using CleanArchitecture.Blazor.Application.Features.SystemLogs.DTOs; -using CleanArchitecture.Blazor.Application.Features.SystemLogs.Mappers; namespace CleanArchitecture.Blazor.Application.Features.Loggers.Queries.Export; @@ -15,16 +15,19 @@ public class ExportSystemLogsQuery : IRequest public class ExportSystemLogsQueryHandler : IRequestHandler { + private readonly IMapper _mapper; private readonly IApplicationDbContext _context; private readonly IExcelService _excelService; private readonly IStringLocalizer _localizer; public ExportSystemLogsQueryHandler( + IMapper mapper, IApplicationDbContext context, IExcelService excelService, IStringLocalizer localizer ) { + _mapper = mapper; _context = context; _excelService = excelService; _localizer = localizer; @@ -35,7 +38,7 @@ public async Task Handle(ExportSystemLogsQuery request, CancellationToke var data = await _context.SystemLogs .Where(x => x.Message!.Contains(request.Keyword) || x.Exception!.Contains(request.Keyword)) .OrderBy($"{request.OrderBy} {request.SortDirection}") - .ProjectTo() + .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(cancellationToken); var result = await _excelService.ExportAsync(data, new Dictionary> diff --git a/src/Application/Features/SystemLogs/Queries/PaginationQuery/SystemLogsWithPaginationQuery.cs b/src/Application/Features/SystemLogs/Queries/PaginationQuery/SystemLogsWithPaginationQuery.cs index 5ff54bec5..c6e61c7a8 100644 --- a/src/Application/Features/SystemLogs/Queries/PaginationQuery/SystemLogsWithPaginationQuery.cs +++ b/src/Application/Features/SystemLogs/Queries/PaginationQuery/SystemLogsWithPaginationQuery.cs @@ -3,8 +3,8 @@ using CleanArchitecture.Blazor.Application.Features.SystemLogs.Caching; using CleanArchitecture.Blazor.Application.Features.SystemLogs.DTOs; -using CleanArchitecture.Blazor.Application.Features.SystemLogs.Mappers; using CleanArchitecture.Blazor.Application.Features.SystemLogs.Specifications; +using Microsoft.Extensions.Logging; namespace CleanArchitecture.Blazor.Application.Features.SystemLogs.Queries.PaginationQuery; @@ -24,12 +24,15 @@ public override string ToString() public class LogsQueryHandler : IRequestHandler> { + private readonly IMapper _mapper; private readonly IApplicationDbContext _context; public LogsQueryHandler( + IMapper mapper, IApplicationDbContext context ) { + _mapper = mapper; _context = context; } @@ -37,8 +40,8 @@ public async Task> Handle(SystemLogsWithPaginationQu CancellationToken cancellationToken) { var data = await _context.SystemLogs.OrderBy($"{request.OrderBy} {request.SortDirection}") - .ProjectToPaginatedDataAsync(request.Specification, request.PageNumber, request.PageSize, - SystemLogMapper.ToDto, cancellationToken); + .ProjectToPaginatedDataAsync(request.Specification, request.PageNumber, request.PageSize, + _mapper.ConfigurationProvider, cancellationToken); return data; } } \ No newline at end of file diff --git a/src/Application/Features/Tenants/Commands/AddEdit/AddEditTenantCommand.cs b/src/Application/Features/Tenants/Commands/AddEdit/AddEditTenantCommand.cs index b2c05be2f..131c660e4 100644 --- a/src/Application/Features/Tenants/Commands/AddEdit/AddEditTenantCommand.cs +++ b/src/Application/Features/Tenants/Commands/AddEdit/AddEditTenantCommand.cs @@ -4,7 +4,8 @@ using CleanArchitecture.Blazor.Application.Common.Interfaces.MultiTenant; using CleanArchitecture.Blazor.Application.Features.Tenants.Caching; -using CleanArchitecture.Blazor.Application.Features.Tenants.Mappers; +using CleanArchitecture.Blazor.Application.Features.Tenants.DTOs; + namespace CleanArchitecture.Blazor.Application.Features.Tenants.Commands.AddEdit; @@ -15,18 +16,29 @@ public class AddEditTenantCommand : ICacheInvalidatorRequest> [Description("Description")] public string? Description { get; set; } public string CacheKey => TenantCacheKey.GetAllCacheKey; public IEnumerable? Tags => TenantCacheKey.Tags; + private class Mapping : Profile + { + public Mapping() + { + CreateMap(MemberList.None); + CreateMap(MemberList.None); + } + } } public class AddEditTenantCommandHandler : IRequestHandler> { private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; private readonly ITenantService _tenantsService; public AddEditTenantCommandHandler( + IMapper mapper, ITenantService tenantsService, IApplicationDbContext context ) { + _mapper = mapper; _tenantsService = tenantsService; _context = context; } @@ -36,12 +48,12 @@ public async Task> Handle(AddEditTenantCommand request, Cancellat var item = await _context.Tenants.FindAsync(new object[] { request.Id }, cancellationToken); if (item is null) { - item = TenantMapper.Map(request); + item = _mapper.Map(request); await _context.Tenants.AddAsync(item, cancellationToken); } else { - TenantMapper.MapTo(request, item); + item = _mapper.Map(request, item); } await _context.SaveChangesAsync(cancellationToken); _tenantsService.Refresh(); diff --git a/src/Application/Features/Tenants/DTOs/TenantDto.cs b/src/Application/Features/Tenants/DTOs/TenantDto.cs index d9032034b..f3e24e463 100644 --- a/src/Application/Features/Tenants/DTOs/TenantDto.cs +++ b/src/Application/Features/Tenants/DTOs/TenantDto.cs @@ -9,4 +9,11 @@ public class TenantDto [Description("Tenant Id")] public string Id { get; set; } = Guid.NewGuid().ToString(); [Description("Tenant Name")] public string? Name { get; set; } [Description("Description")] public string? Description { get; set; } + private class Mapping : Profile + { + public Mapping() + { + CreateMap().ReverseMap(); + } + } } \ No newline at end of file diff --git a/src/Application/Features/Tenants/Mappers/TenantMapper.cs b/src/Application/Features/Tenants/Mappers/TenantMapper.cs deleted file mode 100644 index 00ac2fff8..000000000 --- a/src/Application/Features/Tenants/Mappers/TenantMapper.cs +++ /dev/null @@ -1,16 +0,0 @@ -using CleanArchitecture.Blazor.Application.Features.Tenants.Commands.AddEdit; -using CleanArchitecture.Blazor.Application.Features.Tenants.DTOs; - -namespace CleanArchitecture.Blazor.Application.Features.Tenants.Mappers; -#pragma warning disable RMG020 -#pragma warning disable RMG012 -[Mapper] -public static partial class TenantMapper -{ - public static partial TenantDto ToDto(Tenant tenant); - public static partial Tenant Map(TenantDto dto); - public static partial Tenant Map(AddEditTenantCommand command); - public static partial void MapTo(AddEditTenantCommand command, Tenant tenant); - public static partial AddEditTenantCommand ToEditCommand(TenantDto dto); - public static partial IQueryable ProjectTo(this IQueryable q); -} diff --git a/src/Application/Features/Tenants/Queries/GetAll/GetAllTenantsQuery.cs b/src/Application/Features/Tenants/Queries/GetAll/GetAllTenantsQuery.cs index 253b55eb5..9e4c0fe0e 100644 --- a/src/Application/Features/Tenants/Queries/GetAll/GetAllTenantsQuery.cs +++ b/src/Application/Features/Tenants/Queries/GetAll/GetAllTenantsQuery.cs @@ -1,9 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. + using CleanArchitecture.Blazor.Application.Features.Tenants.Caching; using CleanArchitecture.Blazor.Application.Features.Tenants.DTOs; -using CleanArchitecture.Blazor.Application.Features.Tenants.Mappers; namespace CleanArchitecture.Blazor.Application.Features.Tenants.Queries.GetAll; @@ -16,18 +16,21 @@ public class GetAllTenantsQuery : ICacheableRequest> public class GetAllTenantsQueryHandler : IRequestHandler> { + private readonly IMapper _mapper; private readonly IApplicationDbContext _context; public GetAllTenantsQueryHandler( + IMapper mapper, IApplicationDbContext context ) { + _mapper = mapper; _context = context; } public async Task> Handle(GetAllTenantsQuery request, CancellationToken cancellationToken) { - var data = await _context.Tenants.OrderBy(x => x.Name) - .ProjectTo() + var data = await _context.Tenants + .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(cancellationToken); return data; } diff --git a/src/Application/Features/Tenants/Queries/Pagination/TenantsPaginationQuery.cs b/src/Application/Features/Tenants/Queries/Pagination/TenantsPaginationQuery.cs index 2b175db8f..31dd800d2 100644 --- a/src/Application/Features/Tenants/Queries/Pagination/TenantsPaginationQuery.cs +++ b/src/Application/Features/Tenants/Queries/Pagination/TenantsPaginationQuery.cs @@ -3,7 +3,6 @@ using CleanArchitecture.Blazor.Application.Features.Tenants.Caching; using CleanArchitecture.Blazor.Application.Features.Tenants.DTOs; -using CleanArchitecture.Blazor.Application.Features.Tenants.Mappers; namespace CleanArchitecture.Blazor.Application.Features.Tenants.Queries.Pagination; @@ -22,12 +21,15 @@ public override string ToString() public class TenantsWithPaginationQueryHandler : IRequestHandler> { + private readonly IMapper _mapper; private readonly IApplicationDbContext _context; public TenantsWithPaginationQueryHandler( + IMapper mapper, IApplicationDbContext context ) { + _mapper = mapper; _context = context; } @@ -35,8 +37,8 @@ public async Task> Handle(TenantsWithPaginationQuery re CancellationToken cancellationToken) { var data = await _context.Tenants.OrderBy($"{request.OrderBy} {request.SortDirection}") - .ProjectToPaginatedDataAsync(request.Specification, request.PageNumber, request.PageSize, - TenantMapper.ToDto, cancellationToken); + .ProjectToPaginatedDataAsync(request.Specification, request.PageNumber, request.PageSize, + _mapper.ConfigurationProvider, cancellationToken); return data; } } diff --git a/src/Application/_Imports.cs b/src/Application/_Imports.cs index 7bd672971..d2dbc5dc1 100644 --- a/src/Application/_Imports.cs +++ b/src/Application/_Imports.cs @@ -5,8 +5,10 @@ global using System.Reflection; global using System.Text.Json; global using Ardalis.Specification; -global using Riok.Mapperly.Abstractions; +global using AutoMapper; +global using AutoMapper.QueryableExtensions; global using CleanArchitecture.Blazor.Application.Common.ExceptionHandlers; +global using CleanArchitecture.Blazor.Application.Common.Interfaces.Serialization; global using CleanArchitecture.Blazor.Application.Common.Extensions; global using CleanArchitecture.Blazor.Application.Common.Interfaces; global using CleanArchitecture.Blazor.Application.Common.Interfaces.Caching; diff --git a/src/Infrastructure/Services/Identity/IdentityService.cs b/src/Infrastructure/Services/Identity/IdentityService.cs index 2c742b93d..db031c6ec 100644 --- a/src/Infrastructure/Services/Identity/IdentityService.cs +++ b/src/Infrastructure/Services/Identity/IdentityService.cs @@ -1,7 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using CleanArchitecture.Blazor.Application.Features.Identity.Mappers; +using AutoMapper; +using AutoMapper.QueryableExtensions; using CleanArchitecture.Blazor.Application.Common.ExceptionHandlers; using CleanArchitecture.Blazor.Application.Features.Identity.DTOs; using CleanArchitecture.Blazor.Domain.Identity; @@ -16,6 +17,7 @@ public class IdentityService : IIdentityService private readonly IAuthorizationService _authorizationService; private readonly IStringLocalizer _localizer; private readonly IFusionCache _fusionCache; + private readonly IMapper _mapper; private readonly IUserClaimsPrincipalFactory _userClaimsPrincipalFactory; private readonly UserManager _userManager; @@ -23,6 +25,7 @@ public IdentityService( IServiceScopeFactory scopeFactory, IApplicationSettings appConfig, IFusionCache fusionCache, + IMapper mapper, IStringLocalizer localizer) { var scope = scopeFactory.CreateScope(); @@ -30,6 +33,7 @@ public IdentityService( _userClaimsPrincipalFactory =scope.ServiceProvider.GetRequiredService>(); _authorizationService = scope.ServiceProvider.GetRequiredService(); _fusionCache = fusionCache; + _mapper = mapper; _localizer = localizer; } @@ -83,7 +87,7 @@ public async Task AuthorizeAsync(string userId, string policyName, Cancell var key = GetApplicationUserCacheKey(userName); var result = await _fusionCache.GetOrSetAsync(key, _ => _userManager.Users.Where(x => x.UserName == userName).Include(x => x.UserRoles) - .ThenInclude(x => x.Role).ProjectTo() + .ThenInclude(x => x.Role).ProjectTo(_mapper.ConfigurationProvider) .FirstOrDefaultAsync(cancellation), RefreshInterval); return result; } @@ -95,11 +99,13 @@ public async Task AuthorizeAsync(string userId, string policyName, Cancell async (tenantId, token) => { if (string.IsNullOrEmpty(tenantId)) + { return await _userManager.Users.Include(x => x.UserRoles).ThenInclude(x => x.Role) - .ProjectTo().ToListAsync(); + .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + } return await _userManager.Users.Where(x => x.TenantId == tenantId).Include(x => x.UserRoles) .ThenInclude(x => x.Role) - .ProjectTo().ToListAsync(); + .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); }; var result = await _fusionCache.GetOrSetAsync(key, _=>getUsersByTenantId(tenantId, cancellation), RefreshInterval); return result; diff --git a/src/Infrastructure/Services/Identity/UserService.cs b/src/Infrastructure/Services/Identity/UserService.cs index 5bf08d86b..55a84c9f9 100644 --- a/src/Infrastructure/Services/Identity/UserService.cs +++ b/src/Infrastructure/Services/Identity/UserService.cs @@ -1,4 +1,5 @@ -using CleanArchitecture.Blazor.Application.Features.Identity.Mappers; +using AutoMapper; +using AutoMapper.QueryableExtensions; using CleanArchitecture.Blazor.Application.Features.Identity.DTOs; using CleanArchitecture.Blazor.Domain.Identity; using ZiggyCreatures.Caching.Fusion; @@ -8,13 +9,16 @@ namespace CleanArchitecture.Blazor.Infrastructure.Services.Identity; public class UserService : IUserService { private const string CACHEKEY = "ALL-ApplicationUserDto"; + private readonly IMapper _mapper; private readonly IFusionCache _fusionCache; private readonly UserManager _userManager; public UserService( + IMapper mapper, IFusionCache fusionCache, IServiceScopeFactory scopeFactory) { + _mapper = mapper; _fusionCache = fusionCache; var scope = scopeFactory.CreateScope(); _userManager = scope.ServiceProvider.GetRequiredService>(); @@ -29,7 +33,7 @@ public void Initialize() { DataSource = _fusionCache.GetOrSet(CACHEKEY, _ => _userManager.Users.Include(x => x.UserRoles).ThenInclude(x => x.Role) - .ProjectTo().OrderBy(x => x.UserName) + .ProjectTo(_mapper.ConfigurationProvider).OrderBy(x => x.UserName) .ToList()) ?? new List(); OnChange?.Invoke(); @@ -41,7 +45,7 @@ public void Refresh() _fusionCache.Remove(CACHEKEY); DataSource = _fusionCache.GetOrSet(CACHEKEY, _ => _userManager.Users.Include(x => x.UserRoles).ThenInclude(x => x.Role) - .ProjectTo().OrderBy(x => x.UserName) + .ProjectTo(_mapper.ConfigurationProvider).OrderBy(x => x.UserName) .ToList()) ?? new List(); OnChange?.Invoke(); diff --git a/src/Infrastructure/Services/MultiTenant/TenantService.cs b/src/Infrastructure/Services/MultiTenant/TenantService.cs index 9de03ad99..3a783bf0f 100644 --- a/src/Infrastructure/Services/MultiTenant/TenantService.cs +++ b/src/Infrastructure/Services/MultiTenant/TenantService.cs @@ -1,7 +1,7 @@ -using CleanArchitecture.Blazor.Application.Common.Interfaces.MultiTenant; + +using CleanArchitecture.Blazor.Application.Common.Interfaces.MultiTenant; using CleanArchitecture.Blazor.Application.Features.Tenants.Caching; using CleanArchitecture.Blazor.Application.Features.Tenants.DTOs; -using CleanArchitecture.Blazor.Application.Features.Tenants.Mappers; using ZiggyCreatures.Caching.Fusion; namespace CleanArchitecture.Blazor.Infrastructure.Services.MultiTenant; @@ -9,14 +9,17 @@ namespace CleanArchitecture.Blazor.Infrastructure.Services.MultiTenant; public class TenantService : ITenantService { private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; private readonly IFusionCache _fusionCache; public TenantService( + IMapper mapper, IFusionCache fusionCache, IServiceScopeFactory scopeFactory) { var scope = scopeFactory.CreateScope(); _context = scope.ServiceProvider.GetRequiredService(); + _mapper = mapper; _fusionCache = fusionCache; } @@ -27,8 +30,8 @@ public TenantService( public void Initialize() { DataSource = _fusionCache.GetOrSet(TenantCacheKey.TenantsCacheKey, - _ => _context.Tenants.OrderBy(x => x.Name) - .ProjectTo() + _ => _context.Tenants.ProjectTo(_mapper.ConfigurationProvider) + .OrderBy(x => x.Name) .ToList()) ?? new List(); } @@ -36,8 +39,8 @@ public void Refresh() { _fusionCache.Remove(TenantCacheKey.TenantsCacheKey); DataSource = _fusionCache.GetOrSet(TenantCacheKey.TenantsCacheKey, - _ => _context.Tenants.OrderBy(x => x.Name) - .ProjectTo() + _ => _context.Tenants.ProjectTo(_mapper.ConfigurationProvider) + .OrderBy(x => x.Name) .ToList()) ?? new List(); OnChange?.Invoke(); } diff --git a/src/Infrastructure/Services/PicklistService.cs b/src/Infrastructure/Services/PicklistService.cs index 3f2a1bc05..df86d1df5 100644 --- a/src/Infrastructure/Services/PicklistService.cs +++ b/src/Infrastructure/Services/PicklistService.cs @@ -1,6 +1,5 @@ using CleanArchitecture.Blazor.Application.Features.PicklistSets.Caching; using CleanArchitecture.Blazor.Application.Features.PicklistSets.DTOs; -using CleanArchitecture.Blazor.Application.Features.PicklistSets.Mappers; using ZiggyCreatures.Caching.Fusion; namespace CleanArchitecture.Blazor.Infrastructure.Services; @@ -8,14 +7,17 @@ namespace CleanArchitecture.Blazor.Infrastructure.Services; public class PicklistService : IPicklistService { private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; private readonly IFusionCache _fusionCache; public PicklistService( + IMapper mapper, IFusionCache fusionCache, IServiceScopeFactory scopeFactory) { var scope = scopeFactory.CreateScope(); _context = scope.ServiceProvider.GetRequiredService(); + _mapper = mapper; _fusionCache = fusionCache; } @@ -26,8 +28,8 @@ public PicklistService( public void Initialize() { DataSource = _fusionCache.GetOrSet(PicklistSetCacheKey.PicklistCacheKey, - _ => _context.PicklistSets.OrderBy(x => x.Name).ThenBy(x => x.Value) - .ProjectTo() + _ => _context.PicklistSets.ProjectTo(_mapper.ConfigurationProvider) + .OrderBy(x => x.Name).ThenBy(x => x.Value) .ToList() ) ?? new List(); } @@ -36,8 +38,8 @@ public void Refresh() { _fusionCache.Remove(PicklistSetCacheKey.PicklistCacheKey); DataSource = _fusionCache.GetOrSet(PicklistSetCacheKey.PicklistCacheKey, - _ => _context.PicklistSets.OrderBy(x => x.Name).ThenBy(x => x.Value) - .ProjectTo() + _ => _context.PicklistSets.ProjectTo(_mapper.ConfigurationProvider) + .OrderBy(x => x.Name).ThenBy(x => x.Value) .ToList() ) ?? new List(); OnChange?.Invoke(); diff --git a/src/Infrastructure/_Imports.cs b/src/Infrastructure/_Imports.cs index fce16c5ff..7006ab14e 100644 --- a/src/Infrastructure/_Imports.cs +++ b/src/Infrastructure/_Imports.cs @@ -1,7 +1,9 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. global using System.Security.Claims; +global using AutoMapper; +global using AutoMapper.QueryableExtensions; global using CleanArchitecture.Blazor.Application.Common.Interfaces; global using CleanArchitecture.Blazor.Application.Common.Interfaces.Identity; global using CleanArchitecture.Blazor.Application.Common.Models; diff --git a/src/Server.UI/Pages/Contacts/Contacts.razor b/src/Server.UI/Pages/Contacts/Contacts.razor index 6ecc3d2db..84c0ac478 100644 --- a/src/Server.UI/Pages/Contacts/Contacts.razor +++ b/src/Server.UI/Pages/Contacts/Contacts.razor @@ -3,7 +3,6 @@ @using BlazorDownloadFile @using CleanArchitecture.Blazor.Application.Features.Contacts.Caching @using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs -@using CleanArchitecture.Blazor.Application.Features.Contacts.Mappers @using CleanArchitecture.Blazor.Application.Features.Contacts.Specifications @using CleanArchitecture.Blazor.Application.Features.Contacts.Commands.Delete @using CleanArchitecture.Blazor.Application.Features.Contacts.Commands.Import @@ -25,7 +24,7 @@ MultiSelection="true" T="ContactDto" SelectOnRowClick="false" - RowClick="@(s=>OnView(s.Item))" + RowClick="@(s => OnView(s.Item))" @bind-SelectedItems="_selectedItems" @bind-SelectedItem="_currentDto" Hover="true" @ref="_table"> @@ -49,7 +48,7 @@ @if (_accessRights.Create) { + OnClick="OnCreate"> @ConstantString.New } @@ -61,14 +60,14 @@ @if (_accessRights.Delete) { + OnClick="OnDeleteChecked"> @ConstantString.Delete } @if (_accessRights.Export) { + OnClick="OnExport"> @ConstantString.Export } @@ -78,8 +77,8 @@ + Variant="Variant.Text" + Disabled="@_uploading"> @ConstantString.Import @@ -92,7 +91,7 @@ @if (_accessRights.Search) { + AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Small"> } @@ -106,32 +105,32 @@ @if (_accessRights.Edit || _accessRights.Delete) { + Dense="true" + EndIcon="@Icons.Material.Filled.KeyboardArrowDown" IconColor="Color.Info" AnchorOrigin="Origin.CenterLeft"> @if (_accessRights.Edit) { - @ConstantString.Edit + @ConstantString.Edit } @if (_accessRights.Delete) { - @ConstantString.Delete + @ConstantString.Delete } } else { + Disabled="true" + Icon="@Icons.Material.Filled.DoNotTouch" + Size="Size.Small" + Color="Color.Surface"> @ConstantString.NoAllowed } - +
@context.Item.Name @@ -139,14 +138,14 @@
- - - + + + - + @@ -179,7 +178,7 @@ Title = L[_currentDto.GetClassDescription()]; _accessRights = await PermissionService.GetAccessRightsAsync(); } - + private async Task> ServerReload(GridState state) { try @@ -246,7 +245,14 @@ private async Task OnClone() { var dto = _selectedItems.First(); - var command = ContactMapper.CloneFromDto(dto); + var command = new AddEditContactCommand() + { + Country = dto.Country, + Description = dto.Description, + Email = dto.Email, + Name = dto.Name, + PhoneNumber = dto.PhoneNumber + }; await ShowEditFormDialog(string.Format(ConstantString.CreateAnItem, L["Contact"]), command); } private Task OnEdit(ContactDto dto) @@ -287,13 +293,13 @@ { _exporting = true; var request = new ExportContactsQuery() - { - Keyword = Query.Keyword, - CurrentUser = UserProfile, - ListView = Query.ListView, - OrderBy = _table.SortDefinitions.Values.FirstOrDefault()?.SortBy ?? "Id", - SortDirection = (_table.SortDefinitions.Values.FirstOrDefault()?.Descending ?? true) ? SortDirection.Descending.ToString() : SortDirection.Ascending.ToString() - }; + { + Keyword = Query.Keyword, + CurrentUser = UserProfile, + ListView = Query.ListView, + OrderBy = _table.SortDefinitions.Values.FirstOrDefault()?.SortBy ?? "Id", + SortDirection = (_table.SortDefinitions.Values.FirstOrDefault()?.Descending ?? true) ? SortDirection.Descending.ToString() : SortDirection.Ascending.ToString() + }; var result = await Mediator.Send(request); await result.MatchAsync( async data => diff --git a/src/Server.UI/Pages/Contacts/EditContact.razor b/src/Server.UI/Pages/Contacts/EditContact.razor index 4832ac2d2..0869a4e4c 100644 --- a/src/Server.UI/Pages/Contacts/EditContact.razor +++ b/src/Server.UI/Pages/Contacts/EditContact.razor @@ -1,6 +1,5 @@ @page "/pages/Contacts/edit/{id:int}" @using CleanArchitecture.Blazor.Application.Features.Contacts.Commands.Update -@using CleanArchitecture.Blazor.Application.Features.Contacts.Mappers @using CleanArchitecture.Blazor.Application.Features.Contacts.Queries.GetById @using CleanArchitecture.Blazor.Server.UI.Components.Fusion @@ -68,7 +67,7 @@ var result = await Mediator.Send(new GetContactByIdQuery() { Id = Id }); result.Map(data => { - model = ContactMapper.ToUpdateCommand(data); + model = Mapper.Map(data); return data; }).Match(data => { diff --git a/src/Server.UI/Pages/Documents/Documents.razor b/src/Server.UI/Pages/Documents/Documents.razor index 4af9f7f3d..89b3b72b3 100644 --- a/src/Server.UI/Pages/Documents/Documents.razor +++ b/src/Server.UI/Pages/Documents/Documents.razor @@ -1,7 +1,6 @@ @page "/pages/documents" @attribute [Authorize(Policy = Permissions.Documents.View)] @using CleanArchitecture.Blazor.Application.Features.Documents.DTOs -@using CleanArchitecture.Blazor.Application.Features.Documents.Mappers @using CleanArchitecture.Blazor.Application.Features.Documents.Queries.GetFileStream @using CleanArchitecture.Blazor.Application.Features.Documents.Queries.PaginationQuery @using CleanArchitecture.Blazor.Application.Features.Documents.Specifications @@ -317,7 +316,7 @@ private async Task OnEdit(DocumentDto dto) { - var command = DocumentMapper.ToEditCommand(dto); + var command = Mapper.Map(dto); var parameters = new DialogParameters { { nameof(DocumentFormDialog.Model), command } diff --git a/src/Server.UI/Pages/Identity/Roles/Roles.razor b/src/Server.UI/Pages/Identity/Roles/Roles.razor index 8ca17f1cd..924bf1ae1 100644 --- a/src/Server.UI/Pages/Identity/Roles/Roles.razor +++ b/src/Server.UI/Pages/Identity/Roles/Roles.razor @@ -1,6 +1,5 @@ @page "/identity/roles" @using CleanArchitecture.Blazor.Application.Common.Interfaces.MultiTenant -@using CleanArchitecture.Blazor.Application.Features.Identity.Mappers @using CleanArchitecture.Blazor.Domain.Identity @using CleanArchitecture.Blazor.Application.Features.Identity.DTOs @using System.Security.Claims @@ -236,7 +235,7 @@ OnAssignChanged="OnAssignChangedHandler"> .EfOrderBySortDefinitions(state) .Skip(state.Page * state.PageSize) .Take(state.PageSize) - .ProjectTo() + .ProjectTo(Mapper.ConfigurationProvider) .ToListAsync(); return new GridData { TotalItems = totalCount, Items = roles }; diff --git a/src/Server.UI/Pages/Identity/Users/Profile.razor b/src/Server.UI/Pages/Identity/Users/Profile.razor index 26612bccf..00f58fd79 100644 --- a/src/Server.UI/Pages/Identity/Users/Profile.razor +++ b/src/Server.UI/Pages/Identity/Users/Profile.razor @@ -218,7 +218,7 @@ else var state = await AuthState; _currentUserName = state.User.Identity?.Name ?? string.Empty; await UserProfileStateService.InitializeAsync(state.User.Identity?.Name ?? string.Empty); - model = UserProfileMapper.ToChangeUserProfileModel(UserProfileStateService.UserProfile); + model = Mapper.Map(UserProfileStateService.UserProfile); } protected override void Dispose(bool disposing) diff --git a/src/Server.UI/Pages/Identity/Users/Users.razor b/src/Server.UI/Pages/Identity/Users/Users.razor index 92f5c79e6..6b95e12e2 100644 --- a/src/Server.UI/Pages/Identity/Users/Users.razor +++ b/src/Server.UI/Pages/Identity/Users/Users.razor @@ -1,6 +1,5 @@ @page "/identity/users" @using ActualLab.Fusion -@using CleanArchitecture.Blazor.Application.Features.Identity.Mappers @using CleanArchitecture.Blazor.Application.Features.Fusion @using CleanArchitecture.Blazor.Application.Features.Identity.Notifications.UserActivation @using CleanArchitecture.Blazor.Infrastructure.Persistence @@ -320,7 +319,9 @@ .Include(x => x.LastModifiedByUser) .Include(x => x.Superior) .EfOrderBySortDefinitions(state) - .Skip(state.Page * state.PageSize).Take(state.PageSize).ProjectTo().ToListAsync(); + .Skip(state.Page * state.PageSize).Take(state.PageSize) + .ProjectTo(Mapper.ConfigurationProvider) + .ToListAsync(); return new GridData { TotalItems = count, Items = data }; } diff --git a/src/Server.UI/Pages/PicklistSets/PicklistSets.razor b/src/Server.UI/Pages/PicklistSets/PicklistSets.razor index 674a993e2..805e82d70 100644 --- a/src/Server.UI/Pages/PicklistSets/PicklistSets.razor +++ b/src/Server.UI/Pages/PicklistSets/PicklistSets.razor @@ -1,6 +1,5 @@ @page "/system/picklistset" @using CleanArchitecture.Blazor.Application.Features.PicklistSets.DTOs -@using CleanArchitecture.Blazor.Application.Features.PicklistSets.Mappers @using CleanArchitecture.Blazor.Application.Features.PicklistSets.Queries.Export @using CleanArchitecture.Blazor.Application.Features.PicklistSets.Queries.PaginationQuery @using BlazorDownloadFile @@ -225,7 +224,7 @@ { InvokeAsync(async () => { - var command = PicklistMapper.ToEditCommand(item); + var command = Mapper.Map(item); var result = await Mediator.Send(command); if (!result.Succeeded) { diff --git a/src/Server.UI/Pages/Products/Products.razor b/src/Server.UI/Pages/Products/Products.razor index d38012ad0..03c4bcc6a 100644 --- a/src/Server.UI/Pages/Products/Products.razor +++ b/src/Server.UI/Pages/Products/Products.razor @@ -1,6 +1,5 @@ @page "/pages/products" @using CleanArchitecture.Blazor.Application.Features.Products.DTOs -@using CleanArchitecture.Blazor.Application.Features.Products.Mappers @using CleanArchitecture.Blazor.Application.Features.Products.Queries.Export @using CleanArchitecture.Blazor.Application.Features.Products.Queries.Pagination @using CleanArchitecture.Blazor.Application.Features.Products.Specifications @@ -42,21 +41,19 @@ - @ConstantString.Refresh @if (_accessRights.Create) { - @ConstantString.New } - + @if (_accessRights.Create) { @ConstantString.Clone @@ -86,9 +83,8 @@ - @ConstantString.Import + Disabled="@_uploading"> + @ConstantString.Import
@@ -193,7 +189,7 @@ private bool _pdfExporting; private int _defaultPageSize = 15; - + private ProductsWithPaginationQuery Query { get; } = new(); @@ -203,7 +199,7 @@ Title = L[_currentDto.GetClassDescription()]; _accessRights = await PermissionService.GetAccessRightsAsync(); } - + private async Task> ServerReload(GridState state) { try @@ -257,7 +253,7 @@ { x => x.Model, command } }; var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true }; - var dialog =await DialogService.ShowAsync(title, parameters, options); + var dialog = await DialogService.ShowAsync(title, parameters, options); var state = await dialog.Result; if (state is not null && !state.Canceled) @@ -277,13 +273,22 @@ private async Task OnClone() { var copy = _selectedItems.First(); - var command = ProductMapper.CloneFromDto(copy); + var command = new AddEditProductCommand() + { + Brand = copy.Brand, + Name = copy.Name, + Description = copy.Description, + Price = copy.Price, + Unit = copy.Unit, + Pictures = copy.Pictures + + }; await ShowEditFormDialog(string.Format(ConstantString.CreateAnItem, L["Product"]), command); } private async Task OnEdit(ProductDto dto) { - var command = ProductMapper.ToEditCommand(dto); + var command = Mapper.Map(dto); await ShowEditFormDialog(string.Format(ConstantString.EditTheItem, L["Product"]), command); } @@ -320,19 +325,19 @@ { _exporting = true; var request = new ExportProductsQuery - { - Brand = Query.Brand, - Name = Query.Name, - MinPrice = Query.MinPrice, - MaxPrice = Query.MaxPrice, - Unit = Query.Unit, - Keyword = Query.Keyword, - ListView = Query.ListView, - OrderBy = _table.SortDefinitions.Values.FirstOrDefault()?.SortBy ?? "Id", - SortDirection = _table.SortDefinitions.Values.FirstOrDefault()?.Descending ?? false ? SortDirection.Descending.ToString() : SortDirection.Ascending.ToString(), - CurrentUser = UserProfile, - ExportType = ExportType.Excel - }; + { + Brand = Query.Brand, + Name = Query.Name, + MinPrice = Query.MinPrice, + MaxPrice = Query.MaxPrice, + Unit = Query.Unit, + Keyword = Query.Keyword, + ListView = Query.ListView, + OrderBy = _table.SortDefinitions.Values.FirstOrDefault()?.SortBy ?? "Id", + SortDirection = _table.SortDefinitions.Values.FirstOrDefault()?.Descending ?? false ? SortDirection.Descending.ToString() : SortDirection.Ascending.ToString(), + CurrentUser = UserProfile, + ExportType = ExportType.Excel + }; var result = await Mediator.Send(request); if (result.Succeeded) { @@ -351,19 +356,19 @@ { _pdfExporting = true; var request = new ExportProductsQuery - { - Brand = Query.Brand, - Name = Query.Name, - MinPrice = Query.MinPrice, - MaxPrice = Query.MaxPrice, - Unit = Query.Unit, - Keyword = Query.Keyword, - ListView = Query.ListView, - CurrentUser = UserProfile, - OrderBy = _table.SortDefinitions.Values.FirstOrDefault()?.SortBy ?? "Id", - SortDirection = _table.SortDefinitions.Values.FirstOrDefault()?.Descending ?? false ? SortDirection.Descending.ToString() : SortDirection.Ascending.ToString(), - ExportType = ExportType.PDF - }; + { + Brand = Query.Brand, + Name = Query.Name, + MinPrice = Query.MinPrice, + MaxPrice = Query.MaxPrice, + Unit = Query.Unit, + Keyword = Query.Keyword, + ListView = Query.ListView, + CurrentUser = UserProfile, + OrderBy = _table.SortDefinitions.Values.FirstOrDefault()?.SortBy ?? "Id", + SortDirection = _table.SortDefinitions.Values.FirstOrDefault()?.Descending ?? false ? SortDirection.Descending.ToString() : SortDirection.Ascending.ToString(), + ExportType = ExportType.PDF + }; var result = await Mediator.Send(request); if (result.Succeeded) { diff --git a/src/Server.UI/Pages/Tenants/Tenants.razor b/src/Server.UI/Pages/Tenants/Tenants.razor index 7c7187909..192040321 100644 --- a/src/Server.UI/Pages/Tenants/Tenants.razor +++ b/src/Server.UI/Pages/Tenants/Tenants.razor @@ -1,6 +1,5 @@ @page "/system/tenants" @using CleanArchitecture.Blazor.Application.Features.Tenants.DTOs -@using CleanArchitecture.Blazor.Application.Features.Tenants.Mappers @using CleanArchitecture.Blazor.Application.Features.Tenants.Queries.Pagination @using CleanArchitecture.Blazor.Application.Features.Tenants.Caching @using CleanArchitecture.Blazor.Application.Features.Tenants.Commands.AddEdit @@ -194,7 +193,7 @@ private async Task OnEdit(TenantDto dto) { - var command = TenantMapper.ToEditCommand(dto); + var command = Mapper.Map(dto); await ShowEditDialog(command, L["Edit the Tenant"]); } diff --git a/src/Server.UI/Server.UI.csproj b/src/Server.UI/Server.UI.csproj index a69acf480..693cb5b15 100644 --- a/src/Server.UI/Server.UI.csproj +++ b/src/Server.UI/Server.UI.csproj @@ -15,9 +15,9 @@ default + - diff --git a/src/Server.UI/_Imports.cs b/src/Server.UI/_Imports.cs index 1d3911bce..4654b1ed1 100644 --- a/src/Server.UI/_Imports.cs +++ b/src/Server.UI/_Imports.cs @@ -8,3 +8,4 @@ global using Microsoft.AspNetCore.Components; global using MudBlazor; global using Microsoft.EntityFrameworkCore; +global using AutoMapper; \ No newline at end of file diff --git a/src/Server.UI/_Imports.razor b/src/Server.UI/_Imports.razor index 7a8e3e74a..37cd98816 100644 --- a/src/Server.UI/_Imports.razor +++ b/src/Server.UI/_Imports.razor @@ -19,6 +19,8 @@ @using Microsoft.JSInterop @using MudBlazor @using MediatR +@using AutoMapper +@using AutoMapper.QueryableExtensions @using FluentValidation; @using CleanArchitecture.Blazor.Domain @using CleanArchitecture.Blazor.Application.Common.ExceptionHandlers @@ -61,4 +63,5 @@ @inject IMediator Mediator @inject NavigationManager Navigation @inject DialogServiceHelper DialogServiceHelper -@inject IPermissionService PermissionService \ No newline at end of file +@inject IPermissionService PermissionService +@inject IMapper Mapper \ No newline at end of file diff --git a/tests/Application.UnitTests/Common/Mappings/ApplicationUserMapperTests.cs b/tests/Application.UnitTests/Common/Mappings/ApplicationUserMapperTests.cs deleted file mode 100644 index a7d7a9e77..000000000 --- a/tests/Application.UnitTests/Common/Mappings/ApplicationUserMapperTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using CleanArchitecture.Blazor.Application.Features.Identity.Mappers; -using CleanArchitecture.Blazor.Domain.Identity; -using NUnit.Framework.Legacy; -using NUnit.Framework; - -namespace CleanArchitecture.Blazor.Application.UnitTests.Common.Mappings; -[TestFixture] -public class ApplicationUserMapperTests -{ - [Test] - public void ToApplicationUserDto_Should_Map_Properties_Correctly() - { - // Arrange - var user = new ApplicationUser - { - Id = Guid.NewGuid().ToString(), - UserName = "testuser", - Email = "testuser@example.com", - UserRoles = new List - { - new ApplicationUserRole - { - Role = new ApplicationRole { Name = "Admin" } - }, - new ApplicationUserRole - { - Role = new ApplicationRole { Name = "User" } - } - }, - // Initialize other properties as needed - }; - - // Act - var userDto = ApplicationUserMapper.ToApplicationUserDto(user); - - // Assert - Assert.That(userDto!=null); - Assert.That(user.Id, Is.EqualTo(userDto.Id)); - Assert.That(user.UserName, Is.EqualTo(userDto.UserName)); - Assert.That(user.Email, Is.EqualTo(userDto.Email)); - Assert.That(userDto.AssignedRoles.Length, Is.EqualTo(2)); - CollectionAssert.AreEquivalent(new[] { "Admin", "User" }, userDto.AssignedRoles); - // Add more assertions for other properties if necessary - } - - [Test] - public void ProjectTo_Should_Project_IQueryable_Correctly() - { - // Arrange - var users = new List - { - new ApplicationUser - { - Id = Guid.NewGuid().ToString(), - UserName = "testuser1", - Email = "testuser1@example.com", - UserRoles = new List - { - new ApplicationUserRole - { - Role = new ApplicationRole { Name = "Admin" } - } - }, - }, - new ApplicationUser - { - Id = Guid.NewGuid().ToString(), - UserName = "testuser2", - Email = "testuser2@example.com", - UserRoles = new List - { - new ApplicationUserRole - { - Role = new ApplicationRole { Name = "User" } - } - }, - } - }.AsQueryable(); - - // Act - var userDtos = users.ProjectTo().ToList(); - - // Assert - Assert.That(userDtos.Any()); - Assert.That(users.Count()==userDtos.Count); - - for (int i = 0; i < users.Count(); i++) - { - var user = users.ElementAt(i); - var userDto = userDtos[i]; - - Assert.That(user.Id, Is.EqualTo(userDto.Id)); - Assert.That(user.UserName, Is.EqualTo(userDto.UserName)); - Assert.That(user.Email, Is.EqualTo(userDto.Email)); - Assert.That(userDto.AssignedRoles.Length, Is.EqualTo(1)); - Assert.That(user.UserRoles.First().Role.Name, Is.EqualTo(userDto.AssignedRoles.First())); - } - } -} \ No newline at end of file diff --git a/tests/Application.UnitTests/Common/Mappings/MappingTests.cs b/tests/Application.UnitTests/Common/Mappings/MappingTests.cs new file mode 100644 index 000000000..a9a457220 --- /dev/null +++ b/tests/Application.UnitTests/Common/Mappings/MappingTests.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; +using AutoMapper; +using CleanArchitecture.Blazor.Application.Common.Interfaces; +using CleanArchitecture.Blazor.Application.Features.AuditTrails.DTOs; +using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs; +using CleanArchitecture.Blazor.Application.Features.Documents.DTOs; +using CleanArchitecture.Blazor.Application.Features.Identity.DTOs; +using CleanArchitecture.Blazor.Application.Features.PicklistSets.DTOs; +using CleanArchitecture.Blazor.Application.Features.Products.DTOs; +using CleanArchitecture.Blazor.Application.Features.SystemLogs.DTOs; +using CleanArchitecture.Blazor.Application.Features.Tenants.DTOs; +using CleanArchitecture.Blazor.Domain.Entities; +using CleanArchitecture.Blazor.Domain.Identity; +using NUnit.Framework; + +namespace CleanArchitecture.Blazor.Application.UnitTests.Common.Mappings; +public class MappingTests +{ + private readonly IConfigurationProvider _configuration; + private readonly IMapper _mapper; + + public MappingTests() + { + _configuration = new MapperConfiguration(cfg => cfg.AddMaps(Assembly.GetAssembly(typeof(IApplicationDbContext)))); + _mapper = _configuration.CreateMapper(); + } + + [Test] + public void ShouldHaveValidConfiguration() + { + _configuration.AssertConfigurationIsValid(); + } + + [Test] + [TestCase(typeof(Document), typeof(DocumentDto), true)] + [TestCase(typeof(Tenant), typeof(TenantDto), true)] + [TestCase(typeof(Product), typeof(ProductDto), true)] + [TestCase(typeof(Contact), typeof(ContactDto), true)] + [TestCase(typeof(PicklistSet), typeof(PicklistSetDto), true)] + [TestCase(typeof(ApplicationUser), typeof(ApplicationUserDto), false)] + [TestCase(typeof(ApplicationRole), typeof(ApplicationRoleDto), false)] + [TestCase(typeof(SystemLog), typeof(SystemLogDto), false)] + [TestCase(typeof(AuditTrail), typeof(AuditTrailDto), false)] + public void ShouldSupportMappingFromSourceToDestination(Type source, Type destination, bool inverseMap = false) + { + var instance = GetInstanceOf(source); + + _mapper.Map(instance, source, destination); + + if (inverseMap) + { + ShouldSupportMappingFromSourceToDestination(destination, source, false); + } + } + + private object GetInstanceOf(Type type) + { + if (type.GetConstructor(Type.EmptyTypes) != null) + return Activator.CreateInstance(type); + + // Type without parameterless constructor + return FormatterServices.GetUninitializedObject(type); + } +} From 918c613e7efd1a73aa5a18b8df369b2066dd4f4d Mon Sep 17 00:00:00 2001 From: Zhu Date: Thu, 13 Mar 2025 12:13:40 +0800 Subject: [PATCH 2/4] remove BlazorDownloadFileService --- src/Application/Application.csproj | 4 +-- src/Application/DependencyInjection.cs | 2 +- .../Commands/Update/UpdateContactCommand.cs | 4 +-- src/Domain/Domain.csproj | 18 +++++----- src/Infrastructure/Infrastructure.csproj | 6 ++-- src/Server.UI/DependencyInjection.cs | 3 +- src/Server.UI/Pages/Contacts/Contacts.razor | 5 ++- src/Server.UI/Pages/Documents/Documents.razor | 6 ++-- .../Pages/Identity/Users/Users.razor | 5 ++- .../Pages/PicklistSets/PicklistSets.razor | 5 ++- src/Server.UI/Pages/Products/Products.razor | 7 ++-- src/Server.UI/Server.UI.csproj | 5 ++- .../JsInterop/BlazorDownloadFileService.cs | 35 +++++++++++++++++++ src/Server.UI/_Imports.razor | 1 + src/Server.UI/wwwroot/js/downloadFile.js | 12 +++++++ 15 files changed, 80 insertions(+), 38 deletions(-) create mode 100644 src/Server.UI/Services/JsInterop/BlazorDownloadFileService.cs create mode 100644 src/Server.UI/wwwroot/js/downloadFile.js diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index 85b495ebc..40977c02d 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -18,8 +18,8 @@ - - + + diff --git a/src/Application/DependencyInjection.cs b/src/Application/DependencyInjection.cs index 986f45b53..7a96ae1e6 100644 --- a/src/Application/DependencyInjection.cs +++ b/src/Application/DependencyInjection.cs @@ -13,7 +13,7 @@ public static class DependencyInjection { public static IServiceCollection AddApplication(this IServiceCollection services) { - services.AddAutoMapper(config => { config.AddMaps(Assembly.GetExecutingAssembly()); }); + services.AddAutoMapper(Assembly.GetExecutingAssembly()); services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); services.AddTransient(typeof(IRequestExceptionHandler<,,>), typeof(DbExceptionHandler<,,>)); services.AddMediatR(config => diff --git a/src/Application/Features/Contacts/Commands/Update/UpdateContactCommand.cs b/src/Application/Features/Contacts/Commands/Update/UpdateContactCommand.cs index 9cc1e3002..6b75508cc 100644 --- a/src/Application/Features/Contacts/Commands/Update/UpdateContactCommand.cs +++ b/src/Application/Features/Contacts/Commands/Update/UpdateContactCommand.cs @@ -58,10 +58,10 @@ public Mapping() public class UpdateContactCommandHandler : IRequestHandler> { - private readonly Mapper _mapper; + private readonly IMapper _mapper; private readonly IApplicationDbContext _context; public UpdateContactCommandHandler( - Mapper mapper, + IMapper mapper, IApplicationDbContext context) { _mapper = mapper; diff --git a/src/Domain/Domain.csproj b/src/Domain/Domain.csproj index ade1292e5..159d80a0e 100644 --- a/src/Domain/Domain.csproj +++ b/src/Domain/Domain.csproj @@ -11,23 +11,23 @@ - - + + - + - - + + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index a53abd279..cb76397e0 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -8,9 +8,9 @@ default - - - + + + diff --git a/src/Server.UI/DependencyInjection.cs b/src/Server.UI/DependencyInjection.cs index cf5aa997b..55eb2ec57 100644 --- a/src/Server.UI/DependencyInjection.cs +++ b/src/Server.UI/DependencyInjection.cs @@ -1,5 +1,4 @@ using System.Net.Http.Headers; -using BlazorDownloadFile; using CleanArchitecture.Blazor.Infrastructure.Constants.Localization; using CleanArchitecture.Blazor.Server.UI.Hubs; using CleanArchitecture.Blazor.Server.UI.Services; @@ -102,7 +101,7 @@ public static IServiceCollection AddServerUI(this IServiceCollection services, I .AddScoped() .AddScoped() .AddScoped() - .AddBlazorDownloadFile() + .AddScoped() .AddScoped() .AddScoped() .AddScoped() diff --git a/src/Server.UI/Pages/Contacts/Contacts.razor b/src/Server.UI/Pages/Contacts/Contacts.razor index 84c0ac478..8f34db8f1 100644 --- a/src/Server.UI/Pages/Contacts/Contacts.razor +++ b/src/Server.UI/Pages/Contacts/Contacts.razor @@ -1,6 +1,5 @@ @page "/pages/Contacts" -@using BlazorDownloadFile @using CleanArchitecture.Blazor.Application.Features.Contacts.Caching @using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs @using CleanArchitecture.Blazor.Application.Features.Contacts.Specifications @@ -169,7 +168,7 @@ private ContactsWithPaginationQuery Query { get; set; } = new(); [Inject] - private IBlazorDownloadFileService BlazorDownloadFileService { get; set; } = null!; + private BlazorDownloadFileService BlazorDownloadFileService { get; set; } = null!; private ContactsAccessRights _accessRights = new(); @@ -304,7 +303,7 @@ await result.MatchAsync( async data => { - var downloadresult = await BlazorDownloadFileService.DownloadFile($"{L["Contacts"]}.xlsx", result.Data, contentType: "application/octet-stream"); + await BlazorDownloadFileService.DownloadFileAsync($"{L["Contacts"]}.xlsx", result.Data, contentType: "application/octet-stream"); Snackbar.Add($"{ConstantString.ExportSuccess}", MudBlazor.Severity.Info); return data; }, diff --git a/src/Server.UI/Pages/Documents/Documents.razor b/src/Server.UI/Pages/Documents/Documents.razor index 89b3b72b3..2e5eb34ec 100644 --- a/src/Server.UI/Pages/Documents/Documents.razor +++ b/src/Server.UI/Pages/Documents/Documents.razor @@ -5,14 +5,14 @@ @using CleanArchitecture.Blazor.Application.Features.Documents.Queries.PaginationQuery @using CleanArchitecture.Blazor.Application.Features.Documents.Specifications @using CleanArchitecture.Blazor.Domain.Common.Enums -@using BlazorDownloadFile @using CleanArchitecture.Blazor.Server.UI.Hubs @using CleanArchitecture.Blazor.Server.UI.Pages.Documents.Components @using CleanArchitecture.Blazor.Application.Features.Documents.Caching @using CleanArchitecture.Blazor.Application.Features.Documents.Commands.AddEdit @using CleanArchitecture.Blazor.Application.Features.Documents.Commands.Delete + @implements IDisposable -@inject IBlazorDownloadFileService BlazorDownloadFileService +@inject BlazorDownloadFileService BlazorDownloadFileService @inject HubClient Client @inject IStringLocalizer L @@ -369,7 +369,7 @@ var file = await Mediator.Send(new GetFileStreamQuery(dto.Id)); if (file.Item2 != null) { - var result = await BlazorDownloadFileService.DownloadFile(file.Item1, file.Item2, "application/octet-stream"); + await BlazorDownloadFileService.DownloadFileAsync(file.Item1, file.Item2, "application/octet-stream"); } } catch (Exception e) diff --git a/src/Server.UI/Pages/Identity/Users/Users.razor b/src/Server.UI/Pages/Identity/Users/Users.razor index 6b95e12e2..7a106d009 100644 --- a/src/Server.UI/Pages/Identity/Users/Users.razor +++ b/src/Server.UI/Pages/Identity/Users/Users.razor @@ -12,7 +12,6 @@ @using CleanArchitecture.Blazor.Application.Features.Identity.Notifications.SendWelcome @using CleanArchitecture.Blazor.Application.Common.Interfaces.Identity @using CleanArchitecture.Blazor.Application.Common.Interfaces.MultiTenant -@using BlazorDownloadFile @using CleanArchitecture.Blazor.Server.UI.Pages.Identity.Roles.Components @using CleanArchitecture.Blazor.Server.UI.Pages.Identity.Users.Components @using System.Linq.Expressions @@ -27,7 +26,7 @@ @implements IDisposable @inject IOnlineUserTracker OnlineUserTracker -@inject IBlazorDownloadFileService BlazorDownloadFileService +@inject BlazorDownloadFileService BlazorDownloadFileService @inject IUserService UserService @inject ITenantService TenantsService @inject IFusionCache FusionCache @@ -771,7 +770,7 @@ }).ToListAsync(); var result = await ExportUsers(items); - await BlazorDownloadFileService.DownloadFile($"{L["Users"]}.xlsx", result, "application/octet-stream"); + await BlazorDownloadFileService.DownloadFileAsync($"{L["Users"]}.xlsx", result, "application/octet-stream"); Snackbar.Add(ConstantString.ExportSuccess, Severity.Info); } finally { _exporting = false; } diff --git a/src/Server.UI/Pages/PicklistSets/PicklistSets.razor b/src/Server.UI/Pages/PicklistSets/PicklistSets.razor index 805e82d70..bca0d1ba2 100644 --- a/src/Server.UI/Pages/PicklistSets/PicklistSets.razor +++ b/src/Server.UI/Pages/PicklistSets/PicklistSets.razor @@ -2,7 +2,6 @@ @using CleanArchitecture.Blazor.Application.Features.PicklistSets.DTOs @using CleanArchitecture.Blazor.Application.Features.PicklistSets.Queries.Export @using CleanArchitecture.Blazor.Application.Features.PicklistSets.Queries.PaginationQuery -@using BlazorDownloadFile @using CleanArchitecture.Blazor.Application.Features.PicklistSets.Caching @using CleanArchitecture.Blazor.Application.Features.PicklistSets.Commands.AddEdit @using CleanArchitecture.Blazor.Application.Features.PicklistSets.Commands.Delete @@ -12,7 +11,7 @@ @attribute [Authorize(Policy = Permissions.PicklistSets.View)] @inject IStringLocalizer L -@inject IBlazorDownloadFileService BlazorDownloadFileService +@inject BlazorDownloadFileService BlazorDownloadFileService @Title @@ -302,7 +301,7 @@ { var request = new ExportPicklistSetsQuery { Keyword = _searchString }; var result = await Mediator.Send(request); - await BlazorDownloadFileService.DownloadFile($"{L["Picklist"]}.xlsx", result, "application/octet-stream"); + await BlazorDownloadFileService.DownloadFileAsync($"{L["Picklist"]}.xlsx", result, "application/octet-stream"); Snackbar.Add(ConstantString.ExportSuccess, Severity.Info); } finally diff --git a/src/Server.UI/Pages/Products/Products.razor b/src/Server.UI/Pages/Products/Products.razor index 03c4bcc6a..c3ce2946b 100644 --- a/src/Server.UI/Pages/Products/Products.razor +++ b/src/Server.UI/Pages/Products/Products.razor @@ -4,7 +4,6 @@ @using CleanArchitecture.Blazor.Application.Features.Products.Queries.Pagination @using CleanArchitecture.Blazor.Application.Features.Products.Specifications @using CleanArchitecture.Blazor.Domain.Common.Enums -@using BlazorDownloadFile @using CleanArchitecture.Blazor.Server.UI.Pages.Products.Components @using CleanArchitecture.Blazor.Application.Features.Products.Caching @using CleanArchitecture.Blazor.Application.Features.Products.Commands.AddEdit @@ -13,7 +12,7 @@ @attribute [Authorize(Policy = Permissions.Products.View)] @inject IStringLocalizer L -@inject IBlazorDownloadFileService BlazorDownloadFileService +@inject BlazorDownloadFileService BlazorDownloadFileService @Title @@ -341,7 +340,7 @@ var result = await Mediator.Send(request); if (result.Succeeded) { - var downloadResult = await BlazorDownloadFileService.DownloadFile($"{L["Products"]}.xlsx", result.Data, "application/octet-stream"); + await BlazorDownloadFileService.DownloadFileAsync($"{L["Products"]}.xlsx", result.Data, "application/octet-stream"); Snackbar.Add($"{ConstantString.ExportSuccess}", Severity.Info); } else @@ -372,7 +371,7 @@ var result = await Mediator.Send(request); if (result.Succeeded) { - var downloadResult = await BlazorDownloadFileService.DownloadFile($"{L["Products"]}.pdf", result.Data, "application/octet-stream"); + await BlazorDownloadFileService.DownloadFileAsync($"{L["Products"]}.pdf", result.Data, "application/octet-stream"); Snackbar.Add($"{ConstantString.ExportSuccess}", Severity.Info); } else diff --git a/src/Server.UI/Server.UI.csproj b/src/Server.UI/Server.UI.csproj index 693cb5b15..d9794f309 100644 --- a/src/Server.UI/Server.UI.csproj +++ b/src/Server.UI/Server.UI.csproj @@ -15,14 +15,13 @@ default - - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Server.UI/Services/JsInterop/BlazorDownloadFileService.cs b/src/Server.UI/Services/JsInterop/BlazorDownloadFileService.cs new file mode 100644 index 000000000..dd1c0025e --- /dev/null +++ b/src/Server.UI/Services/JsInterop/BlazorDownloadFileService.cs @@ -0,0 +1,35 @@ +using Microsoft.JSInterop; + +namespace CleanArchitecture.Blazor.Server.UI.Services.JsInterop; + +public class BlazorDownloadFileService +{ + private readonly IJSRuntime _jsRuntime; + private IJSObjectReference? _module; + + public BlazorDownloadFileService(IJSRuntime jsRuntime) + { + _jsRuntime = jsRuntime; + } + + /// + /// Downloads a file by invoking a JavaScript function. + /// + /// The file name including extension. + /// The file data as a byte array. + /// The MIME type of the file. + public async Task DownloadFileAsync(string fileName, byte[] data, string contentType) + { + // Load the JavaScript module from wwwroot/js/downloadFile.js if not already loaded + if (_module == null) + { + _module = await _jsRuntime.InvokeAsync("import", "./js/downloadFile.js"); + } + + // Convert the byte array to a Base64 string + string base64Data = Convert.ToBase64String(data); + + // Call the JavaScript function to trigger the file download + await _module.InvokeVoidAsync("downloadFile", fileName, base64Data, contentType); + } +} diff --git a/src/Server.UI/_Imports.razor b/src/Server.UI/_Imports.razor index 37cd98816..fcac73ba4 100644 --- a/src/Server.UI/_Imports.razor +++ b/src/Server.UI/_Imports.razor @@ -51,6 +51,7 @@ @using CleanArchitecture.Blazor.Server.UI.Components.Shared.Layout @using CleanArchitecture.Blazor.Server.UI.Services.Layout @using CleanArchitecture.Blazor.Server.UI.Extensions +@using CleanArchitecture.Blazor.Server.UI.Services.JsInterop @using Severity=MudBlazor.Severity @using Color = MudBlazor.Color @inject IApplicationSettings ApplicationSettings diff --git a/src/Server.UI/wwwroot/js/downloadFile.js b/src/Server.UI/wwwroot/js/downloadFile.js new file mode 100644 index 000000000..022f811cc --- /dev/null +++ b/src/Server.UI/wwwroot/js/downloadFile.js @@ -0,0 +1,12 @@ +export function downloadFile(fileName, byteBase64, contentType) { + // Create a data URL using the MIME type and the Base64 encoded string + const url = `data:${contentType};base64,${byteBase64}`; + const link = document.createElement('a'); + link.href = url; + link.download = fileName; + + // Append the link to the document, trigger a click, and remove the link afterward + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} From 16a385aa1e7266fd51c7f7f40a474f61d450824a Mon Sep 17 00:00:00 2001 From: Zhu Date: Thu, 13 Mar 2025 13:41:50 +0800 Subject: [PATCH 3/4] clean --- .../Features/Contacts/Commands/Create/CreateContactCommand.cs | 1 - .../Features/Contacts/Commands/Update/UpdateContactCommand.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Application/Features/Contacts/Commands/Create/CreateContactCommand.cs b/src/Application/Features/Contacts/Commands/Create/CreateContactCommand.cs index a4dc1125c..f585dbb3b 100644 --- a/src/Application/Features/Contacts/Commands/Create/CreateContactCommand.cs +++ b/src/Application/Features/Contacts/Commands/Create/CreateContactCommand.cs @@ -26,7 +26,6 @@ // events for integration with other bounded contexts in the application. using CleanArchitecture.Blazor.Application.Features.Contacts.Caching; -using CleanArchitecture.Blazor.Application.Features.Contacts.Commands.AddEdit; using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs; namespace CleanArchitecture.Blazor.Application.Features.Contacts.Commands.Create; diff --git a/src/Application/Features/Contacts/Commands/Update/UpdateContactCommand.cs b/src/Application/Features/Contacts/Commands/Update/UpdateContactCommand.cs index 6b75508cc..c4eaa16a5 100644 --- a/src/Application/Features/Contacts/Commands/Update/UpdateContactCommand.cs +++ b/src/Application/Features/Contacts/Commands/Update/UpdateContactCommand.cs @@ -26,7 +26,6 @@ // invalidated to keep the contact list consistent. using CleanArchitecture.Blazor.Application.Features.Contacts.Caching; -using CleanArchitecture.Blazor.Application.Features.Contacts.Commands.Create; namespace CleanArchitecture.Blazor.Application.Features.Contacts.Commands.Update; From 0a6677b77d3fb7d9dfd01b95a5edb39e6beaebd0 Mon Sep 17 00:00:00 2001 From: Zhu Date: Thu, 13 Mar 2025 13:59:14 +0800 Subject: [PATCH 4/4] test code generator --- .../Contacts/Caching/ContactCacheKey.cs | 4 +- .../Commands/AddEdit/AddEditContactCommand.cs | 57 +++--- .../AddEdit/AddEditContactCommandValidator.cs | 25 +-- .../Commands/Create/CreateContactCommand.cs | 93 ++++----- .../Create/CreateContactCommandValidator.cs | 26 +-- .../Commands/Delete/DeleteContactCommand.cs | 30 +-- .../Delete/DeleteContactCommandValidator.cs | 26 +-- .../Commands/Import/ImportContactsCommand.cs | 43 ++--- .../Import/ImportContactsCommandValidator.cs | 28 +-- .../Commands/Update/UpdateContactCommand.cs | 43 ++--- .../Update/UpdateContactCommandValidator.cs | 31 +-- .../Features/Contacts/DTOs/ContactDto.cs | 49 ++--- .../ContactCreatedEventHandler.cs | 18 +- .../ContactDeletedEventHandler.cs | 18 +- .../ContactUpdatedEventHandler.cs | 18 +- .../Queries/Export/ExportContactsQuery.cs | 15 +- .../Queries/GetAll/GetAllContactsQuery.cs | 7 +- .../Queries/GetById/GetContactByIdQuery.cs | 13 +- .../Pagination/ContactsPaginationQuery.cs | 45 +++-- .../Specifications/ContactAdvancedFilter.cs | 4 +- .../ContactAdvancedSpecification.cs | 4 +- .../ContactByIdSpecification.cs | 4 +- src/Server.UI/Pages/Contacts/Contacts.razor | 180 +++++++++--------- .../Pages/Contacts/CreateContact.razor | 3 +- .../Pages/Contacts/EditContact.razor | 5 +- 25 files changed, 320 insertions(+), 469 deletions(-) diff --git a/src/Application/Features/Contacts/Caching/ContactCacheKey.cs b/src/Application/Features/Contacts/Caching/ContactCacheKey.cs index eb2d58a4f..f4b03d622 100644 --- a/src/Application/Features/Contacts/Caching/ContactCacheKey.cs +++ b/src/Application/Features/Contacts/Caching/ContactCacheKey.cs @@ -5,8 +5,8 @@ // See the LICENSE file in the project root for more information. // // Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 +// Created Date: 2025-03-13 +// Last Modified: 2025-03-13 // Description: // Defines static methods and properties for managing cache keys and expiration // settings for Contact-related data. This includes creating unique cache keys for diff --git a/src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs b/src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs index 6a06d9c82..f9a6e499e 100644 --- a/src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs +++ b/src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs @@ -1,52 +1,39 @@ //------------------------------------------------------------------------------ // -// This file is part of the CleanArchitecture.Blazor project. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -// -// Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 -// Description: -// This file defines the command for adding or editing a contact entity, -// including validation and mapping operations. It handles domain events -// and cache invalidation for updated or newly created contact. -// -// Documentation: -// https://docs.cleanarchitectureblazor.com/features/contact +// CleanArchitecture.Blazor - MIT Licensed. +// Author: neozhu +// Created/Modified: 2025-03-13 +// Command for adding/editing a contact entity with validation, mapping, +// domain events, and cache invalidation. +// Documentation: https://docs.cleanarchitectureblazor.com/features/contact // //------------------------------------------------------------------------------ -// Usage: -// This command can be used to add a new contact or edit an existing one. -// It handles caching logic and domain event raising automatically. +// Usage: Use this command to add or edit a contact. Caching and domain event handling are automatic. using CleanArchitecture.Blazor.Application.Features.Contacts.Caching; using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs; - namespace CleanArchitecture.Blazor.Application.Features.Contacts.Commands.AddEdit; -public class AddEditContactCommand : ICacheInvalidatorRequest> +public class AddEditContactCommand: ICacheInvalidatorRequest> { - [Description("Id")] - public int Id { get; set; } - [Description("Name")] - public string Name { get; set; } + [Description("Id")] + public int Id { get; set; } + [Description("Name")] + public string Name {get;set;} [Description("Description")] - public string? Description { get; set; } + public string? Description {get;set;} [Description("Email")] - public string? Email { get; set; } + public string? Email {get;set;} [Description("Phone number")] - public string? PhoneNumber { get; set; } + public string? PhoneNumber {get;set;} [Description("Country")] - public string? Country { get; set; } - + public string? Country {get;set;} - public string CacheKey => ContactCacheKey.GetAllCacheKey; - public IEnumerable? Tags => ContactCacheKey.Tags; + public string CacheKey => ContactCacheKey.GetAllCacheKey; + public IEnumerable? Tags => ContactCacheKey.Tags; private class Mapping : Profile { public Mapping() @@ -78,8 +65,8 @@ public async Task> Handle(AddEditContactCommand request, Cancellatio return await Result.FailureAsync($"Contact with id: [{request.Id}] not found."); } item = _mapper.Map(request, item); - // raise a update domain event - item.AddDomainEvent(new ContactUpdatedEvent(item)); + // raise a update domain event + item.AddDomainEvent(new ContactUpdatedEvent(item)); await _context.SaveChangesAsync(cancellationToken); return await Result.SuccessAsync(item.Id); } @@ -87,12 +74,12 @@ public async Task> Handle(AddEditContactCommand request, Cancellatio { var item = _mapper.Map(request); // raise a create domain event - item.AddDomainEvent(new ContactCreatedEvent(item)); + item.AddDomainEvent(new ContactCreatedEvent(item)); _context.Contacts.Add(item); await _context.SaveChangesAsync(cancellationToken); return await Result.SuccessAsync(item.Id); } - + } } diff --git a/src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommandValidator.cs b/src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommandValidator.cs index c2dcbc7e9..64612a456 100644 --- a/src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommandValidator.cs +++ b/src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommandValidator.cs @@ -1,27 +1,16 @@ //------------------------------------------------------------------------------ // -// This file is part of the CleanArchitecture.Blazor project. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -// -// Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 -// Description: -// This file defines the validation rules for the AddEditContactCommand -// used to add or edit Contact entities within the CleanArchitecture.Blazor -// application. It enforces maximum field lengths and required properties -// to maintain data integrity and validation standards. -// -// Documentation: -// https://docs.cleanarchitectureblazor.com/features/contact +// CleanArchitecture.Blazor - MIT Licensed. +// Author: neozhu +// Created/Modified: 2025-03-13 +// Validator for AddEditContactCommand: enforces field length and required property rules for Contact entities. +// Docs: https://docs.cleanarchitectureblazor.com/features/contact // //------------------------------------------------------------------------------ // Usage: -// This validator enforces constraints on the AddEditContactCommand, such as -// maximum field length for ... +// Validates AddEditContactCommand constraints (e.g., maximum field lengths). + namespace CleanArchitecture.Blazor.Application.Features.Contacts.Commands.AddEdit; diff --git a/src/Application/Features/Contacts/Commands/Create/CreateContactCommand.cs b/src/Application/Features/Contacts/Commands/Create/CreateContactCommand.cs index f585dbb3b..14c6faffb 100644 --- a/src/Application/Features/Contacts/Commands/Create/CreateContactCommand.cs +++ b/src/Application/Features/Contacts/Commands/Create/CreateContactCommand.cs @@ -1,53 +1,40 @@ //------------------------------------------------------------------------------ // -// This file is part of the CleanArchitecture.Blazor project. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -// -// Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 -// Description: -// This file defines the command and its handler for creating a new Contact entity -// within the CleanArchitecture.Blazor application. The command uses caching -// invalidation to ensure data consistency and raises domain events to maintain -// the integrity of the entity lifecycle. It leverages Clean Architecture principles -// for separation of concerns and encapsulation. -// -// Documentation: -// https://docs.cleanarchitectureblazor.com/features/contact +// CleanArchitecture.Blazor - MIT Licensed. +// Author: neozhu +// Created/Modified: 2025-03-13 +// Command and handler for creating a new Contact. +// Uses caching invalidation and domain events for data consistency. +// Docs: https://docs.cleanarchitectureblazor.com/features/contact // //------------------------------------------------------------------------------ // Usage: -// This command can be used to create a new contact entity in the system. It includes -// the required fields for the contact and automatically raises necessary domain -// events for integration with other bounded contexts in the application. +// Use this command to create a new contact with required fields and automatic domain event handling. + using CleanArchitecture.Blazor.Application.Features.Contacts.Caching; -using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs; namespace CleanArchitecture.Blazor.Application.Features.Contacts.Commands.Create; -public class CreateContactCommand : ICacheInvalidatorRequest> +public class CreateContactCommand: ICacheInvalidatorRequest> { - [Description("Id")] - public int Id { get; set; } - [Description("Name")] - public string Name { get; set; } + [Description("Id")] + public int Id { get; set; } + [Description("Name")] + public string Name {get;set;} [Description("Description")] - public string? Description { get; set; } + public string? Description {get;set;} [Description("Email")] - public string? Email { get; set; } + public string? Email {get;set;} [Description("Phone number")] - public string? PhoneNumber { get; set; } + public string? PhoneNumber {get;set;} [Description("Country")] - public string? Country { get; set; } + public string? Country {get;set;} - public string CacheKey => ContactCacheKey.GetAllCacheKey; - public IEnumerable? Tags => ContactCacheKey.Tags; - private class Mapping : Profile + public string CacheKey => ContactCacheKey.GetAllCacheKey; + public IEnumerable? Tags => ContactCacheKey.Tags; + private class Mapping : Profile { public Mapping() { @@ -55,26 +42,26 @@ public Mapping() } } } - -public class CreateContactCommandHandler : IRequestHandler> -{ - private readonly IMapper _mapper; - private readonly IApplicationDbContext _context; - public CreateContactCommandHandler( - IMapper mapper, - IApplicationDbContext context) - { - _mapper = mapper; - _context = context; - } - public async Task> Handle(CreateContactCommand request, CancellationToken cancellationToken) + + public class CreateContactCommandHandler : IRequestHandler> { - var item = _mapper.Map(request); - // raise a create domain event - item.AddDomainEvent(new ContactCreatedEvent(item)); - _context.Contacts.Add(item); - await _context.SaveChangesAsync(cancellationToken); - return await Result.SuccessAsync(item.Id); + private readonly IMapper _mapper; + private readonly IApplicationDbContext _context; + public CreateContactCommandHandler( + IMapper mapper, + IApplicationDbContext context) + { + _mapper = mapper; + _context = context; + } + public async Task> Handle(CreateContactCommand request, CancellationToken cancellationToken) + { + var item = _mapper.Map(request); + // raise a create domain event + item.AddDomainEvent(new ContactCreatedEvent(item)); + _context.Contacts.Add(item); + await _context.SaveChangesAsync(cancellationToken); + return await Result.SuccessAsync(item.Id); + } } -} diff --git a/src/Application/Features/Contacts/Commands/Create/CreateContactCommandValidator.cs b/src/Application/Features/Contacts/Commands/Create/CreateContactCommandValidator.cs index 5d423379a..72fa93709 100644 --- a/src/Application/Features/Contacts/Commands/Create/CreateContactCommandValidator.cs +++ b/src/Application/Features/Contacts/Commands/Create/CreateContactCommandValidator.cs @@ -1,28 +1,16 @@ //------------------------------------------------------------------------------ // -// This file is part of the CleanArchitecture.Blazor project. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -// -// Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 -// Description: -// This file defines the validation rules for the CreateContactCommand, -// used to create Contact entities within the CleanArchitecture.Blazor -// application. It ensures that essential fields are validated correctly, -// including maximum lengths and mandatory requirements for required fields. -// -// Documentation: -// https://docs.cleanarchitectureblazor.com/features/contact +// CleanArchitecture.Blazor - MIT Licensed. +// Author: neozhu +// Created/Modified: 2025-03-13 +// Validator for CreateContactCommand: enforces max lengths and required fields for Contact entities. +// Docs: https://docs.cleanarchitectureblazor.com/features/contact // //------------------------------------------------------------------------------ // Usage: -// This validator is used to ensure that a CreateContactCommand meets the required -// validation criteria. It enforces constraints like maximum field lengths and -// ensures that the Name field is not empty before proceeding with the command execution. +// Validates CreateContactCommand ensuring constraints (e.g., non-empty Name field) before execution. + namespace CleanArchitecture.Blazor.Application.Features.Contacts.Commands.Create; diff --git a/src/Application/Features/Contacts/Commands/Delete/DeleteContactCommand.cs b/src/Application/Features/Contacts/Commands/Delete/DeleteContactCommand.cs index 4fda2fd3a..8ca75b7f2 100644 --- a/src/Application/Features/Contacts/Commands/Delete/DeleteContactCommand.cs +++ b/src/Application/Features/Contacts/Commands/Delete/DeleteContactCommand.cs @@ -1,30 +1,18 @@ //------------------------------------------------------------------------------ // -// This file is part of the CleanArchitecture.Blazor project. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -// -// Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 -// Description: -// This file defines the command and its handler for deleting one or more -// Contact entities from the CleanArchitecture.Blazor application. It -// implements caching invalidation logic to ensure that data consistency is -// maintained. Domain events are triggered for deleted entities to support -// integration with other parts of the system. -// for separation of concerns and encapsulation. -// -// Documentation: -// https://docs.cleanarchitectureblazor.com/features/contact +// CleanArchitecture.Blazor - MIT Licensed. +// Author: neozhu +// Created/Modified: 2025-03-13 +// Command and handler for deleting Contact entities. +// Implements cache invalidation and triggers domain events. +// Docs: https://docs.cleanarchitectureblazor.com/features/contact // //------------------------------------------------------------------------------ // Usage: -// This command can be used to delete multiple Contacts from the system by specifying -// their IDs. The handler also raises domain events for each deleted contact to -// notify other bounded contexts and invalidate relevant cache entries. +// Delete multiple Contacts by specifying their IDs. +// Domain events are raised for each deletion to support cache invalidation. + using CleanArchitecture.Blazor.Application.Features.Contacts.Caching; diff --git a/src/Application/Features/Contacts/Commands/Delete/DeleteContactCommandValidator.cs b/src/Application/Features/Contacts/Commands/Delete/DeleteContactCommandValidator.cs index c9c39b918..40ba72e4c 100644 --- a/src/Application/Features/Contacts/Commands/Delete/DeleteContactCommandValidator.cs +++ b/src/Application/Features/Contacts/Commands/Delete/DeleteContactCommandValidator.cs @@ -1,28 +1,16 @@ //------------------------------------------------------------------------------ // -// This file is part of the CleanArchitecture.Blazor project. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -// -// Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 -// Description: -// This file defines the validation rules for the DeleteContactCommand used -// to delete Contact entities within the CleanArchitecture.Blazor application. -// It ensures that the command has valid input, particularly verifying that the -// list of contact IDs to delete is not null and contains only positive IDs. -// -// Documentation: -// https://docs.cleanarchitectureblazor.com/features/contact +// CleanArchitecture.Blazor - MIT Licensed. +// Author: neozhu +// Created/Modified: 2025-03-13 +// Validator for DeleteContactCommand: ensures the ID list for contact is not null and contains only positive IDs. +// Docs: https://docs.cleanarchitectureblazor.com/features/contact // //------------------------------------------------------------------------------ // Usage: -// This validator ensures that the DeleteContactCommand is valid before attempting -// to delete contact records from the system. It verifies that the ID list is not -// null and that all IDs are greater than zero. +// Validates DeleteContactCommand by checking that the ID list is non-null and all IDs are > 0. + namespace CleanArchitecture.Blazor.Application.Features.Contacts.Commands.Delete; diff --git a/src/Application/Features/Contacts/Commands/Import/ImportContactsCommand.cs b/src/Application/Features/Contacts/Commands/Import/ImportContactsCommand.cs index 277bb15c5..cd2b20b21 100644 --- a/src/Application/Features/Contacts/Commands/Import/ImportContactsCommand.cs +++ b/src/Application/Features/Contacts/Commands/Import/ImportContactsCommand.cs @@ -1,30 +1,18 @@ //------------------------------------------------------------------------------ // -// This file is part of the CleanArchitecture.Blazor project. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -// -// Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 -// Description: -// This file defines the command, handler, and associated logic for importing -// contacts from an Excel file into the CleanArchitecture.Blazor application. -// The import process supports validating data and ensuring no duplicates are -// inserted. Additionally, a command for creating a contact template file is provided -// to facilitate bulk data entry for end users. -// -// Documentation: -// https://docs.cleanarchitectureblazor.com/features/contact +// CleanArchitecture.Blazor - MIT Licensed. +// Author: neozhu +// Created/Modified: 2025-03-13 +// Import command & template for contacts. +// Validates Excel data, prevents duplicates, and provides a template for bulk entry. +// Docs: https://docs.cleanarchitectureblazor.com/features/contact // //------------------------------------------------------------------------------ - +// // Usage: -// - Use `ImportContactsCommand` to import contacts from an Excel file, ensuring proper validation -// and avoiding duplicates. -// - Use `CreateContactsTemplateCommand` to generate an Excel template for entering contact data -// that can be later imported using the import command. +// - Use `ImportContactsCommand` to import contacts from Excel. +// - Use `CreateContactsTemplateCommand` to generate an Excel template for contact data. + using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs; using CleanArchitecture.Blazor.Application.Features.Contacts.Caching; @@ -55,20 +43,19 @@ public class ImportContactsCommandHandler : private readonly IApplicationDbContext _context; private readonly IStringLocalizer _localizer; private readonly IExcelService _excelService; - private readonly IMapper _mapper; - private readonly ContactDto _dto = new(); - + private readonly ContactDto _dto = new(); + private readonly IMapper _mapper; public ImportContactsCommandHandler( IApplicationDbContext context, - IExcelService excelService, IMapper mapper, + IExcelService excelService, IStringLocalizer localizer) { _context = context; _localizer = localizer; _excelService = excelService; - _mapper = mapper; - } + _mapper = mapper; + } #nullable disable warnings public async Task> Handle(ImportContactsCommand request, CancellationToken cancellationToken) { diff --git a/src/Application/Features/Contacts/Commands/Import/ImportContactsCommandValidator.cs b/src/Application/Features/Contacts/Commands/Import/ImportContactsCommandValidator.cs index 879953de0..7227a2acd 100644 --- a/src/Application/Features/Contacts/Commands/Import/ImportContactsCommandValidator.cs +++ b/src/Application/Features/Contacts/Commands/Import/ImportContactsCommandValidator.cs @@ -1,28 +1,16 @@ //------------------------------------------------------------------------------ // -// This file is part of the CleanArchitecture.Blazor project. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -// -// Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 -// Description: -// This file defines the validation rules for the ImportContactsCommand -// within the CleanArchitecture.Blazor application. It ensures that the -// command's required properties are correctly set before proceeding with -// the contact import process. -// -// Documentation: -// https://docs.cleanarchitectureblazor.com/features/contact +// CleanArchitecture.Blazor - MIT Licensed. +// Author: neozhu +// Created/Modified: 2025-03-13 +// Validator for ImportContactsCommand: ensures the Data property is non-null and non-empty for contact import. +// Docs: https://docs.cleanarchitectureblazor.com/features/contact // //------------------------------------------------------------------------------ - +// // Usage: -// This validator is used to ensure that an ImportContactsCommand has valid input -// before attempting to import contact data. It checks that the Data property is not -// null and is not empty, ensuring that the command has valid content for import. +// Validates ImportContactsCommand to ensure valid data before import. + namespace CleanArchitecture.Blazor.Application.Features.Contacts.Commands.Import; diff --git a/src/Application/Features/Contacts/Commands/Update/UpdateContactCommand.cs b/src/Application/Features/Contacts/Commands/Update/UpdateContactCommand.cs index c4eaa16a5..dbe4c4399 100644 --- a/src/Application/Features/Contacts/Commands/Update/UpdateContactCommand.cs +++ b/src/Application/Features/Contacts/Commands/Update/UpdateContactCommand.cs @@ -1,31 +1,19 @@ //------------------------------------------------------------------------------ // -// This file is part of the CleanArchitecture.Blazor project. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -// -// Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 -// Description: -// This file defines the UpdateContactCommand and its handler for updating -// an existing Contact entity within the CleanArchitecture.Blazor application. -// It includes caching invalidation logic to maintain data consistency and -// raises a domain event upon successful update to notify other parts of the system. -// -// Documentation: -// https://docs.cleanarchitectureblazor.com/features/contact +// CleanArchitecture.Blazor - MIT Licensed. +// Author: neozhu +// Created/Modified: 2025-03-13 +// UpdateContactCommand & handler: updates an existing Contact with cache invalidation and raises ContactUpdatedEvent. +// Docs: https://docs.cleanarchitectureblazor.com/features/contact // //------------------------------------------------------------------------------ - +// // Usage: -// Use `UpdateContactCommand` to update an existing contact entity in the system. -// The handler ensures that if the entity is found, the changes are applied and -// the necessary domain event (`ContactUpdatedEvent`) is raised. Caching is also -// invalidated to keep the contact list consistent. +// Use UpdateContactCommand to update an existing contact. If found, changes are applied, cache is invalidated, and ContactUpdatedEvent is raised. + using CleanArchitecture.Blazor.Application.Features.Contacts.Caching; +using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs; namespace CleanArchitecture.Blazor.Application.Features.Contacts.Commands.Update; @@ -46,25 +34,28 @@ public class UpdateContactCommand: ICacheInvalidatorRequest> public string CacheKey => ContactCacheKey.GetAllCacheKey; public IEnumerable? Tags => ContactCacheKey.Tags; + private class Mapping : Profile { public Mapping() { CreateMap(MemberList.None); + CreateMap(MemberList.None); } } + } public class UpdateContactCommandHandler : IRequestHandler> { - private readonly IMapper _mapper; private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; public UpdateContactCommandHandler( IMapper mapper, IApplicationDbContext context) { - _mapper = mapper; _context = context; + _mapper = mapper; } public async Task> Handle(UpdateContactCommand request, CancellationToken cancellationToken) { @@ -74,9 +65,9 @@ public async Task> Handle(UpdateContactCommand request, Cancellation { return await Result.FailureAsync($"Contact with id: [{request.Id}] not found."); } - item = _mapper.Map(request, item); - // raise a update domain event - item.AddDomainEvent(new ContactUpdatedEvent(item)); + item = _mapper.Map(request, item); + // raise a update domain event + item.AddDomainEvent(new ContactUpdatedEvent(item)); await _context.SaveChangesAsync(cancellationToken); return await Result.SuccessAsync(item.Id); } diff --git a/src/Application/Features/Contacts/Commands/Update/UpdateContactCommandValidator.cs b/src/Application/Features/Contacts/Commands/Update/UpdateContactCommandValidator.cs index 865f235db..58c4c371d 100644 --- a/src/Application/Features/Contacts/Commands/Update/UpdateContactCommandValidator.cs +++ b/src/Application/Features/Contacts/Commands/Update/UpdateContactCommandValidator.cs @@ -1,31 +1,16 @@ //------------------------------------------------------------------------------ // -// This file is part of the CleanArchitecture.Blazor project. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -// -// Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 -// Description: -// This file defines the validation rules for the UpdateContactCommand within -// the CleanArchitecture.Blazor application. It ensures that the command has -// all required fields properly populated and validates constraints such as -// maximum length and non-null requirements before proceeding with updating -// a contact. -// -// Documentation: -// https://docs.cleanarchitectureblazor.com/features/contact +// CleanArchitecture.Blazor - MIT Licensed. +// Author: neozhu +// Created/Modified: 2025-03-13 +// Validator for UpdateContactCommand: ensures required fields (e.g., Id, non-empty Name, max length for properties) are valid for updating a contact. +// Docs: https://docs.cleanarchitectureblazor.com/features/contact // //------------------------------------------------------------------------------ - +// // Usage: -// The `UpdateContactCommandValidator` is used to validate that an `UpdateContactCommand` -// contains valid and complete data before processing the update. It enforces rules such as -// ensuring that the `Id` is provided, the `Name` is not empty, and certain properties -// (e.g., ...) do not exceed their maximum -// allowed length. +// Validates UpdateContactCommand to ensure complete and valid data before processing the update. + namespace CleanArchitecture.Blazor.Application.Features.Contacts.Commands.Update; diff --git a/src/Application/Features/Contacts/DTOs/ContactDto.cs b/src/Application/Features/Contacts/DTOs/ContactDto.cs index 4ce4ec3d5..3a859b557 100644 --- a/src/Application/Features/Contacts/DTOs/ContactDto.cs +++ b/src/Application/Features/Contacts/DTOs/ContactDto.cs @@ -1,30 +1,16 @@ //------------------------------------------------------------------------------ // -// This file is part of the CleanArchitecture.Blazor project. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -// -// Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 -// Description: -// This file defines the Data Transfer Object (DTO) for the Contact entity -// used within the CleanArchitecture.Blazor application. The ContactDto is -// responsible for transferring data between layers while maintaining the -// structure and format required by application features like commands, queries, -// and views. -// -// Documentation: -// https://docs.cleanarchitectureblazor.com/features/contact +// CleanArchitecture.Blazor - MIT Licensed. +// Author: neozhu +// Created/Modified: 2025-03-13 +// ContactDto: transfers contact data between layers. +// Docs: https://docs.cleanarchitectureblazor.com/features/contact // //------------------------------------------------------------------------------ - +// // Usage: -// The `ContactDto` class is used to represent contact data throughout the CleanArchitecture.Blazor -// application, providing a well-defined contract for passing contact information between different -// layers and services. Each property includes a description for better understandability during -// serialization and documentation generation. +// Use ContactDto to represent contact data across commands, queries, and views. + namespace CleanArchitecture.Blazor.Application.Features.Contacts.DTOs; @@ -33,22 +19,24 @@ public class ContactDto { [Description("Id")] public int Id { get; set; } - [Description("Name")] - public string Name { get; set; } + [Description("Name")] + public string Name {get;set;} [Description("Description")] - public string? Description { get; set; } + public string? Description {get;set;} [Description("Email")] - public string? Email { get; set; } + public string? Email {get;set;} [Description("Phone number")] - public string? PhoneNumber { get; set; } + public string? PhoneNumber {get;set;} [Description("Country")] - public string? Country { get; set; } + public string? Country {get;set;} + + private class Mapping : Profile { public Mapping() { - CreateMap(); - CreateMap() + CreateMap(MemberList.None); + CreateMap(MemberList.None) .ForMember(dest => dest.Created, opt => opt.Ignore()) .ForMember(dest => dest.CreatedBy, opt => opt.Ignore()) .ForMember(dest => dest.LastModified, opt => opt.Ignore()) @@ -56,6 +44,5 @@ public Mapping() .ForMember(dest => dest.DomainEvents, opt => opt.Ignore()); } } - } diff --git a/src/Application/Features/Contacts/EventHandlers/ContactCreatedEventHandler.cs b/src/Application/Features/Contacts/EventHandlers/ContactCreatedEventHandler.cs index 2646e9d28..833de08e8 100644 --- a/src/Application/Features/Contacts/EventHandlers/ContactCreatedEventHandler.cs +++ b/src/Application/Features/Contacts/EventHandlers/ContactCreatedEventHandler.cs @@ -1,20 +1,14 @@ //------------------------------------------------------------------------------ // -// This file is part of the CleanArchitecture.Blazor project. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -// -// Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 -// Description: -// Handles the `ContactCreatedEvent` which occurs when a new contact is created. -// This event handler can be extended to trigger additional actions, such as -// sending notifications or updating other systems. +// CleanArchitecture.Blazor - MIT Licensed. +// Author: neozhu +// Created/Modified: 2025-03-13 +// Handles ContactCreatedEvent: triggered when a new contact is created. +// Extendable for additional actions (e.g., notifications, system updates). // //------------------------------------------------------------------------------ + namespace CleanArchitecture.Blazor.Application.Features.Contacts.EventHandlers; public class ContactCreatedEventHandler : INotificationHandler diff --git a/src/Application/Features/Contacts/EventHandlers/ContactDeletedEventHandler.cs b/src/Application/Features/Contacts/EventHandlers/ContactDeletedEventHandler.cs index 3293d114b..1ae960534 100644 --- a/src/Application/Features/Contacts/EventHandlers/ContactDeletedEventHandler.cs +++ b/src/Application/Features/Contacts/EventHandlers/ContactDeletedEventHandler.cs @@ -1,20 +1,14 @@ //------------------------------------------------------------------------------ // -// This file is part of the CleanArchitecture.Blazor project. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -// -// Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 -// Description: -// Handles the `ContactDeletedEvent` which occurs when a new contact is deleted. -// This event handler can be extended to trigger additional actions, such as -// sending notifications or updating other systems. +// CleanArchitecture.Blazor - MIT Licensed. +// Author: neozhu +// Created/Modified: 2025-03-13 +// Handles ContactDeletedEvent: triggered when a contact is deleted. +// Extendable for additional actions (e.g., notifications, system updates). // //------------------------------------------------------------------------------ + namespace CleanArchitecture.Blazor.Application.Features.Contacts.EventHandlers; public class ContactDeletedEventHandler : INotificationHandler diff --git a/src/Application/Features/Contacts/EventHandlers/ContactUpdatedEventHandler.cs b/src/Application/Features/Contacts/EventHandlers/ContactUpdatedEventHandler.cs index 3fd1a930b..ccdc21023 100644 --- a/src/Application/Features/Contacts/EventHandlers/ContactUpdatedEventHandler.cs +++ b/src/Application/Features/Contacts/EventHandlers/ContactUpdatedEventHandler.cs @@ -1,20 +1,14 @@ //------------------------------------------------------------------------------ // -// This file is part of the CleanArchitecture.Blazor project. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -// -// Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 -// Description: -// Handles the `ContactUpdatedEvent` which occurs when a new contact is updated. -// This event handler can be extended to trigger additional actions, such as -// sending notifications or updating other systems. +// CleanArchitecture.Blazor - MIT Licensed. +// Author: neozhu +// Created/Modified: 2025-03-13 +// Handles ContactUpdatedEvent: triggered when a contact is updated. +// Extendable for additional actions (e.g., notifications, system updates). // //------------------------------------------------------------------------------ + namespace CleanArchitecture.Blazor.Application.Features.Contacts.EventHandlers; public class ContactUpdatedEventHandler : INotificationHandler diff --git a/src/Application/Features/Contacts/Queries/Export/ExportContactsQuery.cs b/src/Application/Features/Contacts/Queries/Export/ExportContactsQuery.cs index 828ba0372..a86b8c9e0 100644 --- a/src/Application/Features/Contacts/Queries/Export/ExportContactsQuery.cs +++ b/src/Application/Features/Contacts/Queries/Export/ExportContactsQuery.cs @@ -5,8 +5,8 @@ // See the LICENSE file in the project root for more information. // // Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 +// Created Date: 2025-03-13 +// Last Modified: 2025-03-13 // Description: // Defines a query to export contact data to an Excel file. This query // applies advanced filtering options and generates an Excel file with @@ -14,9 +14,8 @@ // //------------------------------------------------------------------------------ -using AutoMapper.QueryableExtensions; -using CleanArchitecture.Blazor.Application.Features.Contacts.Caching; using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs; +using CleanArchitecture.Blazor.Application.Features.Contacts.Caching; using CleanArchitecture.Blazor.Application.Features.Contacts.Specifications; namespace CleanArchitecture.Blazor.Application.Features.Contacts.Queries.Export; @@ -35,8 +34,8 @@ public override string ToString() public class ExportContactsQueryHandler : IRequestHandler> { - private readonly IMapper _mapper; - private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + private readonly IApplicationDbContext _context; private readonly IExcelService _excelService; private readonly IStringLocalizer _localizer; private readonly ContactDto _dto = new(); @@ -47,8 +46,8 @@ public ExportContactsQueryHandler( IStringLocalizer localizer ) { - _mapper = mapper; - _context = context; + _mapper = mapper; + _context = context; _excelService = excelService; _localizer = localizer; } diff --git a/src/Application/Features/Contacts/Queries/GetAll/GetAllContactsQuery.cs b/src/Application/Features/Contacts/Queries/GetAll/GetAllContactsQuery.cs index 0158e9e7b..87bdc4963 100644 --- a/src/Application/Features/Contacts/Queries/GetAll/GetAllContactsQuery.cs +++ b/src/Application/Features/Contacts/Queries/GetAll/GetAllContactsQuery.cs @@ -5,8 +5,8 @@ // See the LICENSE file in the project root for more information. // // Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 +// Created Date: 2025-03-13 +// Last Modified: 2025-03-13 // Description: // Defines a query to retrieve all contacts from the database. The result // is cached to improve performance and reduce database load for repeated @@ -28,9 +28,8 @@ public class GetAllContactsQuery : ICacheableRequest> public class GetAllContactsQueryHandler : IRequestHandler> { - private readonly IMapper _mapper; private readonly IApplicationDbContext _context; - + private readonly IMapper _mapper; public GetAllContactsQueryHandler( IMapper mapper, IApplicationDbContext context) diff --git a/src/Application/Features/Contacts/Queries/GetById/GetContactByIdQuery.cs b/src/Application/Features/Contacts/Queries/GetById/GetContactByIdQuery.cs index c316591c8..7baf20f82 100644 --- a/src/Application/Features/Contacts/Queries/GetById/GetContactByIdQuery.cs +++ b/src/Application/Features/Contacts/Queries/GetById/GetContactByIdQuery.cs @@ -5,17 +5,16 @@ // See the LICENSE file in the project root for more information. // // Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 +// Created Date: 2025-03-13 +// Last Modified: 2025-03-13 // Description: // Defines a query to retrieve a contact by its ID. The result is cached // to optimize performance for repeated retrievals of the same contact. // //------------------------------------------------------------------------------ -using AutoMapper.QueryableExtensions; -using CleanArchitecture.Blazor.Application.Features.Contacts.Caching; using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs; +using CleanArchitecture.Blazor.Application.Features.Contacts.Caching; using CleanArchitecture.Blazor.Application.Features.Contacts.Specifications; namespace CleanArchitecture.Blazor.Application.Features.Contacts.Queries.GetById; @@ -30,9 +29,8 @@ public class GetContactByIdQuery : ICacheableRequest> public class GetContactByIdQueryHandler : IRequestHandler> { - private readonly IMapper _mapper; private readonly IApplicationDbContext _context; - + private readonly IMapper _mapper; public GetContactByIdQueryHandler( IMapper mapper, IApplicationDbContext context) @@ -45,8 +43,7 @@ public async Task> Handle(GetContactByIdQuery request, Cancel { var data = await _context.Contacts.ApplySpecification(new ContactByIdSpecification(request.Id)) .ProjectTo(_mapper.ConfigurationProvider) - .FirstAsync(cancellationToken) ?? - throw new NotFoundException($"Contact with id: [{request.Id}] not found."); ; + .FirstAsync(cancellationToken) ?? throw new NotFoundException($"Contact with id: [{request.Id}] not found."); return await Result.SuccessAsync(data); } } diff --git a/src/Application/Features/Contacts/Queries/Pagination/ContactsPaginationQuery.cs b/src/Application/Features/Contacts/Queries/Pagination/ContactsPaginationQuery.cs index c8f41efc0..3a49cb956 100644 --- a/src/Application/Features/Contacts/Queries/Pagination/ContactsPaginationQuery.cs +++ b/src/Application/Features/Contacts/Queries/Pagination/ContactsPaginationQuery.cs @@ -5,8 +5,8 @@ // See the LICENSE file in the project root for more information. // // Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 +// Created Date: 2025-03-13 +// Last Modified: 2025-03-13 // Description: // Defines a query for retrieving contacts with pagination and filtering // options. The result is cached to enhance performance for repeated queries. @@ -29,29 +29,28 @@ public override string ToString() public IEnumerable? Tags => ContactCacheKey.Tags; public ContactAdvancedSpecification Specification => new ContactAdvancedSpecification(this); } - + public class ContactsWithPaginationQueryHandler : IRequestHandler> { - private readonly IMapper _mapper; - private readonly IApplicationDbContext _context; - - public ContactsWithPaginationQueryHandler( - IMapper mapper, - IApplicationDbContext context) - { - _mapper = mapper; - _context = context; - } + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + public ContactsWithPaginationQueryHandler( + IMapper mapper, + IApplicationDbContext context) + { + _mapper = mapper; + _context = context; + } - public async Task> Handle(ContactsWithPaginationQuery request, CancellationToken cancellationToken) - { - var data = await _context.Contacts.OrderBy($"{request.OrderBy} {request.SortDirection}") - .ProjectToPaginatedDataAsync(request.Specification, - request.PageNumber, - request.PageSize, - _mapper.ConfigurationProvider, - cancellationToken); - return data; - } + public async Task> Handle(ContactsWithPaginationQuery request, CancellationToken cancellationToken) + { + var data = await _context.Contacts.OrderBy($"{request.OrderBy} {request.SortDirection}") + .ProjectToPaginatedDataAsync(request.Specification, + request.PageNumber, + request.PageSize, + _mapper.ConfigurationProvider, + cancellationToken); + return data; + } } \ No newline at end of file diff --git a/src/Application/Features/Contacts/Specifications/ContactAdvancedFilter.cs b/src/Application/Features/Contacts/Specifications/ContactAdvancedFilter.cs index 91cdd7cf9..ed4dc5371 100644 --- a/src/Application/Features/Contacts/Specifications/ContactAdvancedFilter.cs +++ b/src/Application/Features/Contacts/Specifications/ContactAdvancedFilter.cs @@ -5,8 +5,8 @@ // See the LICENSE file in the project root for more information. // // Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 +// Created Date: 2025-03-13 +// Last Modified: 2025-03-13 // Description: // Defines the available views for filtering contacts and provides advanced // filtering options for contact lists. This includes pagination and various diff --git a/src/Application/Features/Contacts/Specifications/ContactAdvancedSpecification.cs b/src/Application/Features/Contacts/Specifications/ContactAdvancedSpecification.cs index 1a2d36685..64cdd021a 100644 --- a/src/Application/Features/Contacts/Specifications/ContactAdvancedSpecification.cs +++ b/src/Application/Features/Contacts/Specifications/ContactAdvancedSpecification.cs @@ -5,8 +5,8 @@ // See the LICENSE file in the project root for more information. // // Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 +// Created Date: 2025-03-13 +// Last Modified: 2025-03-13 // Description: // Defines a specification for applying advanced filtering options to the // Contact entity, supporting different views and keyword-based searches. diff --git a/src/Application/Features/Contacts/Specifications/ContactByIdSpecification.cs b/src/Application/Features/Contacts/Specifications/ContactByIdSpecification.cs index 30307f08e..9cb52066f 100644 --- a/src/Application/Features/Contacts/Specifications/ContactByIdSpecification.cs +++ b/src/Application/Features/Contacts/Specifications/ContactByIdSpecification.cs @@ -5,8 +5,8 @@ // See the LICENSE file in the project root for more information. // // Author: neozhu -// Created Date: 2024-11-12 -// Last Modified: 2024-11-12 +// Created Date: 2025-03-13 +// Last Modified: 2025-03-13 // Description: // Defines a specification for filtering a Contact entity by its ID. // diff --git a/src/Server.UI/Pages/Contacts/Contacts.razor b/src/Server.UI/Pages/Contacts/Contacts.razor index 8f34db8f1..62e6684c2 100644 --- a/src/Server.UI/Pages/Contacts/Contacts.razor +++ b/src/Server.UI/Pages/Contacts/Contacts.razor @@ -11,24 +11,27 @@ @using CleanArchitecture.Blazor.Server.UI.Pages.Contacts.Components @inject IStringLocalizer L +@inject BlazorDownloadFileService BlazorDownloadFileService + @attribute [Authorize(Policy = Permissions.Contacts.View)] @Title - - + FixedHeader="true" + FixedFooter="true" + Virtualize="true" + @bind-RowsPerPage="_defaultPageSize" + Height="calc(100vh - 330px)" + Loading="@_loading" + MultiSelection="true" + T="ContactDto" + SelectOnRowClick="false" + RowClick="@(s=>OnView(s.Item))" + @bind-SelectedItems="_selectedItems" + @bind-SelectedItem="_currentDto" + Hover="true" @ref="_table"> + + @@ -96,56 +99,61 @@ - - - - - - @if (_accessRights.Edit || _accessRights.Delete) - { - - @if (_accessRights.Edit) + + + + + + @if (_accessRights.Edit || _accessRights.Delete) { - @ConstantString.Edit + + @if (_accessRights.Edit) + { + @ConstantString.Edit + } + @if (_accessRights.Delete) + { + @ConstantString.Delete + } + } - @if (_accessRights.Delete) + else { - @ConstantString.Delete + + @ConstantString.NoAllowed + } - - } - else - { - - @ConstantString.NoAllowed - - } - - + + + @*TODO: Define the fields that should be displayed in data table*@ + + +
+ @context.Item.Name + @context.Item.Description +
+
+
+ + + - - -
- @context.Item.Name - @context.Item.Description -
-
-
- - - - -
- - - - + + + @ConstantString.NoRecords + + + @ConstantString.Loading + + + +
@@ -167,25 +175,25 @@ private TimeSpan _localTimezoneOffset { get; set; } private ContactsWithPaginationQuery Query { get; set; } = new(); - [Inject] - private BlazorDownloadFileService BlazorDownloadFileService { get; set; } = null!; - private ContactsAccessRights _accessRights = new(); protected override async Task OnInitializedAsync() { Title = L[_currentDto.GetClassDescription()]; - _accessRights = await PermissionService.GetAccessRightsAsync(); + _accessRights = await PermissionService.GetAccessRightsAsync(); } - + private async Task> ServerReload(GridState state) { try { _loading = true; Query.CurrentUser = UserProfile; - Query.OrderBy = state.SortDefinitions.FirstOrDefault()?.SortBy ?? "Id"; - Query.SortDirection = state.SortDefinitions.FirstOrDefault()?.Descending ?? true ? SortDirection.Descending.ToString() : SortDirection.Ascending.ToString(); + var sortDefinition = state.SortDefinitions.FirstOrDefault(); + Query.OrderBy = sortDefinition?.SortBy ?? "Id"; + Query.SortDirection = (sortDefinition != null && sortDefinition.Descending) + ? SortDirection.Descending.ToString() + : SortDirection.Ascending.ToString(); Query.PageNumber = state.Page + 1; Query.PageSize = state.PageSize; Query.LocalTimezoneOffset = _localTimezoneOffset; @@ -213,7 +221,7 @@ { ContactCacheKey.Refresh(); _selectedItems.Clear(); - Query.Keyword = string.Empty; + Query.Keyword = string.Empty; await _table.ReloadServerData(); } private async Task ShowEditFormDialog(string title, AddEditContactCommand command) @@ -223,7 +231,7 @@ { x=>x.model,command }, }; var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true }; - var dialog = await DialogService.ShowAsync(title, parameters, options); + var dialog = DialogService.Show(title, parameters, options); var state = await dialog.Result; if (state != null && !state.Canceled) @@ -246,25 +254,25 @@ var dto = _selectedItems.First(); var command = new AddEditContactCommand() { - Country = dto.Country, - Description = dto.Description, - Email = dto.Email, - Name = dto.Name, - PhoneNumber = dto.PhoneNumber + Name = dto.Name, + Description = dto.Description, + Email = dto.Email, + PhoneNumber = dto.PhoneNumber, + Country = dto.Country, + }; await ShowEditFormDialog(string.Format(ConstantString.CreateAnItem, L["Contact"]), command); } - private Task OnEdit(ContactDto dto) + private async Task OnEdit(ContactDto dto) { - Navigation.NavigateTo($"/pages/Contacts/edit/{dto.Id}"); - return Task.CompletedTask; + Navigation.NavigateTo($"/pages/Contacts/edit/{dto.Id}"); } private async Task OnDelete(ContactDto dto) { var contentText = string.Format(ConstantString.DeleteConfirmation, dto.Name); var command = new DeleteContactCommand(new int[] { dto.Id }); - await DialogServiceHelper.ShowDeleteConfirmationDialogAsync(command, ConstantString.DeleteConfirmationTitle, contentText, async () => + await DialogServiceHelper.ShowDeleteConfirmationDialogAsync(command, ConstantString.DeleteConfirmationTitle, contentText,async () => { await InvokeAsync(async () => { @@ -278,7 +286,7 @@ { var contentText = string.Format(ConstantString.DeleteConfirmWithSelected, _selectedItems.Count); var command = new DeleteContactCommand(_selectedItems.Select(x => x.Id).ToArray()); - await DialogServiceHelper.ShowDeleteConfirmationDialogAsync(command, ConstantString.DeleteConfirmationTitle, contentText, async () => + await DialogServiceHelper.ShowDeleteConfirmationDialogAsync(command, ConstantString.DeleteConfirmationTitle, contentText,async () => { await InvokeAsync(async () => { @@ -292,18 +300,18 @@ { _exporting = true; var request = new ExportContactsQuery() - { - Keyword = Query.Keyword, - CurrentUser = UserProfile, - ListView = Query.ListView, - OrderBy = _table.SortDefinitions.Values.FirstOrDefault()?.SortBy ?? "Id", - SortDirection = (_table.SortDefinitions.Values.FirstOrDefault()?.Descending ?? true) ? SortDirection.Descending.ToString() : SortDirection.Ascending.ToString() - }; + { + Keyword = Query.Keyword, + CurrentUser = UserProfile, + ListView = Query.ListView, + OrderBy = _table.SortDefinitions.Values.FirstOrDefault()?.SortBy ?? "Id", + SortDirection = (_table.SortDefinitions.Values.FirstOrDefault()?.Descending ?? true) ? SortDirection.Descending.ToString() : SortDirection.Ascending.ToString() + }; var result = await Mediator.Send(request); await result.MatchAsync( async data => { - await BlazorDownloadFileService.DownloadFileAsync($"{L["Contacts"]}.xlsx", result.Data, contentType: "application/octet-stream"); + await BlazorDownloadFileService.DownloadFileAsync($"{L["Contacts"]}.xlsx", result.Data, contentType:"application/octet-stream"); Snackbar.Add($"{ConstantString.ExportSuccess}", MudBlazor.Severity.Info); return data; }, diff --git a/src/Server.UI/Pages/Contacts/CreateContact.razor b/src/Server.UI/Pages/Contacts/CreateContact.razor index 70c6292c6..84c6a42d0 100644 --- a/src/Server.UI/Pages/Contacts/CreateContact.razor +++ b/src/Server.UI/Pages/Contacts/CreateContact.razor @@ -5,6 +5,7 @@ @inject IValidationService Validator @inject IStringLocalizer L @attribute [Authorize(Policy = Permissions.Contacts.Create)] + @Title @@ -37,7 +38,7 @@ - @ConstantString.Save + @ConstantString.Save diff --git a/src/Server.UI/Pages/Contacts/EditContact.razor b/src/Server.UI/Pages/Contacts/EditContact.razor index 0869a4e4c..34d03b75d 100644 --- a/src/Server.UI/Pages/Contacts/EditContact.razor +++ b/src/Server.UI/Pages/Contacts/EditContact.razor @@ -7,6 +7,7 @@ @inject IValidationService Validator @inject IStringLocalizer L @attribute [Authorize(Policy = Permissions.Contacts.Edit)] + @Title @@ -42,7 +43,7 @@ - @ConstantString.Save + @ConstantString.Save } @@ -88,7 +89,7 @@ await _form!.Validate().ConfigureAwait(false); if (!_form!.IsValid) return; - var result = await Mediator.Send(model!); + var result = await Mediator.Send(model); result.Match( data=> {