Compare commits

...

3 Commits

Author SHA1 Message Date
Amir Hossein Khademi 908424f9be change to comment 2024-09-25 17:08:35 +03:30
Amir Hossein Khademi ca934aecc8 Update Packages 2024-09-24 14:03:16 +03:30
Amir Hossein Khademi 6bd9066f8d Change to comment PHASE A 2024-09-24 00:34:53 +03:30
53 changed files with 3573 additions and 1138 deletions

View File

@ -1,50 +1,50 @@
namespace Netina.Api.Controllers;
public class ProductReviewController : ICarterModule
public class CommentController : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
var group = app.NewVersionedApi("ProductReview")
.MapGroup("api/product/review");
var group = app.NewVersionedApi("Comments")
.MapGroup("api/comment");
group.MapGet("{id}", GetAsync)
.WithDisplayName("Get Product Review")
.WithDisplayName("Get Comment")
.RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser().RequireClaim(CustomClaimType.Permission, ApplicationPermission.ViewAllReviews,ApplicationPermission.ManageReview))
.HasApiVersion(1.0);
group.MapGet("", GetAllAsync)
.WithDisplayName("Get Product Reviews")
.WithDisplayName("Get Comments")
.RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser().RequireClaim(CustomClaimType.Permission, ApplicationPermission.ViewAllReviews, ApplicationPermission.ManageReview))
.HasApiVersion(1.0);
group.MapPost("", PostAsync)
.WithDisplayName("Create Product Review")
.WithDisplayName("Create Comment")
.RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser())
.HasApiVersion(1.0);
group.MapPut("confirm/{id}", ConfirmAsync)
.WithDisplayName("Confirm Product Review")
.WithDisplayName("Confirm Comment")
.RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser().RequireClaim(CustomClaimType.Permission, ApplicationPermission.ConfirmReview, ApplicationPermission.ManageReview))
.HasApiVersion(1.0);
group.MapDelete("{id}", DeleteAsync)
.WithDisplayName("Delete Product Review")
.WithDisplayName("Delete Comment")
.RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser().RequireClaim(CustomClaimType.Permission, ApplicationPermission.ManageReview))
.HasApiVersion(1.0);
}
public async Task<IResult> GetAllAsync([FromQuery] int page, IMediator mediator, CancellationToken cancellationToken)
=> TypedResults.Ok(await mediator.Send(new GetReviewsQuery(page), cancellationToken));
=> TypedResults.Ok(await mediator.Send(new GetCommentsQuery(page), cancellationToken));
public async Task<IResult> GetAsync(Guid id, IMediator mediator, CancellationToken cancellationToken)
=> TypedResults.Ok(await mediator.Send(new GetReviewQuery(id), cancellationToken));
=> TypedResults.Ok(await mediator.Send(new GetCommentQuery(id), cancellationToken));
public async Task<IResult> PostAsync(CreateReviewCommand request, IMediator mediator, CancellationToken cancellationToken)
public async Task<IResult> PostAsync(CreateCommentCommand request, IMediator mediator, CancellationToken cancellationToken)
=> TypedResults.Ok(await mediator.Send(request, cancellationToken));
public async Task<IResult> ConfirmAsync(Guid id, IMediator mediator, CancellationToken cancellationToken)
=> TypedResults.Ok(await mediator.Send(new ConfirmReviewCommand(id), cancellationToken));
=> TypedResults.Ok(await mediator.Send(new ConfirmCommentCommand(id), cancellationToken));
public async Task<IResult> DeleteAsync(Guid id, IMediator mediator, CancellationToken cancellationToken)
=> TypedResults.Ok(await mediator.Send(new DeleteReviewCommand(id), cancellationToken));
=> TypedResults.Ok(await mediator.Send(new DeleteCommentCommand(id), cancellationToken));
}

View File

@ -15,8 +15,6 @@ public class DashboardController : ICarterModule
group.MapGet("orders", GetOrdersDashboardAsync)
.WithDisplayName("Get Orders Dashboard")
.HasApiVersion(1.0);
}
private async Task<IResult> GetOrdersDashboardAsync([FromServices] IDashboardService dashboardService, CancellationToken cancellationToken)

View File

@ -38,8 +38,8 @@ public class ProductController : ICarterModule
.RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser().RequireClaim(CustomClaimType.Permission, ApplicationPermission.ManageProducts))
.HasApiVersion(1.0);
group.MapGet("{productId}/review",GetProductReviewsAsync)
.WithDisplayName("Get Product Reviews")
group.MapGet("{productId}/comment",GetProductCommentsAsync)
.WithDisplayName("Get Product Comments")
.HasApiVersion(1.0);
group.MapDelete("{id}", Delete)
@ -48,8 +48,8 @@ public class ProductController : ICarterModule
.HasApiVersion(1.0);
}
private async Task<IResult> GetProductReviewsAsync([FromQuery] int page, [FromRoute] Guid productId, IMediator mediator, CancellationToken cancellationToken)
=> TypedResults.Ok(await mediator.Send(new GetReviewsQuery(page, productId), cancellationToken));
private async Task<IResult> GetProductCommentsAsync([FromQuery] int page, [FromQuery]int count, [FromRoute] Guid productId, IMediator mediator, CancellationToken cancellationToken)
=> TypedResults.Ok(await mediator.Send(new GetCommentsQuery(page, count, productId), cancellationToken));
// GET:Get All Entity
public async Task<IResult> GetAllAsync([FromQuery] int page,

View File

@ -11,48 +11,48 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8" />
<PackageReference Include="Asp.Versioning.Http" Version="8.1.0" />
<PackageReference Include="Ben.BlockingDetector" Version="0.0.4" />
<PackageReference Include="Carter" Version="8.1.0" />
<PackageReference Include="FluentValidation" Version="11.9.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.0" />
<PackageReference Include="MediatR.Extensions.Autofac.DependencyInjection" Version="12.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
<PackageReference Include="Carter" Version="8.2.1" />
<PackageReference Include="FluentValidation" Version="11.10.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.10.0" />
<PackageReference Include="MediatR.Extensions.Autofac.DependencyInjection" Version="12.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.7">
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Autofac" Version="8.0.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Elmah.Io.AspNetCore.Serilog" Version="5.0.17" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.7" />
<PackageReference Include="Autofac" Version="8.1.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="10.0.0" />
<PackageReference Include="Elmah.Io.AspNetCore.Serilog" Version="5.1.23" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.2" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Sentry.Serilog" Version="4.9.0" />
<PackageReference Include="Sentry.Serilog" Version="4.11.0" />
<PackageReference Include="Serilog" Version="4.0.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.2" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.PostgreSQL" Version="2.3.0" />
<PackageReference Include="Serilog.Sinks.Seq" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.ElmahIo" Version="5.0.38" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.ElmahIo" Version="5.1.43" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="10.2.0" />
<PackageReference Include="StackExchange.Redis.Extensions.Core" Version="10.2.0" />
<PackageReference Include="StackExchange.Redis.Extensions.Newtonsoft" Version="10.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.7.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.8.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.8.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.2" />
<PackageReference Include="System.Data.SqlClient" Version="4.8.6" />
<PackageReference Include="System.Drawing.Common" Version="8.0.7" />
<PackageReference Include="System.Drawing.Common" Version="8.0.8" />
</ItemGroup>
<ItemGroup>

View File

@ -11,7 +11,7 @@
<PackageReference Include="MD.PersianDateTime.Standard" Version="2.5.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.2" />
</ItemGroup>
<!--<PropertyGroup>

View File

@ -1,5 +1,7 @@

namespace Netina.Core.BaseServices;
public class SettingService(IMartenRepositoryWrapper martenRepositoryWrapper) : ISettingService
{
public async Task<object> GetSettingAsync(string settingName, CancellationToken cancellationToken = default)

View File

@ -0,0 +1,36 @@
using Netina.Domain.Entities.Comments;
namespace Netina.Core.EntityServices.CommentHandlers;
public class ConfirmCommentCommandHandler(IRepositoryWrapper repositoryWrapper)
: IRequestHandler<ConfirmCommentCommand, Guid>
{
public async Task<Guid> Handle(ConfirmCommentCommand request, CancellationToken cancellationToken)
{
var review = await repositoryWrapper.SetRepository<Comment>().TableNoTracking
.FirstOrDefaultAsync(r => r.Id == request.Id, cancellationToken);
if (review == null)
throw new AppException("Comment not found", ApiResultStatusCode.NotFound);
review.ConfirmReview();
var productComment = await repositoryWrapper.SetRepository<ProductComment>()
.TableNoTracking.FirstOrDefaultAsync(f => f.Id == request.Id, cancellationToken);
if (productComment != null)
{
var product = await repositoryWrapper.SetRepository<Product>()
.TableNoTracking
.FirstOrDefaultAsync(p => p.Id == productComment.ProductId, cancellationToken);
if (product == null)
throw new AppException("Product not found", ApiResultStatusCode.NotFound);
product.AddRate(productComment.Rate);
repositoryWrapper.SetRepository<Product>().Update(product);
await repositoryWrapper.SaveChangesAsync(cancellationToken);
}
repositoryWrapper.SetRepository<Comment>().Update(review);
await repositoryWrapper.SaveChangesAsync(cancellationToken);
return review.Id;
}
}

View File

@ -1,20 +0,0 @@
using Review = Netina.Domain.Entities.Reviews.Review;
namespace Netina.Core.EntityServices.ReviewHandlers;
public class ConfirmReviewCommandHandler(IRepositoryWrapper repositoryWrapper)
: IRequestHandler<ConfirmReviewCommand, Guid>
{
public async Task<Guid> Handle(ConfirmReviewCommand request, CancellationToken cancellationToken)
{
var review = await repositoryWrapper.SetRepository<Review>().TableNoTracking
.FirstOrDefaultAsync(r => r.Id == request.Id, cancellationToken);
if (review == null)
throw new AppException("Review not found", ApiResultStatusCode.NotFound);
review.ConfirmReview();
repositoryWrapper.SetRepository<Review>().Update(review);
await repositoryWrapper.SaveChangesAsync(cancellationToken);
return review.Id;
}
}

View File

@ -13,8 +13,8 @@
<PackageReference Include="Autofac.Extras.Quartz" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageReference Include="Quartz" Version="3.12.0" />
<PackageReference Include="Syncfusion.Pdf.Net.Core" Version="26.1.35" />
<PackageReference Include="Quartz" Version="3.13.0" />
<PackageReference Include="Syncfusion.Pdf.Net.Core" Version="27.1.50" />
</ItemGroup>
@ -26,7 +26,6 @@
<ItemGroup>
<Folder Include="CoreServices\WebSiteServices\" />
<Folder Include="EntityServices\ProductHandlers\" />
<Folder Include="EntityServices\ReviewHandlers\" />
<Folder Include="Models\Api\" />
<Folder Include="Models\Scraper\" />
<Folder Include="Utilities\" />

View File

@ -0,0 +1,27 @@
namespace Netina.Domain.CommandQueries.Commands;
public sealed record CreateCommentCommand(
string Title,
string Content,
float Rate,
bool IsBuyer,
bool IsAdmin,
Guid? ParentId,
Guid? BlogId = null,
Guid? ProductId = null,
Guid? UserId = null) : IRequest<Guid>;
public sealed record UpdateCommentCommand(
Guid Id,
string Title,
string Content,
float Rate,
bool IsBuyer,
bool IsAdmin,
Guid? ParentId,
Guid? BlogId = null,
Guid? ProductId = null,
Guid? UserId = null) : IRequest<Guid>;
public sealed record ConfirmCommentCommand(Guid Id) : IRequest<Guid>;
public sealed record DeleteCommentCommand(Guid Id) : IRequest<Guid>;

View File

@ -1,6 +0,0 @@
namespace Netina.Domain.CommandQueries.Commands;
public sealed record CreateReviewCommand(string Title, string Comment, float Rate, bool IsBuyer,bool IsAdmin, Guid ProductId, Guid UserId) : IRequest<Guid>;
public sealed record UpdateReviewCommand(Guid Id,string Title, string Comment, float Rate, bool IsBuyer, bool IsAdmin, Guid ProductId, Guid UserId): IRequest<Guid>;
public sealed record ConfirmReviewCommand(Guid Id) : IRequest<Guid>;
public sealed record DeleteReviewCommand(Guid Id) : IRequest<Guid>;

View File

@ -0,0 +1,4 @@
namespace Netina.Domain.CommandQueries.Queries;
public record GetCommentsQuery(int Page = 0,int Count = Refers.SizeM ,Guid? ProductId = null, Guid? BlogId = null) : IRequest<List<CommentSDto>>;
public record GetCommentQuery(Guid Id) : IRequest<CommentLDto>;

View File

@ -1,4 +0,0 @@
namespace Netina.Domain.CommandQueries.Queries;
public record GetReviewsQuery(int Page = 0,Guid? ProductId = null) : IRequest<List<ReviewSDto>>;
public record GetReviewQuery(Guid Id) : IRequest<ReviewLDto>;

View File

@ -0,0 +1,19 @@
using Netina.Domain.Entities.Comments;
namespace Netina.Domain.Dtos.LargDtos;
public class CommentLDto : BaseDto<CommentLDto,Comment>
{
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public float Rate { get; set; }
public bool IsBuyer { get; set; }
public bool IsConfirmed { get; set; }
public Guid ParentId { get; set; }
public Guid UserId { get; set; }
public string UserFullName { get; set; } = string.Empty;
[AdaptIgnore]
public List<CommentSDto> Children { get; set; } = new();
}

View File

@ -27,7 +27,6 @@ public class ProductLDto : BaseDto<ProductLDto,Product>
public bool IsSpecialOffer { get; set; }
public List<SpecificationSDto> Specifications { get; set; } = new();
public List<ReviewSDto> Reviews { get; set; } = new();
public List<StorageFileSDto> Files { get; set; } = new();
public DiscountSDto? SpecialOffer { get; set; }

View File

@ -1,14 +0,0 @@
using Review = Netina.Domain.Entities.Reviews.Review;
namespace Netina.Domain.Dtos.LargDtos;
public class ReviewLDto : BaseDto<ReviewLDto,Review>
{
public string Title { get; set; } = string.Empty;
public string Comment { get; set; } = string.Empty;
public float Rate { get; set; }
public bool IsBuyer { get; set; }
public bool IsConfirmed { get; set; }
public Guid ProductId { get; set; }
public Guid UserId { get; set; }
}

View File

@ -0,0 +1,18 @@
using Netina.Domain.Entities.Comments;
namespace Netina.Domain.Dtos.SmallDtos;
public class CommentSDto : BaseDto<CommentSDto, Comment>
{
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public bool IsAdmin { get; internal set; }
public bool IsConfirmed { get; set; }
public Guid ParentId { get; set; }
public Guid UserId { get; set; }
public string UserFullName { get; set; } = string.Empty;
[AdaptIgnore]
public List<CommentSDto> Children { get; set; } = new();
}

View File

@ -1,14 +0,0 @@
using Review = Netina.Domain.Entities.Reviews.Review;
namespace Netina.Domain.Dtos.SmallDtos;
public class ReviewSDto : BaseDto<ReviewSDto, Review>
{
public string Title { get; set; } = string.Empty;
public string Comment { get; set; } = string.Empty;
public float Rate { get; set; }
public bool IsBuyer { get; set; }
public bool IsAdmin { get; set; }
public Guid ProductId { get; set; }
public Guid UserId { get; set; }
}

View File

@ -30,6 +30,11 @@ public partial class BlogStorageFile
}
}
public partial class BlogComment
{
public static BlogComment Create(string title, string content, float rate, bool isAdmin, Guid userId, Guid blogId)
=> new(title, content, rate, isAdmin, userId, blogId);
}
public partial class BlogCategory
{
public static BlogCategory Create(string name, string description, bool isMain)

View File

@ -0,0 +1,20 @@
using Netina.Domain.Entities.Comments;
namespace Netina.Domain.Entities.Blogs;
public partial class BlogComment : Comment
{
public BlogComment()
{
}
public BlogComment(string title, string content, float rate, bool isAdmin, Guid userId, Guid blogId)
: base(title, content, rate, isAdmin, userId)
{
BlogId = blogId;
}
public Guid BlogId { get; internal set; }
public Blog? Blog { get; internal set; }
}

View File

@ -0,0 +1,18 @@
namespace Netina.Domain.Entities.Comments;
public partial class Comment
{
public static Comment Create(string title, string content, float rate,bool isAdmin, Guid userId)
{
return new Comment(title, content, rate,isAdmin, userId);
}
public void ConfirmReview()
=> IsConfirmed = true;
public void SetParent(Guid parentId)
{
ParentId = parentId;
IsRoot = false;
}
}

View File

@ -1,34 +1,35 @@
namespace Netina.Domain.Entities.Reviews;
namespace Netina.Domain.Entities.Comments;
[AdaptTwoWays("[name]LDto", IgnoreAttributes = new[] { typeof(AdaptIgnoreAttribute) }, MapType = MapType.Map | MapType.MapToTarget | MapType.Projection)]
[AdaptTwoWays("[name]SDto", IgnoreAttributes = new[] { typeof(AdaptIgnoreAttribute) }, MapType = MapType.Map | MapType.MapToTarget)]
[AdaptTo("[name]SDto", IgnoreAttributes = new[] { typeof(AdaptIgnoreAttribute) }, MapType = MapType.Projection)]
[GenerateMapper]
public partial class Review : ApiEntity
public partial class Comment : ApiEntity
{
public Review()
public Comment()
{
}
public Review(string title, string comment, float rate, bool isBuyer,bool isAdmin, Guid productId, Guid userId)
public Comment(string title, string content, float rate, bool isAdmin, Guid userId)
{
Title = title;
Comment = comment;
Content = content;
Rate = rate;
IsBuyer = isBuyer;
IsAdmin = isAdmin;
ProductId = productId;
IsRoot = true;
UserId = userId;
}
public string Title { get; internal set; } = string.Empty;
public string Comment { get; internal set; } = string.Empty;
public string Content { get; internal set; } = string.Empty;
public float Rate { get; internal set; }
public bool IsBuyer { get; internal set; }
public bool IsConfirmed { get; internal set; }
public bool IsAdmin { get; internal set; }
public bool IsRoot { get; internal set; }
public Guid? ParentId { get; internal set; }
public Comment? Parent { get; internal set; }
public Guid ProductId { get; internal set; }
public Product? Product { get; internal set; }
public List<Comment> Children { get; internal set; } = new();
public Guid UserId { get; internal set; }
public ApplicationUser? User { get; internal set; }

View File

@ -60,6 +60,12 @@ public partial class Product
}
}
public partial class ProductComment
{
public static ProductComment Create(string title, string content, float rate, bool isBuyer, bool isAdmin, Guid userId, Guid productId)
=> new(title, content, rate, isBuyer, isAdmin, userId, productId);
}
public partial class ProductStorageFile
{
public static ProductStorageFile Create(string name, string fileLocation, string fileName, bool isHeader,

View File

@ -1,4 +1,6 @@
namespace Netina.Domain.Entities.Products;
using Netina.Domain.Entities.Comments;
namespace Netina.Domain.Entities.Products;
[AdaptTwoWays("[name]LDto", IgnoreAttributes = new[] { typeof(AdaptIgnoreAttribute) }, MapType = MapType.Map | MapType.MapToTarget | MapType.Projection)]
[AdaptTwoWays("[name]SDto", IgnoreAttributes = new[] { typeof(AdaptIgnoreAttribute) }, MapType = MapType.Map | MapType.MapToTarget )]
@ -80,7 +82,7 @@ public partial class Product : ApiEntity
public ProductCategory? Category { get; internal set; }
public List<Specification> Specifications { get; internal set; } = new();
public List<Reviews.Review> Reviews { get; internal set; } = new();
public List<ProductComment> Comments { get; internal set; } = new();
public List<ProductStorageFile> Files { get; internal set; } = new();
public List<OrderProduct> OrderProducts { get; internal set; } = new();

View File

@ -0,0 +1,21 @@
using Netina.Domain.Entities.Comments;
namespace Netina.Domain.Entities.Products;
public partial class ProductComment : Comment
{
public ProductComment()
{
}
public ProductComment(string title, string content, float rate, bool isBuyer, bool isAdmin, Guid userId, Guid productId)
: base(title, content, rate, isAdmin, userId)
{
ProductId = productId;
IsBuyer = isBuyer;
}
public bool IsBuyer { get; internal set; }
public Guid ProductId { get; internal set; }
public Product? Product { get; internal set; }
}

View File

@ -1,12 +0,0 @@
namespace Netina.Domain.Entities.Reviews;
public partial class Review
{
public static Reviews.Review Create(string title, string comment, float rate, bool isBuyer,bool isAdmin, Guid productId, Guid userId)
{
return new Reviews.Review(title, comment, rate, isBuyer,isAdmin, productId, userId);
}
public void ConfirmReview()
=> IsConfirmed = true;
}

View File

@ -0,0 +1,6 @@
namespace Netina.Domain.Mappers
{
public static partial class BlogCommentMapper
{
}
}

View File

@ -0,0 +1,231 @@
using System;
using System.Linq.Expressions;
using Mapster.Models;
using Netina.Domain.Dtos.LargDtos;
using Netina.Domain.Dtos.SmallDtos;
using Netina.Domain.Entities.Comments;
using Netina.Domain.Entities.Users;
namespace Netina.Domain.Mappers
{
public static partial class CommentMapper
{
public static Comment AdaptToComment(this CommentLDto p1)
{
return p1 == null ? null : new Comment()
{
Title = p1.Title,
Content = p1.Content,
Rate = p1.Rate,
IsConfirmed = p1.IsConfirmed,
ParentId = (Guid?)p1.ParentId,
Parent = new Comment() {Id = p1.ParentId},
UserId = p1.UserId,
User = new ApplicationUser() {Id = p1.UserId},
Id = p1.Id,
CreatedAt = p1.CreatedAt
};
}
public static Comment AdaptTo(this CommentLDto p2, Comment p3)
{
if (p2 == null)
{
return null;
}
Comment result = p3 ?? new Comment();
result.Title = p2.Title;
result.Content = p2.Content;
result.Rate = p2.Rate;
result.IsConfirmed = p2.IsConfirmed;
result.ParentId = (Guid?)p2.ParentId;
result.Parent = funcMain1(new Never(), result.Parent, p2);
result.UserId = p2.UserId;
result.User = funcMain2(new Never(), result.User, p2);
result.Id = p2.Id;
result.CreatedAt = p2.CreatedAt;
return result;
}
public static Expression<Func<CommentLDto, Comment>> ProjectToComment => p8 => new Comment()
{
Title = p8.Title,
Content = p8.Content,
Rate = p8.Rate,
IsConfirmed = p8.IsConfirmed,
ParentId = (Guid?)p8.ParentId,
Parent = new Comment() {Id = p8.ParentId},
UserId = p8.UserId,
User = new ApplicationUser() {Id = p8.UserId},
Id = p8.Id,
CreatedAt = p8.CreatedAt
};
public static CommentLDto AdaptToLDto(this Comment p9)
{
return p9 == null ? null : new CommentLDto()
{
Title = p9.Title,
Content = p9.Content,
Rate = p9.Rate,
IsConfirmed = p9.IsConfirmed,
ParentId = p9.ParentId == null ? default(Guid) : (Guid)p9.ParentId,
UserId = p9.UserId,
UserFullName = p9.User != null ? p9.User.FirstName + " " + p9.User.LastName : string.Empty,
Id = p9.Id,
CreatedAt = p9.CreatedAt
};
}
public static CommentLDto AdaptTo(this Comment p10, CommentLDto p11)
{
if (p10 == null)
{
return null;
}
CommentLDto result = p11 ?? new CommentLDto();
result.Title = p10.Title;
result.Content = p10.Content;
result.Rate = p10.Rate;
result.IsConfirmed = p10.IsConfirmed;
result.ParentId = p10.ParentId == null ? default(Guid) : (Guid)p10.ParentId;
result.UserId = p10.UserId;
result.UserFullName = p10.User != null ? p10.User.FirstName + " " + p10.User.LastName : string.Empty;
result.Id = p10.Id;
result.CreatedAt = p10.CreatedAt;
return result;
}
public static Expression<Func<Comment, CommentLDto>> ProjectToLDto => p12 => new CommentLDto()
{
Title = p12.Title,
Content = p12.Content,
Rate = p12.Rate,
IsConfirmed = p12.IsConfirmed,
ParentId = p12.ParentId == null ? default(Guid) : (Guid)p12.ParentId,
UserId = p12.UserId,
UserFullName = p12.User != null ? p12.User.FirstName + " " + p12.User.LastName : string.Empty,
Id = p12.Id,
CreatedAt = p12.CreatedAt
};
public static Comment AdaptToComment(this CommentSDto p13)
{
return p13 == null ? null : new Comment()
{
Title = p13.Title,
Content = p13.Content,
IsConfirmed = p13.IsConfirmed,
IsAdmin = p13.IsAdmin,
ParentId = (Guid?)p13.ParentId,
Parent = new Comment() {Id = p13.ParentId},
UserId = p13.UserId,
User = new ApplicationUser() {Id = p13.UserId},
Id = p13.Id,
CreatedAt = p13.CreatedAt
};
}
public static Comment AdaptTo(this CommentSDto p14, Comment p15)
{
if (p14 == null)
{
return null;
}
Comment result = p15 ?? new Comment();
result.Title = p14.Title;
result.Content = p14.Content;
result.IsConfirmed = p14.IsConfirmed;
result.IsAdmin = p14.IsAdmin;
result.ParentId = (Guid?)p14.ParentId;
result.Parent = funcMain3(new Never(), result.Parent, p14);
result.UserId = p14.UserId;
result.User = funcMain4(new Never(), result.User, p14);
result.Id = p14.Id;
result.CreatedAt = p14.CreatedAt;
return result;
}
public static CommentSDto AdaptToSDto(this Comment p20)
{
return p20 == null ? null : new CommentSDto()
{
Title = p20.Title,
Content = p20.Content,
IsAdmin = p20.IsAdmin,
IsConfirmed = p20.IsConfirmed,
ParentId = p20.ParentId == null ? default(Guid) : (Guid)p20.ParentId,
UserId = p20.UserId,
UserFullName = p20.User != null ? p20.User.FirstName + " " + p20.User.LastName : string.Empty,
Id = p20.Id,
CreatedAt = p20.CreatedAt
};
}
public static CommentSDto AdaptTo(this Comment p21, CommentSDto p22)
{
if (p21 == null)
{
return null;
}
CommentSDto result = p22 ?? new CommentSDto();
result.Title = p21.Title;
result.Content = p21.Content;
result.IsAdmin = p21.IsAdmin;
result.IsConfirmed = p21.IsConfirmed;
result.ParentId = p21.ParentId == null ? default(Guid) : (Guid)p21.ParentId;
result.UserId = p21.UserId;
result.UserFullName = p21.User != null ? p21.User.FirstName + " " + p21.User.LastName : string.Empty;
result.Id = p21.Id;
result.CreatedAt = p21.CreatedAt;
return result;
}
public static Expression<Func<Comment, CommentSDto>> ProjectToSDto => p23 => new CommentSDto()
{
Title = p23.Title,
Content = p23.Content,
IsAdmin = p23.IsAdmin,
IsConfirmed = p23.IsConfirmed,
ParentId = p23.ParentId == null ? default(Guid) : (Guid)p23.ParentId,
UserId = p23.UserId,
UserFullName = p23.User != null ? p23.User.FirstName + " " + p23.User.LastName : string.Empty,
Id = p23.Id,
CreatedAt = p23.CreatedAt
};
private static Comment funcMain1(Never p4, Comment p5, CommentLDto p2)
{
Comment result = p5 ?? new Comment();
result.Id = p2.ParentId;
return result;
}
private static ApplicationUser funcMain2(Never p6, ApplicationUser p7, CommentLDto p2)
{
ApplicationUser result = p7 ?? new ApplicationUser();
result.Id = p2.UserId;
return result;
}
private static Comment funcMain3(Never p16, Comment p17, CommentSDto p14)
{
Comment result = p17 ?? new Comment();
result.Id = p14.ParentId;
return result;
}
private static ApplicationUser funcMain4(Never p18, ApplicationUser p19, CommentSDto p14)
{
ApplicationUser result = p19 ?? new ApplicationUser();
result.Id = p14.UserId;
return result;
}
}
}

View File

@ -0,0 +1,6 @@
namespace Netina.Domain.Mappers
{
public static partial class ProductCommentMapper
{
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,188 +0,0 @@
using System;
using System.Linq.Expressions;
using Netina.Domain.Dtos.LargDtos;
using Netina.Domain.Dtos.SmallDtos;
using Netina.Domain.Entities.Reviews;
namespace Netina.Domain.Mappers
{
public static partial class ReviewMapper
{
public static Review AdaptToReview(this ReviewLDto p1)
{
return p1 == null ? null : new Review()
{
Title = p1.Title,
Comment = p1.Comment,
Rate = p1.Rate,
IsBuyer = p1.IsBuyer,
IsConfirmed = p1.IsConfirmed,
ProductId = p1.ProductId,
UserId = p1.UserId,
Id = p1.Id,
CreatedAt = p1.CreatedAt
};
}
public static Review AdaptTo(this ReviewLDto p2, Review p3)
{
if (p2 == null)
{
return null;
}
Review result = p3 ?? new Review();
result.Title = p2.Title;
result.Comment = p2.Comment;
result.Rate = p2.Rate;
result.IsBuyer = p2.IsBuyer;
result.IsConfirmed = p2.IsConfirmed;
result.ProductId = p2.ProductId;
result.UserId = p2.UserId;
result.Id = p2.Id;
result.CreatedAt = p2.CreatedAt;
return result;
}
public static Expression<Func<ReviewLDto, Review>> ProjectToReview => p4 => new Review()
{
Title = p4.Title,
Comment = p4.Comment,
Rate = p4.Rate,
IsBuyer = p4.IsBuyer,
IsConfirmed = p4.IsConfirmed,
ProductId = p4.ProductId,
UserId = p4.UserId,
Id = p4.Id,
CreatedAt = p4.CreatedAt
};
public static ReviewLDto AdaptToLDto(this Review p5)
{
return p5 == null ? null : new ReviewLDto()
{
Title = p5.Title,
Comment = p5.Comment,
Rate = p5.Rate,
IsBuyer = p5.IsBuyer,
IsConfirmed = p5.IsConfirmed,
ProductId = p5.ProductId,
UserId = p5.UserId,
Id = p5.Id,
CreatedAt = p5.CreatedAt
};
}
public static ReviewLDto AdaptTo(this Review p6, ReviewLDto p7)
{
if (p6 == null)
{
return null;
}
ReviewLDto result = p7 ?? new ReviewLDto();
result.Title = p6.Title;
result.Comment = p6.Comment;
result.Rate = p6.Rate;
result.IsBuyer = p6.IsBuyer;
result.IsConfirmed = p6.IsConfirmed;
result.ProductId = p6.ProductId;
result.UserId = p6.UserId;
result.Id = p6.Id;
result.CreatedAt = p6.CreatedAt;
return result;
}
public static Expression<Func<Review, ReviewLDto>> ProjectToLDto => p8 => new ReviewLDto()
{
Title = p8.Title,
Comment = p8.Comment,
Rate = p8.Rate,
IsBuyer = p8.IsBuyer,
IsConfirmed = p8.IsConfirmed,
ProductId = p8.ProductId,
UserId = p8.UserId,
Id = p8.Id,
CreatedAt = p8.CreatedAt
};
public static Review AdaptToReview(this ReviewSDto p9)
{
return p9 == null ? null : new Review()
{
Title = p9.Title,
Comment = p9.Comment,
Rate = p9.Rate,
IsBuyer = p9.IsBuyer,
IsAdmin = p9.IsAdmin,
ProductId = p9.ProductId,
UserId = p9.UserId,
Id = p9.Id,
CreatedAt = p9.CreatedAt
};
}
public static Review AdaptTo(this ReviewSDto p10, Review p11)
{
if (p10 == null)
{
return null;
}
Review result = p11 ?? new Review();
result.Title = p10.Title;
result.Comment = p10.Comment;
result.Rate = p10.Rate;
result.IsBuyer = p10.IsBuyer;
result.IsAdmin = p10.IsAdmin;
result.ProductId = p10.ProductId;
result.UserId = p10.UserId;
result.Id = p10.Id;
result.CreatedAt = p10.CreatedAt;
return result;
}
public static ReviewSDto AdaptToSDto(this Review p12)
{
return p12 == null ? null : new ReviewSDto()
{
Title = p12.Title,
Comment = p12.Comment,
Rate = p12.Rate,
IsBuyer = p12.IsBuyer,
IsAdmin = p12.IsAdmin,
ProductId = p12.ProductId,
UserId = p12.UserId,
Id = p12.Id,
CreatedAt = p12.CreatedAt
};
}
public static ReviewSDto AdaptTo(this Review p13, ReviewSDto p14)
{
if (p13 == null)
{
return null;
}
ReviewSDto result = p14 ?? new ReviewSDto();
result.Title = p13.Title;
result.Comment = p13.Comment;
result.Rate = p13.Rate;
result.IsBuyer = p13.IsBuyer;
result.IsAdmin = p13.IsAdmin;
result.ProductId = p13.ProductId;
result.UserId = p13.UserId;
result.Id = p13.Id;
result.CreatedAt = p13.CreatedAt;
return result;
}
public static Expression<Func<Review, ReviewSDto>> ProjectToSDto => p15 => new ReviewSDto()
{
Title = p15.Title,
Comment = p15.Comment,
Rate = p15.Rate,
IsBuyer = p15.IsBuyer,
IsAdmin = p15.IsAdmin,
ProductId = p15.ProductId,
UserId = p15.UserId,
Id = p15.Id,
CreatedAt = p15.CreatedAt
};
}
}

View File

@ -1,5 +1,6 @@
using Netina.Domain.Dtos.ResponseDtos.Torob;
using Netina.Domain.Entities.Accounting;
using Netina.Domain.Entities.Comments;
namespace Netina.Domain;
@ -28,6 +29,26 @@ public class MapsterRegister : IRegister
.Map("ShippingMethod", o => o.Shipping != null ? o.Shipping.Name : string.Empty)
.TwoWays();
config.NewConfig<Comment, CommentSDto>()
.Map(d => d.UserFullName, o => o.User != null ? o.User.FirstName + " " + o.User.LastName : string.Empty)
.TwoWays();
config.NewConfig<Comment, CommentLDto>()
.Map(d => d.UserFullName, o => o.User != null ? o.User.FirstName + " " + o.User.LastName : string.Empty)
.TwoWays();
ConfigProductMappers(config);
ConfigOrderMappers(config);
ConfigUserMappers(config);
}
private void ConfigProductMappers(TypeAdapterConfig config)
{
config.NewConfig<ProductCategory, ProductCategorySDto>()
.Map("MainImage", o => o.Files.FirstOrDefault(f => f.IsPrimary) != null ? o.Files.FirstOrDefault(f => f.IsPrimary).FileLocation : o.Files.Count > 0 ? o.Files.FirstOrDefault().FileLocation : string.Empty)
.Map("ParentName", o => o.Parent != null ? o.Parent.Name : string.Empty)
@ -62,16 +83,11 @@ public class MapsterRegister : IRegister
.TwoWays();
config.NewConfig<Product, TorobProductResponseDto>()
.Map(s=>s.availibility,o=>o.IsEnable)
.Map(s=>s.price , o=>o.Cost)
.Map(s=>s.product_id,o=>o.Id.ToString())
.Map(s => s.availibility, o => o.IsEnable)
.Map(s => s.price, o => o.Cost)
.Map(s => s.product_id, o => o.Id.ToString())
.IgnoreNullValues(false)
.TwoWays();
ConfigOrderMappers(config);
ConfigUserMappers(config);
}
private void ConfigOrderMappers(TypeAdapterConfig config)

View File

@ -0,0 +1,8 @@
namespace Netina.Domain.Models;
public static class Refers
{
public const int SizeS = 10;
public const int SizeM = 15;
public const int SizeL = 20;
}

View File

@ -11,9 +11,9 @@
<ItemGroup>
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="Mapster.Core" Version="1.2.1" />
<PackageReference Include="MediatR" Version="12.4.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="8.0.7" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.7" />
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="8.0.8" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.8" />
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0" />
</ItemGroup>
@ -82,6 +82,7 @@
<Using Include="Netina.Domain.Enums" />
<Using Include="Netina.Domain.MartenEntities.Faqs" />
<Using Include="Netina.Domain.MartenEntities.Settings" />
<Using Include="Netina.Domain.Models" />
<Using Include="System.ComponentModel.DataAnnotations" />
<Using Include="System.Diagnostics" />
<Using Include="System.Reflection" />

View File

@ -8,9 +8,9 @@
<ItemGroup>
<PackageReference Include="Marten" Version="7.26.1" />
<PackageReference Include="AWSSDK.S3" Version="3.7.400.2" />
<PackageReference Include="Refit" Version="7.0.0" />
<PackageReference Include="Marten" Version="7.28.0" />
<PackageReference Include="AWSSDK.S3" Version="3.7.403.1" />
<PackageReference Include="Refit" Version="7.2.1" />
</ItemGroup>

View File

@ -0,0 +1,58 @@
using AppException = Netina.Common.Models.Exception.AppException;
namespace Netina.Repository.Handlers.Comments;
public class CreateCommentCommandHandler(IRepositoryWrapper repositoryWrapper, ICurrentUserService currentUserService)
: IRequestHandler<CreateCommentCommand, Guid>
{
public async Task<Guid> Handle(CreateCommentCommand request, CancellationToken cancellationToken)
{
Guid userId;
if (request.UserId == null)
{
if (!Guid.TryParse(currentUserService.UserId, out userId))
throw new AppException("User id is wrong", ApiResultStatusCode.BadRequest);
}
else
userId = request.UserId.Value;
if (request.BlogId != null)
{
var entBlog = BlogComment.Create(request.Title, request.Content, request.Rate, request.IsAdmin, userId, request.BlogId.Value);
if (request.IsAdmin)
entBlog.ConfirmReview();
if (request.ParentId != null)
entBlog.SetParent(request.ParentId.Value);
repositoryWrapper.SetRepository<BlogComment>().Add(entBlog);
await repositoryWrapper.SaveChangesAsync(cancellationToken);
return entBlog.Id;
}
if (request.ProductId != null)
{
var entBlog = ProductComment.Create(request.Title, request.Content, request.Rate, request.IsBuyer,
request.IsAdmin, userId, request.ProductId.Value);
if (request.IsAdmin)
entBlog.ConfirmReview();
if (request.ParentId != null)
entBlog.SetParent(request.ParentId.Value);
repositoryWrapper.SetRepository<ProductComment>().Add(entBlog);
await repositoryWrapper.SaveChangesAsync(cancellationToken);
return entBlog.Id;
}
var ent = Comment.Create(request.Title, request.Content, request.Rate, request.IsAdmin, userId);
if (request.IsAdmin)
ent.ConfirmReview();
if (request.ParentId != null)
ent.SetParent(request.ParentId.Value);
repositoryWrapper.SetRepository<Comment>().Add(ent);
await repositoryWrapper.SaveChangesAsync(cancellationToken);
return ent.Id;
}
}

View File

@ -0,0 +1,35 @@
namespace Netina.Repository.Handlers.Comments;
public class DeleteCommentCommandHandler(IRepositoryWrapper repositoryWrapper)
: IRequestHandler<DeleteCommentCommand, Guid>
{
public async Task<Guid> Handle(DeleteCommentCommand request, CancellationToken cancellationToken)
{
var review = await repositoryWrapper.SetRepository<Comment>().TableNoTracking
.FirstOrDefaultAsync(r => r.Id == request.Id, cancellationToken);
if (review == null)
throw new AppException("Comment not found", ApiResultStatusCode.NotFound);
if (review.IsConfirmed)
{
var productComment = await repositoryWrapper.SetRepository<ProductComment>()
.TableNoTracking.FirstOrDefaultAsync(f => f.Id == request.Id, cancellationToken);
if (productComment != null)
{
var product = await repositoryWrapper.SetRepository<Product>()
.TableNoTracking
.FirstOrDefaultAsync(p => p.Id == productComment.ProductId, cancellationToken);
if (product != null)
{
product.RemoveRate(review.Rate);
repositoryWrapper.SetRepository<Product>().Update(product);
await repositoryWrapper.SaveChangesAsync(cancellationToken);
}
}
}
repositoryWrapper.SetRepository<Comment>().Delete(review);
await repositoryWrapper.SaveChangesAsync(cancellationToken);
return review.Id;
}
}

View File

@ -0,0 +1,19 @@
using Netina.Domain.Entities.Comments;
namespace Netina.Repository.Handlers.Comments;
public class GetCommentQueryHandler(IRepositoryWrapper repositoryWrapper) : IRequestHandler<GetCommentQuery, CommentLDto>
{
public async Task<CommentLDto> Handle(GetCommentQuery request, CancellationToken cancellationToken)
{
var review = await repositoryWrapper.SetRepository<Comment>()
.TableNoTracking
.Where(r => r.Id == request.Id)
.Select(CommentMapper.ProjectToLDto)
.FirstOrDefaultAsync(cancellationToken);
if (review == null)
throw new AppException("Comment not found", ApiResultStatusCode.NotFound);
return review;
}
}

View File

@ -0,0 +1,64 @@
namespace Netina.Repository.Handlers.Comments;
public class GetCommentsQueryHandler(IRepositoryWrapper repositoryWrapper)
: IRequestHandler<GetCommentsQuery, List<CommentSDto>>
{
public async Task<List<CommentSDto>> Handle(GetCommentsQuery request, CancellationToken cancellationToken)
{
var count = request.Count == 0 ? Refers.SizeL : request.Count;
if (count > 50)
throw new BaseApiException(ApiResultStatusCode.BadRequest, "Count limit is 50");
if (request.BlogId != null)
{
var blogQuery = repositoryWrapper.SetRepository<BlogComment>().TableNoTracking
.Where(b => b.IsRoot && b.BlogId == request.BlogId)
.Skip(request.Page * count)
.Take(count)
.Select(CommentMapper.ProjectToSDto);
var blogRoots = await blogQuery.ToListAsync(cancellationToken);
foreach (var root in blogRoots)
await LoadChildrenAsync(root, cancellationToken);
return blogRoots;
}
if (request.ProductId != null)
{
var blogQuery = repositoryWrapper.SetRepository<ProductComment>().TableNoTracking
.Where(b => b.IsRoot && b.ProductId == request.ProductId)
.Skip(request.Page * count)
.Take(count)
.Select(CommentMapper.ProjectToSDto);
var blogRoots = await blogQuery.ToListAsync(cancellationToken);
foreach (var root in blogRoots)
await LoadChildrenAsync(root, cancellationToken);
return blogRoots;
}
var query = repositoryWrapper.SetRepository<Comment>().TableNoTracking
.Where(b => b.IsRoot)
.Skip(request.Page * count)
.Take(count)
.Select(CommentMapper.ProjectToSDto);
var roots = await query.ToListAsync(cancellationToken);
foreach (var root in roots)
await LoadChildrenAsync(root, cancellationToken);
return roots;
}
private async Task LoadChildrenAsync(CommentSDto comment, CancellationToken cancellationToken)
{
var children = await repositoryWrapper.SetRepository<Comment>()
.TableNoTracking
.Where(c => c.ParentId == comment.Id)
.Select(CommentMapper.ProjectToSDto)
.ToListAsync(cancellationToken);
foreach (var blogComment in children)
await LoadChildrenAsync(blogComment, cancellationToken);
comment.Children = children;
}
}

View File

@ -0,0 +1,19 @@
using FluentValidation;
namespace Netina.Repository.Handlers.Comments.Validators;
public class CreateCommentCommandValidator : AbstractValidator<CreateCommentCommand>
{
public CreateCommentCommandValidator()
{
RuleFor(d => d.Title)
.NotNull()
.NotEmpty()
.WithMessage("عنوان نظر مورد نظر را وارد کنید");
RuleFor(d => d.Content)
.NotNull()
.NotEmpty()
.WithMessage("متن نظر مورد نظر را وارد کنید");
}
}

View File

@ -0,0 +1,19 @@
using FluentValidation;
namespace Netina.Repository.Handlers.Comments.Validators;
public class UpdateCommentCommandValidator : AbstractValidator<UpdateCommentCommand>
{
public UpdateCommentCommandValidator()
{
RuleFor(d => d.Title)
.NotNull()
.NotEmpty()
.WithMessage("عنوان نظر مورد نظر را وارد کنید");
RuleFor(d => d.Content)
.NotNull()
.NotEmpty()
.WithMessage("متن نظر مورد نظر را وارد کنید");
}
}

View File

@ -1,34 +0,0 @@
using AppException = Netina.Common.Models.Exception.AppException;
namespace Netina.Repository.Handlers.Reviews;
public class CreateReviewCommandHandler(IRepositoryWrapper repositoryWrapper, ICurrentUserService currentUserService)
: IRequestHandler<CreateReviewCommand, Guid>
{
public async Task<Guid> Handle(CreateReviewCommand request, CancellationToken cancellationToken)
{
Guid userId = request.UserId;
if (userId == default)
{
if (!Guid.TryParse(currentUserService.UserId, out userId))
throw new AppException("User id is wrong", ApiResultStatusCode.BadRequest);
}
var review = Review.Create(request.Title, request.Comment, request.Rate, request.IsBuyer, request.IsAdmin, request.ProductId,
userId);
var product = await repositoryWrapper.SetRepository<Product>()
.TableNoTracking
.FirstOrDefaultAsync(p => p.Id == request.ProductId, cancellationToken);
if (product == null)
throw new AppException("Product not found", ApiResultStatusCode.NotFound);
product.AddRate(request.Rate);
repositoryWrapper.SetRepository<Product>().Update(product);
await repositoryWrapper.SaveChangesAsync(cancellationToken);
repositoryWrapper.SetRepository<Review>().Add(review);
await repositoryWrapper.SaveChangesAsync(cancellationToken);
return review.Id;
}
}

View File

@ -1,26 +0,0 @@
namespace Netina.Repository.Handlers.Reviews;
public class DeleteReviewCommandHandler(IRepositoryWrapper repositoryWrapper)
: IRequestHandler<DeleteReviewCommand, Guid>
{
public async Task<Guid> Handle(DeleteReviewCommand request, CancellationToken cancellationToken)
{
var review = await repositoryWrapper.SetRepository<Review>().TableNoTracking
.FirstOrDefaultAsync(r => r.Id == request.Id, cancellationToken);
if (review == null)
throw new AppException("Review not found", ApiResultStatusCode.NotFound);
var product = await repositoryWrapper.SetRepository<Product>()
.TableNoTracking
.FirstOrDefaultAsync(p => p.Id == review.ProductId, cancellationToken);
if (product != null)
{
product.RemoveRate(review.Rate);
repositoryWrapper.SetRepository<Product>().Update(product);
await repositoryWrapper.SaveChangesAsync(cancellationToken);
}
repositoryWrapper.SetRepository<Review>().Delete(review);
await repositoryWrapper.SaveChangesAsync(cancellationToken);
return review.Id;
}
}

View File

@ -1,19 +0,0 @@
using Review = Netina.Domain.Entities.Reviews.Review;
namespace Netina.Repository.Handlers.Reviews;
public class GetReviewQueryHandler(IRepositoryWrapper repositoryWrapper) : IRequestHandler<GetReviewQuery, ReviewLDto>
{
public async Task<ReviewLDto> Handle(GetReviewQuery request, CancellationToken cancellationToken)
{
var review = await repositoryWrapper.SetRepository<Review>()
.TableNoTracking
.Where(r => r.Id == request.Id)
.Select(ReviewMapper.ProjectToLDto)
.FirstOrDefaultAsync(cancellationToken);
if (review == null)
throw new AppException("Review not found", ApiResultStatusCode.NotFound);
return review;
}
}

View File

@ -1,22 +0,0 @@
using Review = Netina.Domain.Entities.Reviews.Review;
namespace Netina.Repository.Handlers.Reviews;
public class GetReviewsQueryHandler(IRepositoryWrapper repositoryWrapper)
: IRequestHandler<GetReviewsQuery, List<ReviewSDto>>
{
public async Task<List<ReviewSDto>> Handle(GetReviewsQuery request, CancellationToken cancellationToken)
{
var query = repositoryWrapper.SetRepository<Review>()
.TableNoTracking;
if (request.ProductId != null)
query = query.Where(q => q.ProductId == request.ProductId);
return await query
.OrderByDescending(r => r.CreatedAt)
.Skip(request.Page * 15)
.Take(15)
.Select(ReviewMapper.ProjectToSDto)
.ToListAsync(cancellationToken);
}
}

View File

@ -1,24 +0,0 @@
using FluentValidation;
namespace Netina.Repository.Handlers.Reviews.Validators;
public class CreateReviewCommandValidator : AbstractValidator<CreateReviewCommand>
{
public CreateReviewCommandValidator()
{
RuleFor(d => d.Title)
.NotNull()
.NotEmpty()
.WithMessage("عنوان نظر مورد نظر را وارد کنید");
RuleFor(d => d.Comment)
.NotNull()
.NotEmpty()
.WithMessage("متن نظر مورد نظر را وارد کنید");
RuleFor(d => d.ProductId)
.NotEqual(Guid.Empty)
.NotEmpty()
.WithMessage("کالا مورد نظر را وارد کنید");
}
}

View File

@ -1,24 +0,0 @@
using FluentValidation;
namespace Netina.Repository.Handlers.Reviews.Validators;
public class UpdateReviewCommandValidator : AbstractValidator<UpdateReviewCommand>
{
public UpdateReviewCommandValidator()
{
RuleFor(d => d.Title)
.NotNull()
.NotEmpty()
.WithMessage("عنوان نظر مورد نظر را وارد کنید");
RuleFor(d => d.Comment)
.NotNull()
.NotEmpty()
.WithMessage("متن نظر مورد نظر را وارد کنید");
RuleFor(d => d.ProductId)
.NotEqual(Guid.Empty)
.NotEmpty()
.WithMessage("کالا مورد نظر را وارد کنید");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,162 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NetinaShop.Repository.Migrations
{
/// <inheritdoc />
public partial class ChangeToComment : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Reviews",
schema: "public");
migrationBuilder.CreateTable(
name: "Comments",
schema: "public",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Title = table.Column<string>(type: "text", nullable: false),
Content = table.Column<string>(type: "text", nullable: false),
Rate = table.Column<float>(type: "real", nullable: false),
IsConfirmed = table.Column<bool>(type: "boolean", nullable: false),
IsAdmin = table.Column<bool>(type: "boolean", nullable: false),
IsRoot = table.Column<bool>(type: "boolean", nullable: false),
ParentId = table.Column<Guid>(type: "uuid", nullable: true),
UserId = table.Column<Guid>(type: "uuid", nullable: false),
Discriminator = table.Column<string>(type: "character varying(21)", maxLength: 21, nullable: false),
BlogId = table.Column<Guid>(type: "uuid", nullable: true),
IsBuyer = table.Column<bool>(type: "boolean", nullable: true),
ProductId = table.Column<Guid>(type: "uuid", nullable: true),
RemovedAt = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
CreatedBy = table.Column<string>(type: "text", nullable: false),
IsRemoved = table.Column<bool>(type: "boolean", nullable: false),
RemovedBy = table.Column<string>(type: "text", nullable: false),
ModifiedAt = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
ModifiedBy = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Comments", x => x.Id);
table.ForeignKey(
name: "FK_Comments_Blogs_BlogId",
column: x => x.BlogId,
principalSchema: "public",
principalTable: "Blogs",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Comments_Comments_ParentId",
column: x => x.ParentId,
principalSchema: "public",
principalTable: "Comments",
principalColumn: "Id");
table.ForeignKey(
name: "FK_Comments_Products_ProductId",
column: x => x.ProductId,
principalSchema: "public",
principalTable: "Products",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Comments_Users_UserId",
column: x => x.UserId,
principalSchema: "public",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_Comments_BlogId",
schema: "public",
table: "Comments",
column: "BlogId");
migrationBuilder.CreateIndex(
name: "IX_Comments_ParentId",
schema: "public",
table: "Comments",
column: "ParentId");
migrationBuilder.CreateIndex(
name: "IX_Comments_ProductId",
schema: "public",
table: "Comments",
column: "ProductId");
migrationBuilder.CreateIndex(
name: "IX_Comments_UserId",
schema: "public",
table: "Comments",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Comments",
schema: "public");
migrationBuilder.CreateTable(
name: "Reviews",
schema: "public",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
ProductId = table.Column<Guid>(type: "uuid", nullable: false),
UserId = table.Column<Guid>(type: "uuid", nullable: false),
Comment = table.Column<string>(type: "text", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
CreatedBy = table.Column<string>(type: "text", nullable: false),
IsAdmin = table.Column<bool>(type: "boolean", nullable: false),
IsBuyer = table.Column<bool>(type: "boolean", nullable: false),
IsConfirmed = table.Column<bool>(type: "boolean", nullable: false),
IsRemoved = table.Column<bool>(type: "boolean", nullable: false),
ModifiedAt = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
ModifiedBy = table.Column<string>(type: "text", nullable: false),
Rate = table.Column<float>(type: "real", nullable: false),
RemovedAt = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
RemovedBy = table.Column<string>(type: "text", nullable: false),
Title = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Reviews", x => x.Id);
table.ForeignKey(
name: "FK_Reviews_Products_ProductId",
column: x => x.ProductId,
principalSchema: "public",
principalTable: "Products",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Reviews_Users_UserId",
column: x => x.UserId,
principalSchema: "public",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_Reviews_ProductId",
schema: "public",
table: "Reviews",
column: "ProductId");
migrationBuilder.CreateIndex(
name: "IX_Reviews_UserId",
schema: "public",
table: "Reviews",
column: "UserId");
}
}
}

View File

@ -18,7 +18,7 @@ namespace NetinaShop.Repository.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("public")
.HasAnnotation("ProductVersion", "8.0.7")
.HasAnnotation("ProductVersion", "8.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "fuzzystrmatch");
@ -386,6 +386,80 @@ namespace NetinaShop.Repository.Migrations
b.ToTable("Brands", "public");
});
modelBuilder.Entity("Netina.Domain.Entities.Comments.Comment", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp without time zone");
b.Property<string>("CreatedBy")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Discriminator")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("character varying(21)");
b.Property<bool>("IsAdmin")
.HasColumnType("boolean");
b.Property<bool>("IsConfirmed")
.HasColumnType("boolean");
b.Property<bool>("IsRemoved")
.HasColumnType("boolean");
b.Property<bool>("IsRoot")
.HasColumnType("boolean");
b.Property<DateTime>("ModifiedAt")
.HasColumnType("timestamp without time zone");
b.Property<string>("ModifiedBy")
.IsRequired()
.HasColumnType("text");
b.Property<Guid?>("ParentId")
.HasColumnType("uuid");
b.Property<float>("Rate")
.HasColumnType("real");
b.Property<DateTime>("RemovedAt")
.HasColumnType("timestamp without time zone");
b.Property<string>("RemovedBy")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("ParentId");
b.HasIndex("UserId");
b.ToTable("Comments", "public");
b.HasDiscriminator().HasValue("Comment");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("Netina.Domain.Entities.Discounts.Discount", b =>
{
b.Property<Guid>("Id")
@ -958,71 +1032,6 @@ namespace NetinaShop.Repository.Migrations
b.ToTable("Specifications", "public");
});
modelBuilder.Entity("Netina.Domain.Entities.Reviews.Review", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Comment")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp without time zone");
b.Property<string>("CreatedBy")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("IsAdmin")
.HasColumnType("boolean");
b.Property<bool>("IsBuyer")
.HasColumnType("boolean");
b.Property<bool>("IsConfirmed")
.HasColumnType("boolean");
b.Property<bool>("IsRemoved")
.HasColumnType("boolean");
b.Property<DateTime>("ModifiedAt")
.HasColumnType("timestamp without time zone");
b.Property<string>("ModifiedBy")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("ProductId")
.HasColumnType("uuid");
b.Property<float>("Rate")
.HasColumnType("real");
b.Property<DateTime>("RemovedAt")
.HasColumnType("timestamp without time zone");
b.Property<string>("RemovedBy")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("ProductId");
b.HasIndex("UserId");
b.ToTable("Reviews", "public");
});
modelBuilder.Entity("Netina.Domain.Entities.StorageFiles.StorageFile", b =>
{
b.Property<Guid>("Id")
@ -1574,6 +1583,33 @@ namespace NetinaShop.Repository.Migrations
b.ToTable("Shippings", "public");
});
modelBuilder.Entity("Netina.Domain.Entities.Blogs.BlogComment", b =>
{
b.HasBaseType("Netina.Domain.Entities.Comments.Comment");
b.Property<Guid>("BlogId")
.HasColumnType("uuid");
b.HasIndex("BlogId");
b.HasDiscriminator().HasValue("BlogComment");
});
modelBuilder.Entity("Netina.Domain.Entities.Products.ProductComment", b =>
{
b.HasBaseType("Netina.Domain.Entities.Comments.Comment");
b.Property<bool>("IsBuyer")
.HasColumnType("boolean");
b.Property<Guid>("ProductId")
.HasColumnType("uuid");
b.HasIndex("ProductId");
b.HasDiscriminator().HasValue("ProductComment");
});
modelBuilder.Entity("Netina.Domain.Entities.Discounts.CategoryDiscount", b =>
{
b.HasBaseType("Netina.Domain.Entities.Discounts.Discount");
@ -1734,6 +1770,22 @@ namespace NetinaShop.Repository.Migrations
b.Navigation("Parent");
});
modelBuilder.Entity("Netina.Domain.Entities.Comments.Comment", b =>
{
b.HasOne("Netina.Domain.Entities.Comments.Comment", "Parent")
.WithMany("Children")
.HasForeignKey("ParentId");
b.HasOne("Netina.Domain.Entities.Users.ApplicationUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Parent");
b.Navigation("User");
});
modelBuilder.Entity("Netina.Domain.Entities.Discounts.Discount", b =>
{
b.HasOne("Netina.Domain.Entities.Users.Marketer", "Marketer")
@ -1847,23 +1899,6 @@ namespace NetinaShop.Repository.Migrations
b.Navigation("Product");
});
modelBuilder.Entity("Netina.Domain.Entities.Reviews.Review", b =>
{
b.HasOne("Netina.Domain.Entities.Products.Product", "Product")
.WithMany("Reviews")
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("Netina.Domain.Entities.Users.ApplicationUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Product");
b.Navigation("User");
});
modelBuilder.Entity("Netina.Domain.Entities.Users.Customer", b =>
{
b.HasOne("Netina.Domain.Entities.Users.ApplicationUser", "User")
@ -1921,6 +1956,26 @@ namespace NetinaShop.Repository.Migrations
b.Navigation("User");
});
modelBuilder.Entity("Netina.Domain.Entities.Blogs.BlogComment", b =>
{
b.HasOne("Netina.Domain.Entities.Blogs.Blog", "Blog")
.WithMany()
.HasForeignKey("BlogId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Blog");
});
modelBuilder.Entity("Netina.Domain.Entities.Products.ProductComment", b =>
{
b.HasOne("Netina.Domain.Entities.Products.Product", "Product")
.WithMany("Comments")
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Product");
});
modelBuilder.Entity("Netina.Domain.Entities.Discounts.CategoryDiscount", b =>
{
b.HasOne("Netina.Domain.Entities.ProductCategories.ProductCategory", "Category")
@ -1998,6 +2053,11 @@ namespace NetinaShop.Repository.Migrations
b.Navigation("Products");
});
modelBuilder.Entity("Netina.Domain.Entities.Comments.Comment", b =>
{
b.Navigation("Children");
});
modelBuilder.Entity("Netina.Domain.Entities.Discounts.Discount", b =>
{
b.Navigation("Orders");
@ -2021,12 +2081,12 @@ namespace NetinaShop.Repository.Migrations
modelBuilder.Entity("Netina.Domain.Entities.Products.Product", b =>
{
b.Navigation("Comments");
b.Navigation("Files");
b.Navigation("OrderProducts");
b.Navigation("Reviews");
b.Navigation("Specifications");
});

View File

@ -7,7 +7,6 @@ public class ApplicationContext(DbContextOptions<ApplicationContext> options, IL
: IdentityDbContext<ApplicationUser, ApplicationRole, Guid>(options)
{
private readonly Assembly _projectAssembly = options.GetExtension<DbContextOptionCustomExtensions>().ProjectAssembly;
protected override void OnModelCreating(ModelBuilder builder)
{
var stopwatch = new Stopwatch();

View File

@ -9,22 +9,22 @@
<ItemGroup>
<PackageReference Include="FuzzySharp" Version="2.0.2" />
<PackageReference Include="MediatR" Version="12.4.0" />
<PackageReference Include="FluentValidation" Version="11.9.0" />
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="FluentValidation" Version="11.10.0" />
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.7">
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Pluralize.NET" Version="1.0.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="StackExchange.Redis" Version="2.7.33" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
<PackageReference Include="StackExchange.Redis.Extensions.Core" Version="10.2.0" />
</ItemGroup>
@ -59,15 +59,17 @@
<Using Include="Netina.Domain.Dtos.LargDtos" />
<Using Include="Netina.Domain.Dtos.ResponseDtos" />
<Using Include="Netina.Domain.Dtos.SmallDtos" />
<Using Include="Netina.Domain.Entities.Blogs" />
<Using Include="Netina.Domain.Entities.Discounts" />
<Using Include="Netina.Domain.Entities.ProductCategories" />
<Using Include="Netina.Domain.Entities.Products" />
<Using Include="Netina.Domain.Entities.Reviews" />
<Using Include="Netina.Domain.Entities.Comments" />
<Using Include="Netina.Domain.Entities.Users" />
<Using Include="Netina.Domain.Enums" />
<Using Include="Netina.Domain.Extensions" />
<Using Include="Netina.Domain.Mappers" />
<Using Include="Netina.Domain.MartenEntities.Faqs" />
<Using Include="Netina.Domain.Models" />
<Using Include="Netina.Domain.Models.Claims" />
<Using Include="Netina.Domain.Models.Settings" />
<Using Include="Netina.Repository.Abstracts" />

View File

@ -8,10 +8,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.62" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.66" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Refit" Version="7.0.0" />
<PackageReference Include="Refit.Newtonsoft.Json" Version="7.0.0" />
<PackageReference Include="Refit" Version="7.2.1" />
<PackageReference Include="Refit.Newtonsoft.Json" Version="7.2.1" />
</ItemGroup>
<ItemGroup>