Merge branch 'master' into fit/master/Hassanzadeh

This commit is contained in:
pns-hassanzadeh
2025-09-06 21:53:11 +03:30
25 changed files with 292 additions and 63 deletions
@@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VizhTalk.BuildingBlocks.Application.Exceptions
{
public class LanguageTitleAlreadyExistsException : BaseException
{
public LanguageTitleAlreadyExistsException() : base("title already exists")
{
}
}
}
@@ -1,7 +1,10 @@
namespace VizhTalk.BuildingBlocks.Application; using System.Transactions;
namespace VizhTalk.BuildingBlocks.Application;
public interface IUnitOfWorkBase : IDisposable public interface IUnitOfWorkBase : IDisposable
{ {
Task<int> CommitAsync( Task BeginTransactionAsync(CancellationToken cancellationToken = default);
CancellationToken cancellationToken = default); Task RollbackTransactionAsync(CancellationToken cancellationToken = default);
Task<int> CommitAsync(CancellationToken cancellationToken = default);
} }
@@ -8,5 +8,6 @@ namespace VizhTalk.BuildingBlocks.Domain.Contract
{ {
public interface IGuardException public interface IGuardException
{ {
public string Message { get; }
} }
} }
@@ -1,9 +0,0 @@
namespace VizhTalk.BuildingBlocks.Domain.Exceptions;
public class ValueObjectValidationException : Exception
{
public ValueObjectValidationException(string message)
:base(message)
{
}
}
@@ -21,7 +21,7 @@ namespace VizhTalk.BuildingBlocks.Infrastructure
this._dbContext = dbContext; this._dbContext = dbContext;
} }
protected DbSet<T> Entity => _dbContext.Set<T>();
public async Task<T> AddAsync(T entity, CancellationToken cancellationToken = default) public async Task<T> AddAsync(T entity, CancellationToken cancellationToken = default)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@@ -1,4 +1,6 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Threading;
using System.Transactions;
using VizhTalk.BuildingBlocks.Application; using VizhTalk.BuildingBlocks.Application;
using VizhTalk.BuildingBlocks.Infrastructure.DomainEventsDispatching; using VizhTalk.BuildingBlocks.Infrastructure.DomainEventsDispatching;
@@ -17,12 +19,24 @@ public class UnitOfWorkBase : IUnitOfWorkBase
this._domainEventsDispatcher = domainEventsDispatcher; this._domainEventsDispatcher = domainEventsDispatcher;
} }
public async Task BeginTransactionAsync(CancellationToken cancellationToken = default)
{
using var tran = await _context.Database.BeginTransactionAsync(cancellationToken);
}
public async Task RollbackTransactionAsync(CancellationToken cancellationToken = default)
=> await _context.Database.RollbackTransactionAsync(cancellationToken);
public async Task<int> CommitAsync( public async Task<int> CommitAsync(
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
await this._domainEventsDispatcher.DispatchEventsAsync(); await this._domainEventsDispatcher.DispatchEventsAsync();
return await _context.SaveChangesAsync(cancellationToken); var num = await _context.SaveChangesAsync(cancellationToken);
if (_context.Database.CurrentTransaction != null)
await _context.Database.CommitTransactionAsync(cancellationToken);
return num;
} }
public void Dispose() public void Dispose()
@@ -30,4 +44,6 @@ public class UnitOfWorkBase : IUnitOfWorkBase
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
_context.Dispose(); _context.Dispose();
} }
} }
+2
View File
@@ -10,6 +10,8 @@ message Id{
message UploadFileDTO{ message UploadFileDTO{
Id mediaGroupMediaId = 1; Id mediaGroupMediaId = 1;
string contentType = 2;
int32 size = 3;
} }
message MediaGroupDTO{ message MediaGroupDTO{
Id id = 1; Id id = 1;
@@ -1,4 +1,6 @@
using Mapster; using Mapster;
using System.ComponentModel.DataAnnotations;
using System.Net.Sockets;
using VizhTalk.BuildingBlocks.Application.Configuration.Commnad; using VizhTalk.BuildingBlocks.Application.Configuration.Commnad;
using VizhTalk.BuildingBlocks.Application.Exceptions; using VizhTalk.BuildingBlocks.Application.Exceptions;
using VizhTalk.BuildingBlocks.Domain.Contract; using VizhTalk.BuildingBlocks.Domain.Contract;
@@ -12,23 +14,55 @@ using VizhTalk.Learning.Domain.ValueObjects;
namespace VizhTalk.Learning.Application.CommandHandlers; namespace VizhTalk.Learning.Application.CommandHandlers;
public class CreateLanguageCommandHandler(IUnitOfWork unitOfWork, CDNGrpc.CDNGrpcClient cdnClient) : ICommandHandler<CreateLanguageCommand, LanguageDTO> public class CreateLanguageCommandHandler(IUnitOfWork unitOfWork,
CDNGrpc.CDNGrpcClient cdnClient) : ICommandHandler<CreateLanguageCommand, LanguageDTO>
{ {
public async Task<LanguageDTO> Handle(CreateLanguageCommand request, CancellationToken cancellationToken) public async Task<LanguageDTO> Handle(CreateLanguageCommand request, CancellationToken cancellationToken)
{ {
var title = request.Title.Trim(); await unitOfWork.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
if (await unitOfWork.Language.Exists(x => x.Title == title)) try
throw new LanguageTitleAlreadyExistsException(); {
var title = request.Title.Trim();
if (await unitOfWork.Language.Exists(x => x.Title == title))
throw new LanguageTitleAlreadyExistsException();
var language = Language.CreateNew(new LanguageTitle(title), null); var fileGroup = await unitOfWork.FileGroup.GetByFileGroupKey(FileGroupKey.IconLanguage);
await unitOfWork.Language.AddAsync(language, cancellationToken); if (fileGroup == null)
await unitOfWork.SaveChangeAsync(cancellationToken); {
return language.Adapt<LanguageDTO>(); var group = await cdnClient.CreateMediaGroupAsync(new CreateMediaGroupCommand()
{
IsSecure = false,
Key = FileGroupKey.IconLanguage.Value,
Title = nameof(FileGroupKey.IconLanguage)
}, cancellationToken: cancellationToken);
fileGroup = FileGroup.CreateNew(FileGroupKey.IconLanguage, new FileGroupTitle(nameof(FileGroupKey.IconLanguage)), false, group.Id);
await unitOfWork.FileGroup.AddAsync(fileGroup, cancellationToken);
}
var upload = await cdnClient.UploadFileAsync(new UploadFileCommand()
{
File = request.Icon,
FileName = title,
MediaGroupId = fileGroup.MediaGroupId
}, cancellationToken: cancellationToken);
var language = Language.CreateNew(new LanguageTitle(title));
await unitOfWork.Language.AddAsync(language, cancellationToken);
await unitOfWork.SaveChangeAsync(cancellationToken);
var media = Media.CreateNew(title, upload.ContentType, upload.Size, new EntityType(language.Id.Value.ToString(), EntityTypeEnum.Language));
await unitOfWork.Media.AddAsync(media, cancellationToken);
language.SetIcon(media);
return language.Adapt<LanguageDTO>();
}
catch (Exception ex)
{
await unitOfWork.RollbackTransactionAsync(cancellationToken).ConfigureAwait(false);
throw;
}
} }
} }
public class UpdateLanguageCommandHandler(IUnitOfWork unitOfWork) : ICommandHandler<UpdateLanguageCommand, LanguageDTO> public class UpdateLanguageCommandHandler(IUnitOfWork unitOfWork, CDNGrpc.CDNGrpcClient cdnClient) : ICommandHandler<UpdateLanguageCommand, LanguageDTO>
{ {
public async Task<LanguageDTO> Handle(UpdateLanguageCommand request, CancellationToken cancellationToken) public async Task<LanguageDTO> Handle(UpdateLanguageCommand request, CancellationToken cancellationToken)
{ {
@@ -37,7 +71,21 @@ public class UpdateLanguageCommandHandler(IUnitOfWork unitOfWork) : ICommandHand
throw new LanguageNotFoundException(); throw new LanguageNotFoundException();
language.SetTitle(new LanguageTitle(request.Title)); language.SetTitle(new LanguageTitle(request.Title));
if(request.Icon != null)
{
var fileGroup = await unitOfWork.FileGroup.GetByFileGroupKey(FileGroupKey.IconLanguage);
var upload = await cdnClient.UploadFileAsync(new UploadFileCommand()
{
File = request.Icon,
FileName = request.Title,
MediaGroupId = fileGroup.MediaGroupId
}, cancellationToken: cancellationToken);
var media = Media.CreateNew(request.Title, upload.ContentType, upload.Size, new EntityType(language.Id.Value.ToString(), EntityTypeEnum.Language));
language.SetIcon(media);
}
return language.Adapt<LanguageDTO>(); return language.Adapt<LanguageDTO>();
} }
} }
@@ -0,0 +1,10 @@
using VizhTalk.BuildingBlocks.Application.Exceptions;
namespace VizhTalk.Learning.Application.Exceptions;
public class LanguageTitleAlreadyExistsException : BaseException
{
public LanguageTitleAlreadyExistsException() : base("title already exists")
{
}
}
@@ -6,6 +6,8 @@ namespace VizhTalk.Learning.Application;
public interface IUnitOfWork : IUnitOfWorkBase public interface IUnitOfWork : IUnitOfWorkBase
{ {
ILanguageRepository Language { get; } ILanguageRepository Language { get; }
IFileGroupRepository FileGroup { get; }
IMediaRepository Media { get; }
Task SaveChangeAsync(CancellationToken cancellationToken = default); Task SaveChangeAsync(CancellationToken cancellationToken = default);
} }
+25 -7
View File
@@ -1,12 +1,30 @@
using System; using VizhTalk.BuildingBlocks.Domain;
using System.Collections.Generic; using VizhTalk.Learning.Domain.TypeIds;
using System.Linq; using VizhTalk.Learning.Domain.ValueObjects;
using System.Text;
using System.Threading.Tasks;
namespace VizhTalk.Learning.Domain.Entities namespace VizhTalk.Learning.Domain.Entities;
public class Course : Entity<CourseId>
{ {
internal class Course private Course()
{ {
} }
private Course(CourseTitle title, int order)
{
Title = title;
Order = order;
}
public CourseTitle Title { get; private set; }
public Media Image { get; private set; }
public int Order { get; private set; }
internal static Course CreateNew(CourseTitle title, int order)
=> new(title, order);
public void SetTitle(CourseTitle title)
=> Title = title;
public void SetOrder(int order)
=> Order = order;
public void SetImage(Media media)
=> Image = media;
} }
+25 -8
View File
@@ -1,4 +1,5 @@
using VizhTalk.BuildingBlocks.Domain; using VizhTalk.BuildingBlocks.Domain;
using VizhTalk.Learning.Domain.Exceptions;
using VizhTalk.Learning.Domain.TypeIds; using VizhTalk.Learning.Domain.TypeIds;
using VizhTalk.Learning.Domain.ValueObjects; using VizhTalk.Learning.Domain.ValueObjects;
@@ -6,24 +7,40 @@ namespace VizhTalk.Learning.Domain.Entities;
public class Language : Entity<LanguageId>, IAggregateRoot public class Language : Entity<LanguageId>, IAggregateRoot
{ {
public Language() private List<Course> _courses = new();
private Language()
{ {
// only EF // only EF
} }
private Language(LanguageTitle title, Media icon) private Language(LanguageTitle title)
{ {
Title = title; Title = title;
Icon = icon;
} }
public LanguageTitle Title { get; private set; } public LanguageTitle Title { get; private set; }
public Media Icon { get; private set; } public Media Icon { get; private set; }
public IReadOnlyList<Course> Courses => _courses;
public static Language CreateNew(LanguageTitle title)
=> new(title);
public static Language CreateNew(LanguageTitle title,Media icon)
=> new(title, icon);
public void SetTitle(LanguageTitle title) public void SetTitle(LanguageTitle title)
=> Title = title; => Title = title;
public void SetIcon(Media icon) public void SetIcon(Media icon)
=> Icon = icon; {
if (Icon != null)
Icon.SetDeleted(true);
Icon = icon;
}
public Course AddCourse(CourseTitle courseTitle)
{
var order = Courses.Count;
var course = Course.CreateNew(courseTitle, order);
_courses.Add(course);
return course;
}
public Course UpdateCourse(Course course)
{
var _course = _courses.FirstOrDefault(x => x.Id == course.Id) ?? throw new CourseNotFoundException(Title);
throw new Exception();
}
} }
@@ -6,9 +6,28 @@ namespace VizhTalk.Learning.Domain.Entities;
public class Media : Entity<MediaId> public class Media : Entity<MediaId>
{ {
private Media()
{
}
private Media(string name,
string contentType,
int size,
EntityType type)
{
Name = name;
ContentType = contentType;
Size = size;
EntityType = type;
}
public string Name { get; private set; } public string Name { get; private set; }
public string ContentType { get; private set; } public string ContentType { get; private set; }
public int Size { get; private set; } public int Size { get; private set; }
public EntityType EntityType { get; private set; } public EntityType EntityType { get; private set; }
public static Media CreateNew(string name,
string contentType,
int size,
EntityType type)
=> new(name, contentType, size, type);
} }
@@ -0,0 +1,8 @@
using VizhTalk.BuildingBlocks.Domain.Contract;
namespace VizhTalk.Learning.Domain.Exceptions;
public class CourseNotFoundException(string languageTitle)
: Exception($"course not found in {languageTitle} language"),IGuardException
{
}
@@ -0,0 +1,11 @@
using VizhTalk.BuildingBlocks.Domain;
using VizhTalk.Learning.Domain.Entities;
using VizhTalk.Learning.Domain.TypeIds;
using VizhTalk.Learning.Domain.ValueObjects;
namespace VizhTalk.Learning.Domain.Repositories;
public interface IFileGroupRepository : IRepositoryBase<FileGroup,FileGroupId>
{
Task<FileGroup> GetByFileGroupKey(FileGroupKey key, CancellationToken cancellationToken = default);
}
@@ -2,10 +2,9 @@
using VizhTalk.Learning.Domain.Entities; using VizhTalk.Learning.Domain.Entities;
using VizhTalk.Learning.Domain.TypeIds; using VizhTalk.Learning.Domain.TypeIds;
namespace VizhTalk.Learning.Domain.Repositories namespace VizhTalk.Learning.Domain.Repositories;
{
public interface ILanguageRepository : IRepositoryBase<Language,LanguageId> public interface ILanguageRepository : IRepositoryBase<Language,LanguageId>
{ {
}
} }
@@ -0,0 +1,9 @@
using VizhTalk.BuildingBlocks.Domain;
using VizhTalk.Learning.Domain.Entities;
using VizhTalk.Learning.Domain.TypeIds;
namespace VizhTalk.Learning.Domain.Repositories;
public interface IMediaRepository : IRepositoryBase<Media,MediaId>
{
}
@@ -0,0 +1,10 @@
using VizhTalk.BuildingBlocks.Domain;
namespace VizhTalk.Learning.Domain.TypeIds;
public class CourseId : TypeIdValueBase<long>
{
public CourseId(long value) : base(value)
{
}
}
@@ -0,0 +1,18 @@
using VizhTalk.BuildingBlocks.Domain.Guards;
namespace VizhTalk.Learning.Domain.ValueObjects;
public class CourseTitle
{
public CourseTitle(string title)
{
title = title.Trim();
Guard.NullOrEmpty(title, nameof(LanguageTitle));
Guard.MaxLength(title, nameof(LanguageTitle), 60);
Value = title;
}
public string Value { get; private set; }
public static implicit operator string(CourseTitle title) => title.Value;
}
@@ -10,7 +10,7 @@ namespace VizhTalk.Learning.Domain.ValueObjects
{ {
public class FileGroupKey : ValueObject public class FileGroupKey : ValueObject
{ {
public static FileGroupKey LanguageIcon = new("Language"); public static FileGroupKey IconLanguage = new("IconLanguage");
public FileGroupKey(string entityName, string? entityId = null) public FileGroupKey(string entityName, string? entityId = null)
{ {
Guard.NullOrEmpty(entityName, entityName); Guard.NullOrEmpty(entityName, entityName);
@@ -8,7 +8,6 @@
<ItemGroup> <ItemGroup>
<Folder Include="Events\" /> <Folder Include="Events\" />
<Folder Include="Exceptions\" />
<Folder Include="Rules\" /> <Folder Include="Rules\" />
</ItemGroup> </ItemGroup>
@@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore;
using VizhTalk.BuildingBlocks.Infrastructure;
using VizhTalk.Learning.Domain.Entities;
using VizhTalk.Learning.Domain.Repositories;
using VizhTalk.Learning.Domain.TypeIds;
using VizhTalk.Learning.Domain.ValueObjects;
using VizhTalk.Learning.Infrastructure.EF;
namespace VizhTalk.Learning.Infrastructure.Repositories;
public class FileGroupRepository : RepositoryBase<FileGroup, FileGroupId>, IFileGroupRepository
{
public FileGroupRepository(LearningDbContext dbContext) : base(dbContext)
{
}
public async Task<FileGroup> GetByFileGroupKey(FileGroupKey key, CancellationToken cancellationToken = default)
{
var fileGroup = await Entity.FirstOrDefaultAsync(x=>x.Key == key, cancellationToken);
return fileGroup;
}
}
@@ -0,0 +1,14 @@
using Microsoft.EntityFrameworkCore;
using VizhTalk.BuildingBlocks.Infrastructure;
using VizhTalk.Learning.Domain.Entities;
using VizhTalk.Learning.Domain.Repositories;
using VizhTalk.Learning.Domain.TypeIds;
namespace VizhTalk.Learning.Infrastructure.Repositories;
public class MediaRepository : RepositoryBase<Media, MediaId>, IMediaRepository
{
public MediaRepository(DbContext dbContext) : base(dbContext)
{
}
}
+26 -1
View File
@@ -1,4 +1,5 @@
using VizhTalk.BuildingBlocks.Infrastructure; using System.Runtime.CompilerServices;
using VizhTalk.BuildingBlocks.Infrastructure;
using VizhTalk.BuildingBlocks.Infrastructure.DomainEventsDispatching; using VizhTalk.BuildingBlocks.Infrastructure.DomainEventsDispatching;
using VizhTalk.Learning.Application; using VizhTalk.Learning.Application;
using VizhTalk.Learning.Domain.Repositories; using VizhTalk.Learning.Domain.Repositories;
@@ -22,6 +23,30 @@ namespace VizhTalk.Learning.Infrastructure
} }
} }
private IFileGroupRepository _fileGroup;
public IFileGroupRepository FileGroup
{
get
{
if (_fileGroup == null)
_fileGroup = new FileGroupRepository(learningDbContext);
return _fileGroup;
}
}
private IMediaRepository _media;
public IMediaRepository Media
{
get
{
if(_media == null)
_media = new MediaRepository(learningDbContext);
return _media;
}
}
public async Task SaveChangeAsync(CancellationToken cancellationToken = default) public async Task SaveChangeAsync(CancellationToken cancellationToken = default)
=> await learningDbContext.SaveChangesAsync(cancellationToken); => await learningDbContext.SaveChangesAsync(cancellationToken);
} }
@@ -14,5 +14,5 @@ message CreateLanguageCommand{
message UpdateLanguageCommand{ message UpdateLanguageCommand{
Id Id = 1; Id Id = 1;
string title = 2; string title = 2;
bytes icon = 3; optional bytes icon = 3;
} }