feat: Add SitePageSettings - simplified page management system
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m2s

- New entities: SitePageSettings + SitePageImage (4 fixed pages)
- EF Migration: AddSitePageSettings (SitePageSettings + SitePageImages tables)
- Proto: sitepagesettings.proto with 5 RPCs
- CQRS: GetPageSettings, GetAllPageSettings, SavePageSettings, SavePageImage, DeletePageImage
- gRPC Service: SitePageSettingsService (auto-registered)
- Proto NuGet bumped to 0.0.180
This commit is contained in:
masoodafar-web
2026-02-17 22:34:29 +03:30
parent 89e146bf32
commit fd24dcebcd
18 changed files with 5860 additions and 1 deletions
@@ -0,0 +1,36 @@
using CMSMicroservice.Application.Common.Interfaces;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.SitePageSettingsCQ.Commands.DeletePageImage;
// ── Command ──
public class DeletePageImageCommand : IRequest<Unit>
{
public long Id { get; set; }
}
// ── Handler ──
public class DeletePageImageCommandHandler : IRequestHandler<DeletePageImageCommand, Unit>
{
private readonly IApplicationDbContext _context;
public DeletePageImageCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<Unit> Handle(DeletePageImageCommand request, CancellationToken cancellationToken)
{
var entity = await _context.SitePageImages
.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, cancellationToken);
if (entity != null)
{
entity.IsDeleted = true; // Soft delete
await _context.SaveChangesAsync(cancellationToken);
}
return Unit.Value;
}
}
@@ -0,0 +1,96 @@
using CMSMicroservice.Application.Common.FileManager;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities.Content;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.SitePageSettingsCQ.Commands.SavePageImage;
// ── Command ──
public class SavePageImageCommand : IRequest<SavePageImageResult>
{
public long Id { get; set; } // 0 = ایجاد جدید
public long SitePageSettingsId { get; set; }
public string ImageGroup { get; set; } = default!;
public string? Title { get; set; }
public string? Subtitle { get; set; }
public string? Description { get; set; }
public string? LinkUrl { get; set; }
public string? IconName { get; set; }
public int SortOrder { get; set; }
public bool IsActive { get; set; } = true;
// Image upload
public byte[]? ImageFileBytes { get; set; }
public string? ImageFileMime { get; set; }
public string? ImageFileName { get; set; }
}
public class SavePageImageResult
{
public long Id { get; set; }
public bool Success { get; set; }
}
// ── Handler ──
public class SavePageImageCommandHandler : IRequestHandler<SavePageImageCommand, SavePageImageResult>
{
private readonly IApplicationDbContext _context;
private readonly IFileManager _fileManager;
public SavePageImageCommandHandler(IApplicationDbContext context, IFileManager fileManager)
{
_context = context;
_fileManager = fileManager;
}
public async Task<SavePageImageResult> Handle(SavePageImageCommand request, CancellationToken cancellationToken)
{
SitePageImage entity;
if (request.Id > 0)
{
// ویرایش
entity = await _context.SitePageImages
.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, cancellationToken)
?? throw new Exception($"تصویر با شناسه {request.Id} یافت نشد");
}
else
{
// ایجاد جدید
entity = new SitePageImage
{
SitePageSettingsId = request.SitePageSettingsId,
ImageGroup = request.ImageGroup,
ImagePath = string.Empty // placeholder — will be set below
};
_context.SitePageImages.Add(entity);
}
entity.Title = request.Title;
entity.Subtitle = request.Subtitle;
entity.Description = request.Description;
entity.LinkUrl = request.LinkUrl;
entity.IconName = request.IconName;
entity.SortOrder = request.SortOrder;
entity.IsActive = request.IsActive;
// آپلود تصویر
if (request.ImageFileBytes is { Length: > 0 })
{
var result = await _fileManager.UploadImageAsync(
$"Images/SitePageSettings/{request.ImageGroup}",
request.ImageFileBytes,
request.ImageFileMime ?? "image/jpeg",
request.ImageFileName,
cancellationToken);
entity.ImagePath = result.Main.Path;
entity.ThumbnailPath = result.Thumbnail.Path;
}
await _context.SaveChangesAsync(cancellationToken);
return new SavePageImageResult { Id = entity.Id, Success = true };
}
}
@@ -0,0 +1,83 @@
using CMSMicroservice.Application.Common.FileManager;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities.Content;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.SitePageSettingsCQ.Commands.SavePageSettings;
// ── Command ──
public class SavePageSettingsCommand : IRequest<SavePageSettingsResult>
{
public string PageKey { get; set; } = default!;
public string Title { get; set; } = default!;
public string? MetaDescription { get; set; }
public string? HeroTitle { get; set; }
public string? HeroSubtitle { get; set; }
public bool IsActive { get; set; }
public string? SettingsJson { get; set; }
// Hero image upload
public byte[]? ImageFileBytes { get; set; }
public string? ImageFileMime { get; set; }
public string? ImageFileName { get; set; }
}
public class SavePageSettingsResult
{
public long Id { get; set; }
public bool Success { get; set; }
}
// ── Handler ──
public class SavePageSettingsCommandHandler : IRequestHandler<SavePageSettingsCommand, SavePageSettingsResult>
{
private readonly IApplicationDbContext _context;
private readonly IFileManager _fileManager;
public SavePageSettingsCommandHandler(IApplicationDbContext context, IFileManager fileManager)
{
_context = context;
_fileManager = fileManager;
}
public async Task<SavePageSettingsResult> Handle(SavePageSettingsCommand request, CancellationToken cancellationToken)
{
var entity = await _context.SitePageSettingsEntities
.FirstOrDefaultAsync(x => x.PageKey == request.PageKey && !x.IsDeleted, cancellationToken);
if (entity == null)
{
// ایجاد صفحه جدید (فقط برای seed اولیه — صفحات ثابت هستند)
entity = new SitePageSettings
{
PageKey = request.PageKey,
};
_context.SitePageSettingsEntities.Add(entity);
}
entity.Title = request.Title;
entity.MetaDescription = request.MetaDescription;
entity.HeroTitle = request.HeroTitle;
entity.HeroSubtitle = request.HeroSubtitle;
entity.IsActive = request.IsActive;
entity.SettingsJson = request.SettingsJson;
// آپلود تصویر هیرو
if (request.ImageFileBytes is { Length: > 0 })
{
var result = await _fileManager.UploadImageAsync(
"Images/SitePageSettings",
request.ImageFileBytes,
request.ImageFileMime ?? "image/jpeg",
request.ImageFileName,
cancellationToken);
entity.HeroImagePath = result.Main.Path;
}
await _context.SaveChangesAsync(cancellationToken);
return new SavePageSettingsResult { Id = entity.Id, Success = true };
}
}
@@ -0,0 +1,30 @@
using FluentValidation;
namespace CMSMicroservice.Application.SitePageSettingsCQ.Commands.SavePageSettings;
public class SavePageSettingsCommandValidator : AbstractValidator<SavePageSettingsCommand>
{
private static readonly string[] ValidPageKeys = { "landing", "about", "contact", "licenses" };
public SavePageSettingsCommandValidator()
{
RuleFor(x => x.PageKey)
.NotEmpty().WithMessage("کلید صفحه الزامی است")
.MaximumLength(50)
.Must(key => ValidPageKeys.Contains(key))
.WithMessage("کلید صفحه باید یکی از مقادیر landing, about, contact, licenses باشد");
RuleFor(x => x.Title)
.NotEmpty().WithMessage("عنوان صفحه الزامی است")
.MaximumLength(200).WithMessage("عنوان صفحه حداکثر ۲۰۰ کاراکتر");
RuleFor(x => x.MetaDescription)
.MaximumLength(300).WithMessage("توضیحات متا حداکثر ۳۰۰ کاراکتر");
RuleFor(x => x.HeroTitle)
.MaximumLength(200).WithMessage("عنوان هیرو حداکثر ۲۰۰ کاراکتر");
RuleFor(x => x.HeroSubtitle)
.MaximumLength(500).WithMessage("زیرعنوان هیرو حداکثر ۵۰۰ کاراکتر");
}
}
@@ -0,0 +1,49 @@
using CMSMicroservice.Application.Common.Interfaces;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.SitePageSettingsCQ.Queries.GetAllPageSettings;
// ── Query ──
public class GetAllPageSettingsQuery : IRequest<List<PageSettingsSummaryDto>>
{
}
// ── Handler ──
public class GetAllPageSettingsQueryHandler : IRequestHandler<GetAllPageSettingsQuery, List<PageSettingsSummaryDto>>
{
private readonly IApplicationDbContext _context;
public GetAllPageSettingsQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<List<PageSettingsSummaryDto>> Handle(GetAllPageSettingsQuery request, CancellationToken cancellationToken)
{
return await _context.SitePageSettingsEntities
.Where(x => !x.IsDeleted)
.OrderBy(x => x.Id)
.Select(x => new PageSettingsSummaryDto
{
Id = x.Id,
PageKey = x.PageKey,
Title = x.Title,
IsActive = x.IsActive,
ImageCount = x.Images.Count(i => !i.IsDeleted),
LastModified = x.LastModified ?? x.Created
})
.ToListAsync(cancellationToken);
}
}
// ── DTO ──
public class PageSettingsSummaryDto
{
public long Id { get; set; }
public string PageKey { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public bool IsActive { get; set; }
public int ImageCount { get; set; }
public DateTime LastModified { get; set; }
}
@@ -0,0 +1,92 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities.Content;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.SitePageSettingsCQ.Queries.GetPageSettings;
// ── Query ──
public class GetPageSettingsQuery : IRequest<PageSettingsDto?>
{
public string PageKey { get; set; } = default!;
}
// ── Handler ──
public class GetPageSettingsQueryHandler : IRequestHandler<GetPageSettingsQuery, PageSettingsDto?>
{
private readonly IApplicationDbContext _context;
public GetPageSettingsQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<PageSettingsDto?> Handle(GetPageSettingsQuery request, CancellationToken cancellationToken)
{
var entity = await _context.SitePageSettingsEntities
.Include(x => x.Images.Where(i => !i.IsDeleted && i.IsActive).OrderBy(i => i.SortOrder))
.FirstOrDefaultAsync(x => x.PageKey == request.PageKey && !x.IsDeleted, cancellationToken);
if (entity == null)
return null;
return MapToDto(entity);
}
public static PageSettingsDto MapToDto(SitePageSettings entity) => new()
{
Id = entity.Id,
PageKey = entity.PageKey,
Title = entity.Title,
MetaDescription = entity.MetaDescription,
HeroTitle = entity.HeroTitle,
HeroSubtitle = entity.HeroSubtitle,
HeroImagePath = entity.HeroImagePath,
IsActive = entity.IsActive,
SettingsJson = entity.SettingsJson,
Images = entity.Images.Select(i => new PageImageDto
{
Id = i.Id,
ImageGroup = i.ImageGroup,
Title = i.Title,
Subtitle = i.Subtitle,
Description = i.Description,
ImagePath = i.ImagePath,
ThumbnailPath = i.ThumbnailPath,
LinkUrl = i.LinkUrl,
IconName = i.IconName,
SortOrder = i.SortOrder,
IsActive = i.IsActive
}).ToList()
};
}
// ── DTOs ──
public class PageSettingsDto
{
public long Id { get; set; }
public string PageKey { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string? MetaDescription { get; set; }
public string? HeroTitle { get; set; }
public string? HeroSubtitle { get; set; }
public string? HeroImagePath { get; set; }
public bool IsActive { get; set; }
public string? SettingsJson { get; set; }
public List<PageImageDto> Images { get; set; } = new();
}
public class PageImageDto
{
public long Id { get; set; }
public string ImageGroup { get; set; } = string.Empty;
public string? Title { get; set; }
public string? Subtitle { get; set; }
public string? Description { get; set; }
public string ImagePath { get; set; } = string.Empty;
public string? ThumbnailPath { get; set; }
public string? LinkUrl { get; set; }
public string? IconName { get; set; }
public int SortOrder { get; set; }
public bool IsActive { get; set; }
}