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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/Application/Application.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<LangVersion>default</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageReference Include="Ardalis.Specification" Version="8.0.0" />
<PackageReference Include="Ardalis.Specification.EntityFrameworkCore" Version="8.0.0" />
Expand All @@ -17,10 +18,9 @@
<PackageReference Include="jcamp.FluentEmail.Razor" Version="3.8.0" />
<PackageReference Include="FluentValidation" Version="12.0.0-preview1" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0-preview1" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.2" />
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="9.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="9.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Riok.Mapperly" Version="4.2.0-next.0" />
<PackageReference Include="Hangfire.Core" Version="1.8.18" />
<PackageReference Include="ZiggyCreatures.FusionCache" Version="2.1.0" />
<PackageReference Include="ActualLab.Fusion" Version="9.8.46" />
Expand Down
30 changes: 30 additions & 0 deletions src/Application/Common/Extensions/QueryableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -29,6 +30,35 @@ public static IQueryable<T> ApplySpecification<T>(this IQueryable<T> query, ISpe
return SpecificationEvaluator.Default.GetQuery(query, spec, evaluateCriteriaOnly);
}

/// <summary>
/// Extension method to provided ordered queryable data to a paginated result set.
/// </summary>
/// <remarks>
/// This method will apply the given specification to the query, paginate the results, and project them to the desired
/// result type.
/// </remarks>
/// <typeparam name="T">Source type of the entities in the query</typeparam>
/// <typeparam name="TResult">Destination type to which the entities should be projected</typeparam>
/// <param name="query">The original ordered query to project and paginate</param>
/// <param name="spec">The specification to apply to the query before projection and pagination</param>
/// <param name="pageNumber">The desired page number of the paginated results</param>
/// <param name="pageSize">The number of items per page in the paginated results</param>
/// <param name="configuration">Configuration for the projection</param>
/// <param name="cancellationToken">Optional cancellation token to cancel the operation</param>
/// <returns>The paginated and projected data</returns>
public static async Task<PaginatedData<TResult>> ProjectToPaginatedDataAsync<T, TResult>(
this IOrderedQueryable<T> query, ISpecification<T> 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<TResult>(configuration)
.ToListAsync(cancellationToken);
return new PaginatedData<TResult>(data, count, pageNumber, pageSize);
}

/// <summary>
/// Projects the query to a paginated data asynchronously.
/// </summary>
Expand Down
5 changes: 0 additions & 5 deletions src/Application/Common/Security/UserProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,3 @@ public class UserProfile
: TimeZoneInfo.FindSystemTimeZoneById(TimeZoneId).BaseUtcOffset;
}

[Mapper]
public static partial class UserProfileMapper
{
public static partial ChangeUserProfileModel ToChangeUserProfileModel(UserProfile entity);
}
11 changes: 8 additions & 3 deletions src/Application/Common/Security/UserProfileStateService.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,13 +12,17 @@ public class UserProfileStateService
private TimeSpan RefreshInterval => TimeSpan.FromSeconds(60);
private UserProfile _userProfile = new UserProfile() { Email="", UserId="", UserName="" };
private readonly UserManager<ApplicationUser> _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<UserManager<ApplicationUser>>();
_mapper = mapper;
_fusionCache = fusionCache;

}
Expand All @@ -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<ApplicationUserDto>(_mapper.ConfigurationProvider)
.FirstOrDefaultAsync(), RefreshInterval);
if(result is not null)
{
Expand Down
1 change: 1 addition & 0 deletions src/Application/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public static class DependencyInjection
{
public static IServiceCollection AddApplication(this IServiceCollection services)
{
services.AddAutoMapper(Assembly.GetExecutingAssembly());
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
services.AddTransient(typeof(IRequestExceptionHandler<,,>), typeof(DbExceptionHandler<,,>));
services.AddMediatR(config =>
Expand Down
13 changes: 13 additions & 0 deletions src/Application/Features/AuditTrails/DTOs/AuditTrailDto.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -26,4 +28,15 @@ public class AuditTrailDto
[Description("Is Successful")]
public bool IsSuccessful=> string.IsNullOrEmpty(ErrorMessage);


private class Mapping : Profile
{
public Mapping()
{
CreateMap<AuditTrail, AuditTrailDto>(MemberList.None)
.ForMember(x => x.PrimaryKey,
s => s.MapFrom(y => JsonSerializer.Serialize(y.PrimaryKey, DefaultJsonSerializerOptions.Options)));
}
}

}
19 changes: 0 additions & 19 deletions src/Application/Features/AuditTrails/Mappers/AuditTrailMapper.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -18,16 +18,19 @@ public class ExportAuditTrailsQueryHandler :
{
private readonly IApplicationDbContext _context;
private readonly IExcelService _excelService;
private readonly IMapper _mapper;
private readonly IStringLocalizer<ExportAuditTrailsQueryHandler> _localizer;

public ExportAuditTrailsQueryHandler(
IApplicationDbContext context,
IExcelService excelService,
IMapper mapper,
IStringLocalizer<ExportAuditTrailsQueryHandler> localizer
)
{
_context = context;
_excelService = excelService;
_mapper = mapper;
_localizer = localizer;
}

Expand All @@ -36,7 +39,7 @@ public async Task<byte[]> Handle(ExportAuditTrailsQuery request, CancellationTok
var data = await _context.AuditTrails
.Where(x => x.TableName!.Contains(request.Keyword))
.OrderBy($"{request.OrderBy} {request.SortDirection}")
.ProjectTo()
.ProjectTo<AuditTrailDto>(_mapper.ConfigurationProvider)
.ToListAsync(cancellationToken);
var result = await _excelService.ExportAsync(data,
new Dictionary<string, Func<AuditTrailDto, object?>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,20 +23,23 @@ public override string ToString()
public class AuditTrailsQueryHandler : IRequestHandler<AuditTrailsWithPaginationQuery, PaginatedData<AuditTrailDto>>
{
private readonly IApplicationDbContext _context;
private readonly IMapper _mapper;

public AuditTrailsQueryHandler(
IApplicationDbContext context
IApplicationDbContext context,
IMapper mapper
)
{
_context = context;
_mapper = mapper;
}

public async Task<PaginatedData<AuditTrailDto>> 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<AuditTrail, AuditTrailDto>(request.Specification, request.PageNumber,
request.PageSize, _mapper.ConfigurationProvider, cancellationToken);

return data;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Application/Features/Contacts/Caching/ContactCacheKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -35,7 +35,7 @@
public static string GetByIdCacheKey(string parameters) {
return $"ContactCacheKey:GetByIdCacheKey,{parameters}";
}
public static IEnumerable<string>? Tags => new string[] { "contact" };

Check warning on line 38 in src/Application/Features/Contacts/Caching/ContactCacheKey.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

Check warning on line 38 in src/Application/Features/Contacts/Caching/ContactCacheKey.cs

View workflow job for this annotation

GitHub Actions / Analyze

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.
public static void Refresh()
{
FusionCacheFactory.RemoveByTags(Tags);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,19 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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
// </auto-generated>
//------------------------------------------------------------------------------

// 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.Mappers;

using CleanArchitecture.Blazor.Application.Features.Contacts.DTOs;
namespace CleanArchitecture.Blazor.Application.Features.Contacts.Commands.AddEdit;

public class AddEditContactCommand: ICacheInvalidatorRequest<Result<int>>
Expand All @@ -35,25 +23,36 @@
[Description("Name")]
public string Name {get;set;}
[Description("Description")]
public string? Description {get;set;}

Check warning on line 26 in src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

Check warning on line 26 in src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs

View workflow job for this annotation

GitHub Actions / Analyze

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.
[Description("Email")]
public string? Email {get;set;}

Check warning on line 28 in src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

Check warning on line 28 in src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs

View workflow job for this annotation

GitHub Actions / Analyze

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.
[Description("Phone number")]
public string? PhoneNumber {get;set;}

Check warning on line 30 in src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

Check warning on line 30 in src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs

View workflow job for this annotation

GitHub Actions / Analyze

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.
[Description("Country")]
public string? Country {get;set;}

Check warning on line 32 in src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

Check warning on line 32 in src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs

View workflow job for this annotation

GitHub Actions / Analyze

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.


public string CacheKey => ContactCacheKey.GetAllCacheKey;
public IEnumerable<string>? Tags => ContactCacheKey.Tags;

Check warning on line 36 in src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

Check warning on line 36 in src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs

View workflow job for this annotation

GitHub Actions / Analyze

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.
private class Mapping : Profile
{
public Mapping()
{
CreateMap<ContactDto, AddEditContactCommand>(MemberList.None);
CreateMap<AddEditContactCommand, Contact>(MemberList.None);
}
}
}

public class AddEditContactCommandHandler : IRequestHandler<AddEditContactCommand, Result<int>>
{
private readonly IMapper _mapper;
private readonly IApplicationDbContext _context;
public AddEditContactCommandHandler(
IMapper mapper,
IApplicationDbContext context)
{
_mapper = mapper;
_context = context;
}
public async Task<Result<int>> Handle(AddEditContactCommand request, CancellationToken cancellationToken)
Expand All @@ -65,15 +64,15 @@
{
return await Result<int>.FailureAsync($"Contact with id: [{request.Id}] not found.");
}
ContactMapper.ApplyChangesFrom(request,item);
item = _mapper.Map(request, item);
// raise a update domain event
item.AddDomainEvent(new ContactUpdatedEvent(item));
await _context.SaveChangesAsync(cancellationToken);
return await Result<int>.SuccessAsync(item.Id);
}
else
{
var item = ContactMapper.FromEditCommand(request);
var item = _mapper.Map<Contact>(request);
// raise a create domain event
item.AddDomainEvent(new ContactCreatedEvent(item));
_context.Contacts.Add(item);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,16 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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
// </auto-generated>
//------------------------------------------------------------------------------

// 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;

Expand Down
Loading
Loading