feat(AppVersion): Add version tracking system for frontend cache invalidation
Build and Deploy to Production / build-and-deploy (push) Successful in 1m37s

- Add AppVersion entity with CurrentVersion, MinRequiredVersion, RequiresFullCacheClear
- Add GetAppVersion query to check app version and compare with client version
- Add GetAllAppVersions query for admin panel
- Add UpdateAppVersion command to update/create app versions
- Add appversion.proto for gRPC communication
- Add AppVersionService for gRPC endpoints
- Add database migration for AppVersions table
This commit is contained in:
masoodafar-web
2025-12-25 18:06:12 +03:30
parent 2e54aa7aca
commit 17c2958b9c
15 changed files with 4244 additions and 0 deletions
@@ -0,0 +1,42 @@
namespace CMSMicroservice.Application.AppVersionCQ.Commands.UpdateAppVersion;
/// <summary>
/// Command برای آپدیت یا ایجاد نسخه جدید اپلیکیشن
/// </summary>
public record UpdateAppVersionCommand : IRequest<Unit>
{
/// <summary>
/// نام اپلیکیشن (FrontOffice, BackOffice, MobileApp)
/// </summary>
public string AppName { get; init; } = string.Empty;
/// <summary>
/// شماره نسخه جدید (مثلاً 1.2.3)
/// </summary>
public string CurrentVersion { get; init; } = string.Empty;
/// <summary>
/// حداقل نسخه مورد نیاز
/// </summary>
public string? MinRequiredVersion { get; init; }
/// <summary>
/// آیا کش کامل باید پاک بشه؟
/// </summary>
public bool RequiresFullCacheClear { get; init; }
/// <summary>
/// پیام آپدیت
/// </summary>
public string? UpdateMessage { get; init; }
/// <summary>
/// توضیحات تغییرات این نسخه
/// </summary>
public string? ReleaseNotes { get; init; }
/// <summary>
/// دلیل آپدیت (برای لاگ)
/// </summary>
public string? UpdateReason { get; init; }
}
@@ -0,0 +1,65 @@
using CMSMicroservice.Domain.Entities.Configuration;
namespace CMSMicroservice.Application.AppVersionCQ.Commands.UpdateAppVersion;
/// <summary>
/// Handler برای آپدیت یا ایجاد نسخه جدید اپلیکیشن
/// </summary>
public class UpdateAppVersionCommandHandler : IRequestHandler<UpdateAppVersionCommand, Unit>
{
private readonly IApplicationDbContext _context;
private readonly ILogger<UpdateAppVersionCommandHandler> _logger;
public UpdateAppVersionCommandHandler(
IApplicationDbContext context,
ILogger<UpdateAppVersionCommandHandler> logger)
{
_context = context;
_logger = logger;
}
public async Task<Unit> Handle(UpdateAppVersionCommand request, CancellationToken cancellationToken)
{
// پیدا کردن نسخه موجود برای این اپ
var existingVersion = await _context.AppVersions
.Where(v => v.AppName == request.AppName && !v.IsDeleted)
.FirstOrDefaultAsync(cancellationToken);
if (existingVersion != null)
{
// آپدیت نسخه موجود
existingVersion.CurrentVersion = request.CurrentVersion;
existingVersion.MinRequiredVersion = request.MinRequiredVersion ?? request.CurrentVersion;
existingVersion.RequiresFullCacheClear = request.RequiresFullCacheClear;
existingVersion.UpdateMessage = request.UpdateMessage;
existingVersion.ReleaseNotes = request.ReleaseNotes;
_logger.LogInformation(
"App version updated: {AppName} v{Version}, CacheClear={CacheClear}, Reason={Reason}",
request.AppName, request.CurrentVersion, request.RequiresFullCacheClear, request.UpdateReason);
}
else
{
// ایجاد نسخه جدید
var newVersion = new AppVersion
{
AppName = request.AppName,
CurrentVersion = request.CurrentVersion,
MinRequiredVersion = request.MinRequiredVersion ?? request.CurrentVersion,
RequiresFullCacheClear = request.RequiresFullCacheClear,
UpdateMessage = request.UpdateMessage,
ReleaseNotes = request.ReleaseNotes,
IsActive = true
};
_context.AppVersions.Add(newVersion);
_logger.LogInformation(
"New app version created: {AppName} v{Version}, CacheClear={CacheClear}, Reason={Reason}",
request.AppName, request.CurrentVersion, request.RequiresFullCacheClear, request.UpdateReason);
}
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}
@@ -0,0 +1,23 @@
namespace CMSMicroservice.Application.AppVersionCQ.Queries.GetAllAppVersions;
/// <summary>
/// Query برای دریافت همه نسخه‌های اپلیکیشن‌ها
/// </summary>
public record GetAllAppVersionsQuery(bool IncludeInactive = false) : IRequest<List<AppVersionItemDto>>;
/// <summary>
/// DTO برای هر آیتم نسخه اپلیکیشن
/// </summary>
public record AppVersionItemDto
{
public long Id { get; init; }
public string AppName { get; init; } = string.Empty;
public string CurrentVersion { get; init; } = string.Empty;
public string MinRequiredVersion { get; init; } = string.Empty;
public bool RequiresFullCacheClear { get; init; }
public string? UpdateMessage { get; init; }
public string? ReleaseNotes { get; init; }
public bool IsActive { get; init; }
public DateTime Created { get; init; }
public DateTime? LastModified { get; init; }
}
@@ -0,0 +1,44 @@
namespace CMSMicroservice.Application.AppVersionCQ.Queries.GetAllAppVersions;
/// <summary>
/// Handler برای دریافت همه نسخه‌های اپلیکیشن‌ها
/// </summary>
public class GetAllAppVersionsQueryHandler : IRequestHandler<GetAllAppVersionsQuery, List<AppVersionItemDto>>
{
private readonly IApplicationDbContext _context;
public GetAllAppVersionsQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<List<AppVersionItemDto>> Handle(GetAllAppVersionsQuery request, CancellationToken cancellationToken)
{
var query = _context.AppVersions
.Where(v => !v.IsDeleted);
if (!request.IncludeInactive)
{
query = query.Where(v => v.IsActive);
}
var versions = await query
.OrderBy(v => v.AppName)
.Select(v => new AppVersionItemDto
{
Id = v.Id,
AppName = v.AppName,
CurrentVersion = v.CurrentVersion,
MinRequiredVersion = v.MinRequiredVersion,
RequiresFullCacheClear = v.RequiresFullCacheClear,
UpdateMessage = v.UpdateMessage,
ReleaseNotes = v.ReleaseNotes,
IsActive = v.IsActive,
Created = v.Created,
LastModified = v.LastModified
})
.ToListAsync(cancellationToken);
return versions;
}
}
@@ -0,0 +1,22 @@
namespace CMSMicroservice.Application.AppVersionCQ.Queries.GetAppVersion;
/// <summary>
/// Query برای دریافت آخرین نسخه یک اپلیکیشن
/// </summary>
public record GetAppVersionQuery(string AppName, string? CurrentClientVersion = null) : IRequest<AppVersionDto?>;
/// <summary>
/// DTO برای اطلاعات نسخه اپلیکیشن
/// </summary>
public record AppVersionDto
{
public bool Found { get; init; }
public string AppName { get; init; } = string.Empty;
public string CurrentVersion { get; init; } = string.Empty;
public string MinRequiredVersion { get; init; } = string.Empty;
public bool RequiresFullCacheClear { get; init; }
public bool RequiresUpdate { get; init; }
public string? UpdateMessage { get; init; }
public string? ReleaseNotes { get; init; }
public DateTime? LastUpdated { get; init; }
}
@@ -0,0 +1,75 @@
using CMSMicroservice.Domain.Entities.Configuration;
namespace CMSMicroservice.Application.AppVersionCQ.Queries.GetAppVersion;
/// <summary>
/// Handler برای دریافت آخرین نسخه اپلیکیشن
/// </summary>
public class GetAppVersionQueryHandler : IRequestHandler<GetAppVersionQuery, AppVersionDto?>
{
private readonly IApplicationDbContext _context;
public GetAppVersionQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<AppVersionDto?> Handle(GetAppVersionQuery request, CancellationToken cancellationToken)
{
var appVersion = await _context.AppVersions
.Where(v => v.AppName == request.AppName && v.IsActive && !v.IsDeleted)
.FirstOrDefaultAsync(cancellationToken);
if (appVersion == null)
{
return new AppVersionDto
{
Found = false,
AppName = request.AppName
};
}
// مقایسه ورژن کلاینت با حداقل نسخه مورد نیاز
bool requiresUpdate = false;
if (!string.IsNullOrEmpty(request.CurrentClientVersion) && !string.IsNullOrEmpty(appVersion.MinRequiredVersion))
{
requiresUpdate = CompareVersions(request.CurrentClientVersion, appVersion.MinRequiredVersion) < 0;
}
return new AppVersionDto
{
Found = true,
AppName = appVersion.AppName,
CurrentVersion = appVersion.CurrentVersion,
MinRequiredVersion = appVersion.MinRequiredVersion,
RequiresFullCacheClear = appVersion.RequiresFullCacheClear,
RequiresUpdate = requiresUpdate,
UpdateMessage = appVersion.UpdateMessage,
ReleaseNotes = appVersion.ReleaseNotes,
LastUpdated = appVersion.LastModified ?? appVersion.Created
};
}
/// <summary>
/// مقایسه دو نسخه (مثلاً 1.2.3 با 1.3.0)
/// برگشت: منفی = اولی کوچکتر، مثبت = اولی بزرگتر، صفر = برابر
/// </summary>
private static int CompareVersions(string version1, string version2)
{
var v1Parts = version1.Split('.').Select(s => int.TryParse(s, out var n) ? n : 0).ToArray();
var v2Parts = version2.Split('.').Select(s => int.TryParse(s, out var n) ? n : 0).ToArray();
var maxLen = Math.Max(v1Parts.Length, v2Parts.Length);
for (int i = 0; i < maxLen; i++)
{
var v1 = i < v1Parts.Length ? v1Parts[i] : 0;
var v2 = i < v2Parts.Length ? v2Parts[i] : 0;
if (v1 != v2)
return v1.CompareTo(v2);
}
return 0;
}
}
@@ -47,6 +47,7 @@ public interface IApplicationDbContext
DbSet<CommissionPayoutHistory> CommissionPayoutHistories { get; } DbSet<CommissionPayoutHistory> CommissionPayoutHistories { get; }
DbSet<WorkerExecutionLog> WorkerExecutionLogs { get; } DbSet<WorkerExecutionLog> WorkerExecutionLogs { get; }
DbSet<DayaLoanContract> DayaLoanContracts { get; } DbSet<DayaLoanContract> DayaLoanContracts { get; }
DbSet<AppVersion> AppVersions { get; }
// ============= Discount Shop ============= // ============= Discount Shop =============
DbSet<DiscountProduct> DiscountProducts { get; } DbSet<DiscountProduct> DiscountProducts { get; }
@@ -0,0 +1,43 @@
namespace CMSMicroservice.Domain.Entities.Configuration;
/// <summary>
/// نسخه اپلیکیشن‌های فرانت‌اند
/// وقتی ورژن آپدیت بشه، فرانت‌ها باید کش و داده‌های محلی رو پاک کنن
/// </summary>
public class AppVersion : BaseAuditableEntity
{
/// <summary>
/// نام اپلیکیشن (FrontOffice, BackOffice, MobileApp)
/// </summary>
public string AppName { get; set; } = string.Empty;
/// <summary>
/// شماره نسخه فعلی (مثلاً 1.2.3)
/// </summary>
public string CurrentVersion { get; set; } = string.Empty;
/// <summary>
/// حداقل نسخه مورد نیاز - اگر کاربر از این پایین‌تر باشه باید آپدیت کنه
/// </summary>
public string MinRequiredVersion { get; set; } = string.Empty;
/// <summary>
/// آیا کش کامل باید پاک بشه؟
/// </summary>
public bool RequiresFullCacheClear { get; set; }
/// <summary>
/// پیام آپدیت برای نمایش به کاربر
/// </summary>
public string? UpdateMessage { get; set; }
/// <summary>
/// توضیحات تغییرات این نسخه
/// </summary>
public string? ReleaseNotes { get; set; }
/// <summary>
/// فعال یا غیرفعال
/// </summary>
public bool IsActive { get; set; } = true;
}
@@ -87,6 +87,7 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext
// Configuration // Configuration
public DbSet<SystemConfiguration> SystemConfigurations => Set<SystemConfiguration>(); public DbSet<SystemConfiguration> SystemConfigurations => Set<SystemConfiguration>();
public DbSet<SystemConfigurationHistory> SystemConfigurationHistories => Set<SystemConfigurationHistory>(); public DbSet<SystemConfigurationHistory> SystemConfigurationHistories => Set<SystemConfigurationHistory>();
public DbSet<AppVersion> AppVersions => Set<AppVersion>();
// Club Management // Club Management
public DbSet<ClubMembership> ClubMemberships => Set<ClubMembership>(); public DbSet<ClubMembership> ClubMemberships => Set<ClubMembership>();
@@ -0,0 +1,48 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddAppVersionsTable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AppVersions",
schema: "CMS",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
AppName = table.Column<string>(type: "nvarchar(max)", nullable: false),
CurrentVersion = table.Column<string>(type: "nvarchar(max)", nullable: false),
MinRequiredVersion = table.Column<string>(type: "nvarchar(max)", nullable: false),
RequiresFullCacheClear = table.Column<bool>(type: "bit", nullable: false),
UpdateMessage = table.Column<string>(type: "nvarchar(max)", nullable: true),
ReleaseNotes = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false),
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AppVersions", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AppVersions",
schema: "CMS");
}
}
}
@@ -451,6 +451,58 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.ToTable("WorkerExecutionLogs", "CMS"); b.ToTable("WorkerExecutionLogs", "CMS");
}); });
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Configuration.AppVersion", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<string>("AppName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("Created")
.HasColumnType("datetime2");
b.Property<string>("CreatedBy")
.HasColumnType("nvarchar(max)");
b.Property<string>("CurrentVersion")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<DateTime?>("LastModified")
.HasColumnType("datetime2");
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<string>("MinRequiredVersion")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("ReleaseNotes")
.HasColumnType("nvarchar(max)");
b.Property<bool>("RequiresFullCacheClear")
.HasColumnType("bit");
b.Property<string>("UpdateMessage")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("AppVersions", "CMS");
});
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Configuration.SystemConfiguration", b => modelBuilder.Entity("CMSMicroservice.Domain.Entities.Configuration.SystemConfiguration", b =>
{ {
b.Property<long>("Id") b.Property<long>("Id")
@@ -57,6 +57,8 @@
<Protobuf Include="Protos\discountorder.proto" ProtoRoot="Protos\" GrpcServices="Both" /> <Protobuf Include="Protos\discountorder.proto" ProtoRoot="Protos\" GrpcServices="Both" />
<!-- Geography System (GMS) --> <!-- Geography System (GMS) -->
<Protobuf Include="Protos\city.proto" ProtoRoot="Protos\" GrpcServices="Both" /> <Protobuf Include="Protos\city.proto" ProtoRoot="Protos\" GrpcServices="Both" />
<!-- App Version Tracking System -->
<Protobuf Include="Protos\appversion.proto" ProtoRoot="Protos\" GrpcServices="Both" />
</ItemGroup> </ItemGroup>
<Target Name="PushToFoursatNuget" AfterTargets="Pack"> <Target Name="PushToFoursatNuget" AfterTargets="Pack">
@@ -0,0 +1,95 @@
syntax = "proto3";
package appversion;
import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/timestamp.proto";
import "google/api/annotations.proto";
option csharp_namespace = "CMSMicroservice.Protobuf.Protos.AppVersion";
// Service for tracking app versions and cache invalidation
service AppVersionContract
{
// Get current version for an app - called by frontends to check if cache clear is needed
rpc GetAppVersion(GetAppVersionRequest) returns (GetAppVersionResponse){
option (google.api.http) = {
get: "/AppVersion/Get"
};
};
// Update/Create app version - called by admin when deploying new version
rpc UpdateAppVersion(UpdateAppVersionRequest) returns (google.protobuf.Empty){
option (google.api.http) = {
post: "/AppVersion/Update"
body: "*"
};
};
// Get all app versions
rpc GetAllAppVersions(GetAllAppVersionsRequest) returns (GetAllAppVersionsResponse){
option (google.api.http) = {
get: "/AppVersion/GetAll"
};
};
}
// Request to get current version for specific app
message GetAppVersionRequest
{
string app_name = 1; // e.g., "FrontOffice", "BackOffice", "Mobile"
google.protobuf.StringValue current_client_version = 2; // Current version on client for comparison
}
// Response with version info
message GetAppVersionResponse
{
bool found = 1; // Whether the app was found
string app_name = 2;
string current_version = 3;
string min_required_version = 4;
bool requires_full_cache_clear = 5;
bool requires_update = 6; // True if client version < min_required_version
google.protobuf.StringValue update_message = 7;
google.protobuf.StringValue release_notes = 8;
google.protobuf.Timestamp last_updated = 9;
}
// Request to update/create app version
message UpdateAppVersionRequest
{
string app_name = 1;
string current_version = 2;
google.protobuf.StringValue min_required_version = 3;
bool requires_full_cache_clear = 4;
google.protobuf.StringValue update_message = 5;
google.protobuf.StringValue release_notes = 6;
google.protobuf.StringValue update_reason = 7; // For admin logs
}
// Request to get all app versions
message GetAllAppVersionsRequest
{
bool include_inactive = 1;
}
// Response with all app versions
message GetAllAppVersionsResponse
{
repeated AppVersionItem items = 1;
}
message AppVersionItem
{
int64 id = 1;
string app_name = 2;
string current_version = 3;
string min_required_version = 4;
bool requires_full_cache_clear = 5;
string update_message = 6;
string release_notes = 7;
bool is_active = 8;
google.protobuf.Timestamp created = 9;
google.protobuf.Timestamp last_modified = 10;
}
@@ -0,0 +1,32 @@
using CMSMicroservice.Protobuf.Protos.AppVersion;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.AppVersionCQ.Queries.GetAppVersion;
using CMSMicroservice.Application.AppVersionCQ.Queries.GetAllAppVersions;
using CMSMicroservice.Application.AppVersionCQ.Commands.UpdateAppVersion;
namespace CMSMicroservice.WebApi.Services;
public class AppVersionService : AppVersionContract.AppVersionContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
public AppVersionService(IDispatchRequestToCQRS dispatchRequestToCQRS)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
public override async Task<GetAppVersionResponse> GetAppVersion(GetAppVersionRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetAppVersionRequest, GetAppVersionQuery, GetAppVersionResponse>(request, context);
}
public override async Task<Empty> UpdateAppVersion(UpdateAppVersionRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdateAppVersionRequest, UpdateAppVersionCommand>(request, context);
}
public override async Task<GetAllAppVersionsResponse> GetAllAppVersions(GetAllAppVersionsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetAllAppVersionsRequest, GetAllAppVersionsQuery, GetAllAppVersionsResponse>(request, context);
}
}