Initial code commit.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Contracts.Common
|
||||
{
|
||||
public interface IService
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Common;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Contracts.Repositories
|
||||
{
|
||||
public interface IAsyncRepository<T, in TId> where T : class, IEntity<TId>
|
||||
{
|
||||
IQueryable<T> Entities { get; }
|
||||
|
||||
Task<T?> GetByIdAsync(TId id);
|
||||
|
||||
Task<List<T>> GetAllAsync();
|
||||
|
||||
Task<List<T>> GetPagedResponseAsync(int pageNumber, int pageSize);
|
||||
|
||||
Task<T> AddAsync(T entity);
|
||||
|
||||
Task UpdateAsync(T entity);
|
||||
|
||||
Task DeleteAsync(T entity);
|
||||
Task<int> DeleteByIdAsync(TId id);
|
||||
|
||||
Task<int> CountAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Common;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Contracts.Repositories
|
||||
{
|
||||
public interface IUnitOfWork<TId> : IDisposable
|
||||
{
|
||||
IAsyncRepository<T, TId> Repository<T>() where T : AuditableEntity<TId>;
|
||||
|
||||
Task<int> Commit(CancellationToken cancellationToken);
|
||||
|
||||
Task<int> CommitAndRemoveCache(CancellationToken cancellationToken, params string[] cacheKeys);
|
||||
|
||||
Task Rollback();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Contracts.Services
|
||||
{
|
||||
public interface IAppCache
|
||||
{
|
||||
ValueTask<T> GetOrAddAsync<T>(string key, Func<CancellationToken, ValueTask<T>> factory,
|
||||
IEnumerable<string>? tags = null, TimeSpan? absoluteExpireTime = null);
|
||||
ValueTask RemoveAsync(string key);
|
||||
ValueTask RemoveByTagAsync(string tag);
|
||||
ValueTask ResetAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Common;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Contracts.Services
|
||||
{
|
||||
public interface ICurrentUserService : IService
|
||||
{
|
||||
string? UserId { get; }
|
||||
}
|
||||
}
|
||||
10
Sufi.Demo.PeopleDirectory.Application/Enums/AuditType.cs
Normal file
10
Sufi.Demo.PeopleDirectory.Application/Enums/AuditType.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Enums
|
||||
{
|
||||
public enum AuditType
|
||||
{
|
||||
None = 0,
|
||||
Create = 1,
|
||||
Update = 2,
|
||||
Delete = 3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using FluentValidation;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Extensions
|
||||
{
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddApplicationLayer(this IServiceCollection services, string licenseKey)
|
||||
{
|
||||
services.AddAutoMapper(config =>
|
||||
{
|
||||
config.LicenseKey = licenseKey;
|
||||
config.AddMaps(Assembly.GetExecutingAssembly());
|
||||
});
|
||||
|
||||
services.AddMediatR(config =>
|
||||
{
|
||||
config.LicenseKey = licenseKey;
|
||||
config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
||||
});
|
||||
|
||||
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using AutoMapper;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Repositories;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Services;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Entities.Misc;
|
||||
using Sufi.Demo.PeopleDirectory.Shared.Wrapper;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Commands
|
||||
{
|
||||
public class AddEditContactCommand : IRequest<IResult<int>>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string UserName { get; set; } = null!;
|
||||
public string Phone { get; set; } = Random.Shared.Next(1000000000, 1999999999).ToString();
|
||||
public string Email { get; set; } = "user@example.com";
|
||||
public string SkillSets { get; set; } = "skill1, skill2, skill3";
|
||||
public string Hobby { get; set; } = "Hobby";
|
||||
}
|
||||
|
||||
public sealed class AddEditContactCommandValidator : AbstractValidator<AddEditContactCommand>
|
||||
{
|
||||
public AddEditContactCommandValidator()
|
||||
{
|
||||
RuleFor(v => v.UserName)
|
||||
.NotEmpty().WithMessage("UserName is required.")
|
||||
.MaximumLength(50).WithMessage("UserName must not exceed 50 characters.");
|
||||
RuleFor(v => v.Phone)
|
||||
.NotEmpty().WithMessage("Phone is required.")
|
||||
.MaximumLength(20).WithMessage("Phone must not exceed 20 characters.");
|
||||
RuleFor(v => v.Email)
|
||||
.NotEmpty().WithMessage("Email is required.")
|
||||
.EmailAddress().WithMessage("A valid email is required.")
|
||||
.MaximumLength(100)
|
||||
.WithMessage("Email must not exceed 100 characters.");
|
||||
RuleFor(v => v.SkillSets)
|
||||
.NotEmpty().WithMessage("SkillSets is required.")
|
||||
.MaximumLength(255).WithMessage("SkillSets must not exceed 255 characters.");
|
||||
RuleFor(v => v.Hobby)
|
||||
.NotEmpty().WithMessage("Hobby is required.")
|
||||
.MaximumLength(255).WithMessage("Hobby must not exceed 255 characters.");
|
||||
}
|
||||
}
|
||||
|
||||
public class AddEditContactCommandHandler(
|
||||
IMapper mapper,
|
||||
IUnitOfWork<int> unitOfWork,
|
||||
ILogger<AddEditContactCommandHandler> logger,
|
||||
IAppCache appCache
|
||||
) : IRequestHandler<AddEditContactCommand, IResult<int>>
|
||||
{
|
||||
public async Task<IResult<int>> Handle(AddEditContactCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.Id == 0)
|
||||
{
|
||||
// Only add if max count is not more than 100.
|
||||
var count = await unitOfWork.Repository<Contact>().CountAsync();
|
||||
if (count > 100)
|
||||
{
|
||||
return await Result<int>.FailAsync("Max item count reached. Please delete some first.");
|
||||
}
|
||||
|
||||
var contact = mapper.Map<Contact>(command);
|
||||
await unitOfWork.Repository<Contact>().AddAsync(contact);
|
||||
await unitOfWork.Commit(cancellationToken);
|
||||
|
||||
// Invalidate cache.
|
||||
await appCache.RemoveAsync("contact_all");
|
||||
|
||||
logger.LogInformation("New contact added with ID: {Id}", contact.Id);
|
||||
|
||||
return await Result<int>.SuccessAsync(contact.Id, "New contact saved.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var contact = await unitOfWork.Repository<Contact>().GetByIdAsync(command.Id);
|
||||
if (contact != null)
|
||||
{
|
||||
mapper.Map(command, contact);
|
||||
|
||||
await unitOfWork.Repository<Contact>().UpdateAsync(contact);
|
||||
await unitOfWork.Commit(cancellationToken);
|
||||
|
||||
// Invalidate cache.
|
||||
await appCache.RemoveAsync($"contact_{command.Id}");
|
||||
await appCache.RemoveAsync("contact_all");
|
||||
|
||||
logger.LogInformation("Contact updated with ID: {Id}", contact.Id);
|
||||
|
||||
return await Result<int>.SuccessAsync(contact.Id, "Contact updated.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogWarning("Contact not found with ID: {Id}", command.Id);
|
||||
|
||||
return await Result<int>.FailAsync("Contact not found!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Repositories;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Services;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Entities.Misc;
|
||||
using Sufi.Demo.PeopleDirectory.Shared.Wrapper;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Commands
|
||||
{
|
||||
public class DeleteContactCommand : IRequest<IResult>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
}
|
||||
|
||||
public sealed class DeleteContactCommandValidator : AbstractValidator<DeleteContactCommand>
|
||||
{
|
||||
public DeleteContactCommandValidator()
|
||||
{
|
||||
RuleFor(v => v.Id)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("A valid Id is required.");
|
||||
}
|
||||
}
|
||||
|
||||
public class DeleteContactCommandHandler(
|
||||
IUnitOfWork<int> unitOfWork,
|
||||
ILogger<DeleteContactCommandHandler> logger,
|
||||
IAppCache appCache
|
||||
) : IRequestHandler<DeleteContactCommand, IResult>
|
||||
{
|
||||
public async Task<IResult> Handle(DeleteContactCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var itemToDelete = await unitOfWork.Repository<Contact>().GetByIdAsync(request.Id);
|
||||
if (itemToDelete != null)
|
||||
{
|
||||
await unitOfWork.Repository<Contact>().DeleteByIdAsync(request.Id);
|
||||
|
||||
// Clear cache entries related to contacts.
|
||||
await appCache.RemoveAsync($"contact_{request.Id}");
|
||||
await appCache.RemoveAsync("contact_all");
|
||||
|
||||
logger.LogInformation("Contact with ID: {Id} deleted.", request.Id);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
logger.LogWarning("No contact found with ID: {Id}", request.Id);
|
||||
|
||||
return Result.Fail("No data to delete.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Repositories;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Services;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Entities.Misc;
|
||||
using Sufi.Demo.PeopleDirectory.Shared.Wrapper;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetAll
|
||||
{
|
||||
public class GetAllContactsQuery : IRequest<IResult<List<GetAllContactsResponse>>>
|
||||
{
|
||||
}
|
||||
|
||||
public class GetAllContactsQueryHandler(
|
||||
IUnitOfWork<int> unitOfWork,
|
||||
IMapper mapper,
|
||||
IAppCache appCache
|
||||
) : IRequestHandler<GetAllContactsQuery, IResult<List<GetAllContactsResponse>>>
|
||||
{
|
||||
public async Task<IResult<List<GetAllContactsResponse>>> Handle(GetAllContactsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
Task<List<Contact>> allContactsFunc() => unitOfWork.Repository<Contact>().GetAllAsync();
|
||||
|
||||
var allContacts = await appCache.GetOrAddAsync(
|
||||
"contact_all",
|
||||
async token => await allContactsFunc(),
|
||||
absoluteExpireTime: TimeSpan.FromMinutes(2),
|
||||
tags: ["contacts"]
|
||||
);
|
||||
|
||||
var mappedContacts = mapper.Map<List<GetAllContactsResponse>>(allContacts);
|
||||
return await Result<List<GetAllContactsResponse>>.SuccessAsync(mappedContacts);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetAll
|
||||
{
|
||||
public record GetAllContactsResponse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string UserName { get; set; } = null!;
|
||||
public string Phone { get; set; } = null!;
|
||||
public string Email { get; set; } = null!;
|
||||
public string SkillSets { get; set; } = null!;
|
||||
public string Hobby { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using AutoMapper;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Repositories;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Services;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Entities.Misc;
|
||||
using Sufi.Demo.PeopleDirectory.Shared.Wrapper;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetById
|
||||
{
|
||||
public class GetContactByIdQuery : IRequest<IResult<GetContactByIdResponse>>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
}
|
||||
|
||||
public sealed class GetContactByIdQueryValidator : AbstractValidator<GetContactByIdQuery>
|
||||
{
|
||||
public GetContactByIdQueryValidator()
|
||||
{
|
||||
RuleFor(v => v.Id)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("A valid Id is required.");
|
||||
}
|
||||
}
|
||||
|
||||
public class GetContactByIdQueryHandler(
|
||||
IUnitOfWork<int> unitOfWork,
|
||||
IMapper mapper,
|
||||
IAppCache appCache
|
||||
) : IRequestHandler<GetContactByIdQuery, IResult<GetContactByIdResponse>>
|
||||
{
|
||||
public async Task<IResult<GetContactByIdResponse>> Handle(GetContactByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
Task<Contact?> getContactByIdFunc() => unitOfWork.Repository<Contact>().GetByIdAsync(request.Id);
|
||||
|
||||
var contact = await appCache.GetOrAddAsync(
|
||||
$"contact_{request.Id}",
|
||||
async token => await getContactByIdFunc(),
|
||||
absoluteExpireTime: TimeSpan.FromMinutes(2),
|
||||
tags: ["contacts"]
|
||||
);
|
||||
|
||||
var mappedContact = mapper.Map<GetContactByIdResponse>(contact);
|
||||
return await Result<GetContactByIdResponse>.SuccessAsync(mappedContact);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetById
|
||||
{
|
||||
public record GetContactByIdResponse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string UserName { get; set; } = null!;
|
||||
public string Phone { get; set; } = null!;
|
||||
public string Email { get; set; } = null!;
|
||||
public string SkillSets { get; set; } = null!;
|
||||
public string Hobby { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using AutoMapper;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Commands;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetAll;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetById;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Responses;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Entities.Misc;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Mappings
|
||||
{
|
||||
public class ContactProfile : Profile
|
||||
{
|
||||
public ContactProfile()
|
||||
{
|
||||
CreateMap<AddEditContactCommand, Contact>().ReverseMap();
|
||||
CreateMap<GetAllContactsResponse, Contact>().ReverseMap();
|
||||
CreateMap<Contact, GetContactByIdResponse>();
|
||||
CreateMap<Contact, ContactResponse>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Responses
|
||||
{
|
||||
public class ContactResponse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string UserName { get; set; } = null!;
|
||||
public string Phone { get; set; } = null!;
|
||||
public string Email { get; set; } = null!;
|
||||
public string SkillSets { get; set; } = null!;
|
||||
public string Hobby { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="15.0.1" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0" />
|
||||
<PackageReference Include="MediatR" Version="13.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Sufi.Demo.PeopleDirectory.Domain\Sufi.Demo.PeopleDirectory.Domain.csproj" />
|
||||
<ProjectReference Include="..\Sufi.Demo.PeopleDirectory.Shared\Sufi.Demo.PeopleDirectory.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Features\Boomerang\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user