upgrade:fake 8.0.5.2 fei shu notice,rem log helper
This commit is contained in:
parent
fb02eeeb21
commit
8c8647a0ca
@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!-- All Fake packages -->
|
||||
<FakePackageVersion>8.0.2</FakePackageVersion>
|
||||
<FakePackageVersion>8.0.5.2</FakePackageVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@ -1,22 +1,18 @@
|
||||
using Fake.AspNetCore;
|
||||
using Fake.AspNetCore.Auditing;
|
||||
using Fake.AspNetCore.Mvc;
|
||||
using Fake.AspNetCore.Mvc.Conventions;
|
||||
using Fake.AspNetCore.Mvc.Filters;
|
||||
using Fake.Authorization;
|
||||
using Fake.Autofac;
|
||||
using Fake.FeiShu;
|
||||
using Fake.Modularity;
|
||||
using Fake.Security.Claims;
|
||||
using Fake.SyncEx;
|
||||
using InterfaceForward.Application;
|
||||
using InterfaceForward.Application.Filters;
|
||||
using InterfaceForward.Domain.Shared;
|
||||
using InterfaceForward.Domain.Shared.Helpers;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Authorization;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace InterfaceForward.Api;
|
||||
|
||||
@ -124,12 +120,12 @@ public class InterfaceForwardApiModule : FakeModule
|
||||
app.UseForwardedHeaders();//Nginx代理的话获取真实IP
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
LogHelper.Info("Application started", true);
|
||||
|
||||
app.Services.GetRequiredService<IFeiShuNotificationService>().Send("Application started");
|
||||
}
|
||||
|
||||
public override void Shutdown(ApplicationShutdownContext context)
|
||||
{
|
||||
LogHelper.Info("Application stoped", true);
|
||||
context.ServiceProvider.GetRequiredService<IFeiShuNotificationService>().Send("Application stoped");
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,7 @@ using Serilog;
|
||||
var builder = WebApplication.CreateSlimBuilder(args);
|
||||
var configuration = builder.Configuration;
|
||||
builder.Configuration.AddConsul("idwms/itfxapi", source => source.ReloadOnChange = true);
|
||||
builder.Configuration.AddConsul("idwms/common");
|
||||
builder.Configuration.AddConsul("idwms/common", source => source.ReloadOnChange = true);
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(configuration)
|
||||
.CreateLogger();
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
using System.Text;
|
||||
using Fake.DependencyInjection;
|
||||
using Fake.ExceptionHandling;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace InterfaceForward.Application.Filters;
|
||||
|
||||
public class ExceptionNotifier : IExceptionNotifier, ITransientDependency
|
||||
{
|
||||
public async Task NotifyAsync(ExceptionNotificationContext context)
|
||||
{
|
||||
var httpContext = context.ServiceProvider.GetRequiredService<IHttpContextAccessor>().HttpContext;
|
||||
if (httpContext is null)
|
||||
{
|
||||
LogHelper.Error($@"
|
||||
|-报错时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss ffff}
|
||||
|-异常堆栈:{context.Exception} ");
|
||||
}
|
||||
|
||||
var request = httpContext!.Request;
|
||||
|
||||
// 1. 读取请求路径
|
||||
var path = request.Path; // 例如: /api/users
|
||||
var queryString = request.QueryString; // 例如: ?id=123
|
||||
var fullPath = $"{path}{queryString}";
|
||||
|
||||
// 2. 读取请求方法
|
||||
var method = request.Method; // GET, POST, PUT, etc.
|
||||
|
||||
// 3. 读取请求头
|
||||
var headers = request.Headers;
|
||||
|
||||
// 4. 读取 Body (重要:需要启用缓冲)
|
||||
request.EnableBuffering();
|
||||
|
||||
// 如果 Body 已经被其他中间件读取过,需要先重置:
|
||||
request.Body.Position = 0;
|
||||
|
||||
string body;
|
||||
using (var reader = new StreamReader(
|
||||
request.Body,
|
||||
encoding: Encoding.UTF8,
|
||||
detectEncodingFromByteOrderMarks: false,
|
||||
leaveOpen: true))
|
||||
{
|
||||
body = await reader.ReadToEndAsync();
|
||||
request.Body.Position = 0; // 重置流位置,以便后续中间件可以读取
|
||||
}
|
||||
|
||||
if (context.Exception is BusinessException)
|
||||
{
|
||||
LogHelper.Warn($@"
|
||||
|-请求路径:{method} {fullPath}
|
||||
|-报错时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss ffff}
|
||||
|-请求用户:{headers.Authorization}
|
||||
|-请求参数:{body}
|
||||
|-业务异常:{context.Exception}", true);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.Error($@"
|
||||
|-请求路径:{method} {fullPath}
|
||||
|-报错时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss ffff}
|
||||
|-请求用户:{headers.Authorization}
|
||||
|-请求参数:{body}
|
||||
|-异常堆栈:{context.Exception} ");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -17,9 +17,9 @@ public class ForwardExceptionFilter(
|
||||
{
|
||||
private readonly InterfaceRelayOptions _options = options.Value;
|
||||
|
||||
public Task OnExceptionAsync(ExceptionContext context)
|
||||
public async Task OnExceptionAsync(ExceptionContext context)
|
||||
{
|
||||
if (context.ExceptionHandled) return Task.CompletedTask;
|
||||
if (context.ExceptionHandled) return;
|
||||
|
||||
/*
|
||||
* ServiceProviderRequestLog设计的意义是:
|
||||
@ -50,7 +50,7 @@ public class ForwardExceptionFilter(
|
||||
}
|
||||
else //未处理异常
|
||||
{
|
||||
logRepository.SendFeiShu(context.Exception);
|
||||
await logRepository.SendFeiShuAsync(context.Exception);
|
||||
res.Code = StatusCodes.Status500InternalServerError.ToString();
|
||||
res.Msg = "服务器发生未处理异常";
|
||||
context.Result = new JsonResult(res);
|
||||
@ -65,7 +65,6 @@ public class ForwardExceptionFilter(
|
||||
logRepository.AppRequestLog.Response = JsonConvert.SerializeObject(res);
|
||||
_ = logRepository.WriteAppLog(context.Exception);
|
||||
context.ExceptionHandled = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private bool NeedPush(ExceptionContext context, out string subscribeName)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using System.Net;
|
||||
using Fake.DependencyInjection;
|
||||
using Fake.FeiShu;
|
||||
using InterfaceForward.Application.Rabbit;
|
||||
using InterfaceForward.Domain.Shared;
|
||||
using InterfaceForward.Domain.Shared.Enum;
|
||||
@ -7,7 +8,7 @@ using RabbitMQ.Client;
|
||||
|
||||
namespace InterfaceForward.Application.HostServices;
|
||||
|
||||
public class AppSubscribeEventHandler(RabbitClient rabbitClient) : ITransientDependency
|
||||
public class AppSubscribeEventHandler(RabbitClient rabbitClient, IFeiShuNotificationService feiShuNotification) : ITransientDependency
|
||||
{
|
||||
public const string AppSubscribeEventSubscribe = "interface.relay.app.subscribe";
|
||||
|
||||
@ -19,15 +20,15 @@ public class AppSubscribeEventHandler(RabbitClient rabbitClient) : ITransientDep
|
||||
break;
|
||||
case OptionType.新增:
|
||||
var res = DoAdd(@event);
|
||||
LogHelper.Info($"{Dns.GetHostName()}\n{@event.ToJson()}\n{res}");
|
||||
feiShuNotification.Send($"{Dns.GetHostName()}\n{@event.ToJson()}\n{res}");
|
||||
break;
|
||||
case OptionType.修改:
|
||||
var res1 = DoUpdate(@event);
|
||||
LogHelper.Info($"{Dns.GetHostName()}\n{@event.ToJson()}\n{res1}");
|
||||
feiShuNotification.Send($"{Dns.GetHostName()}\n{@event.ToJson()}\n{res1}");
|
||||
break;
|
||||
case OptionType.删除:
|
||||
var res2 = DoDelete(@event);
|
||||
LogHelper.Info($"{Dns.GetHostName()}\n{@event.ToJson()}\n{res2}");
|
||||
feiShuNotification.Send($"{Dns.GetHostName()}\n{@event.ToJson()}\n{res2}");
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using Fake.SyncEx;
|
||||
using Fake.FeiShu;
|
||||
using Fake.SyncEx;
|
||||
using InterfaceForward.Application.Helpers;
|
||||
using InterfaceForward.Application.Rabbit;
|
||||
using InterfaceForward.Domain.Shared;
|
||||
@ -13,15 +14,17 @@ namespace InterfaceForward.Application.HostServices;
|
||||
public class AppSubscribeHostService : IHostedService
|
||||
{
|
||||
private readonly AppSubscribeEventHandler _appSubscribeEventHandler;
|
||||
private readonly IFeiShuNotificationService _feishuNotificationService;
|
||||
private readonly IBasicRepository<AppSubscribeConfigEntity> _appSubscribeConfigRepository;
|
||||
private readonly RabbitClient _rabbitClient;
|
||||
|
||||
public AppSubscribeHostService(IBasicRepository<AppSubscribeConfigEntity> appSubscribeConfigRepository,
|
||||
RabbitClient rabbitClient, AppSubscribeEventHandler appSubscribeEventHandler)
|
||||
RabbitClient rabbitClient, AppSubscribeEventHandler appSubscribeEventHandler, IFeiShuNotificationService feishuNotificationService)
|
||||
{
|
||||
_appSubscribeConfigRepository = appSubscribeConfigRepository;
|
||||
_rabbitClient = rabbitClient;
|
||||
_appSubscribeEventHandler = appSubscribeEventHandler;
|
||||
_feishuNotificationService = feishuNotificationService;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
@ -59,7 +62,7 @@ public class AppSubscribeHostService : IHostedService
|
||||
public void AppSubScribe(string chan, object msg)
|
||||
{
|
||||
#if DEBUG
|
||||
LogHelper.Info($"接受到来自redis的订阅:{chan}\n{msg.ToJson()}", true);
|
||||
_feishuNotificationService.Send($"接受到来自redis的订阅:{chan}\n{msg.ToJson()}");
|
||||
#endif
|
||||
if (chan == AppSubscribeEventHandler.AppSubscribeEventSubscribe && msg is string str)
|
||||
{
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using System.Text;
|
||||
using Fake.DependencyInjection;
|
||||
using Fake.FeiShu;
|
||||
using Fake.UnitOfWork;
|
||||
using InterfaceForward.Application.Contracts.ForwardCore;
|
||||
using InterfaceForward.Application.Services;
|
||||
@ -15,7 +16,8 @@ namespace InterfaceForward.Application.Rabbit;
|
||||
public class BatchForwardEventHandler(
|
||||
InterfaceForwardCommon forwardCommon,
|
||||
RabbitClient rabbitClient,
|
||||
ILogRepository logRepository)
|
||||
ILogRepository logRepository,
|
||||
IFeiShuNotificationService feiShuNotificationService)
|
||||
: ITransientDependency, IRabbitHandler
|
||||
{
|
||||
public const string BatchForwardEventSubscribe = "interface.relay.batch";
|
||||
@ -86,7 +88,7 @@ public class BatchForwardEventHandler(
|
||||
catch (Exception ex)
|
||||
{
|
||||
replayEvent.Message = ex.Message;
|
||||
LogHelper.Error(ex.ToString(), false);
|
||||
feiShuNotificationService.Send(ex.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@ -17,9 +17,9 @@ public class ShopeeForwardExceptionFilter(
|
||||
{
|
||||
private readonly InterfaceRelayOptions _options = options.Value;
|
||||
|
||||
public Task OnExceptionAsync(ExceptionContext context)
|
||||
public async Task OnExceptionAsync(ExceptionContext context)
|
||||
{
|
||||
if (context.ExceptionHandled) return Task.CompletedTask;
|
||||
if (context.ExceptionHandled) return;
|
||||
|
||||
/*
|
||||
* ServiceProviderRequestLog设计的意义是:
|
||||
@ -50,7 +50,7 @@ public class ShopeeForwardExceptionFilter(
|
||||
}
|
||||
else //未处理异常
|
||||
{
|
||||
logRepository.SendFeiShu(context.Exception);
|
||||
await logRepository.SendFeiShuAsync(context.Exception);
|
||||
res.Code = StatusCodes.Status500InternalServerError.ToString();
|
||||
res.Msg = "服务器发生未处理异常";
|
||||
context.Result = new JsonResult(res);
|
||||
@ -65,7 +65,6 @@ public class ShopeeForwardExceptionFilter(
|
||||
logRepository.AppRequestLog.Response = JsonConvert.SerializeObject(res);
|
||||
_ = logRepository.WriteAppLog(context.Exception);
|
||||
context.ExceptionHandled = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private bool NeedPush(ExceptionContext context, out string subscribeName)
|
||||
|
||||
@ -1,98 +0,0 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Fake.SyncEx;
|
||||
using InterfaceForward.Domain.Shared.Options;
|
||||
using Serilog;
|
||||
|
||||
namespace InterfaceForward.Domain.Shared.Helpers;
|
||||
|
||||
public static class LogHelper
|
||||
{
|
||||
private static FeiShuNoticeOptions _options = null!;
|
||||
|
||||
public static void Init(FeiShuNoticeOptions options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
public static void Info(string msg, bool isSend = false)
|
||||
{
|
||||
Log.Information(msg);
|
||||
if (!isSend)
|
||||
return;
|
||||
SyncContext.Run(() => NoticeAsync(msg, isSend, "Info"));
|
||||
}
|
||||
|
||||
public static void Warn(string msg, bool isSend = false)
|
||||
{
|
||||
Log.Warning(msg);
|
||||
if (!isSend)
|
||||
return;
|
||||
SyncContext.Run(() => NoticeAsync(msg, isSend, "Warn"));
|
||||
}
|
||||
|
||||
public static void Error(string msg, bool isSend = true)
|
||||
{
|
||||
Log.Error(msg);
|
||||
SyncContext.Run(() => NoticeAsync(msg, isSend, "Error"));
|
||||
}
|
||||
|
||||
public static async Task NoticeAsync(string content, bool isSend, string subTitle = "")
|
||||
{
|
||||
#if DEBUG
|
||||
// 调试模式下不发送通知
|
||||
return;
|
||||
#endif
|
||||
if (!isSend)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_options.Webhook.IsNullOrWhiteSpace())
|
||||
{
|
||||
Log.Warning("未配置飞书webhook");
|
||||
return;
|
||||
}
|
||||
|
||||
var title = subTitle.IsNullOrWhiteSpace()
|
||||
? _options.TitlePrefix
|
||||
: _options.TitlePrefix + "_" + subTitle;
|
||||
|
||||
await HandleNoticeAsync(content, title);
|
||||
}
|
||||
|
||||
private static async Task HandleNoticeAsync(string content, string title)
|
||||
{
|
||||
var message = new
|
||||
{
|
||||
msg_type = "post",
|
||||
content = new
|
||||
{
|
||||
post = new
|
||||
{
|
||||
zh_cn = new
|
||||
{
|
||||
title,
|
||||
content = new List<object>()
|
||||
{
|
||||
new List<object>()
|
||||
{
|
||||
new
|
||||
{
|
||||
tag = "text",
|
||||
text = content + Environment.NewLine
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
using var httpClient = new HttpClient();
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(_options.Timeout);
|
||||
using var httpContent = new StringContent(JsonSerializer.Serialize(message), Encoding.UTF8);
|
||||
httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
await httpClient.PostAsync(_options.Webhook, httpContent);
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,5 @@
|
||||
using Fake.DomainDrivenDesign;
|
||||
using Fake.Modularity;
|
||||
using InterfaceForward.Domain.Shared.Helpers;
|
||||
using InterfaceForward.Domain.Shared.Options;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace InterfaceForward.Domain.Shared
|
||||
{
|
||||
@ -12,8 +8,6 @@ namespace InterfaceForward.Domain.Shared
|
||||
{
|
||||
public override void ConfigureServices(ServiceConfigurationContext context)
|
||||
{
|
||||
var configuration = context.Services.GetConfiguration();
|
||||
LogHelper.Init(configuration.GetSection("FeiShuNotice").Get<FeiShuNoticeOptions>()!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -40,7 +40,7 @@ namespace InterfaceForward.Repositories.Log.Services
|
||||
/// 发飞书
|
||||
/// </summary>
|
||||
/// <param name="contextException"></param>
|
||||
void SendFeiShu(Exception contextException);
|
||||
Task SendFeiShuAsync(Exception contextException);
|
||||
|
||||
/// <summary>
|
||||
/// 添加日志
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
using InterfaceForward.Domain.Shared.Dtos;
|
||||
using Fake.FeiShu;
|
||||
using InterfaceForward.Domain.Shared.Dtos;
|
||||
using InterfaceForward.Domain.Shared.Enum;
|
||||
using InterfaceForward.Domain.Shared.Helpers;
|
||||
using InterfaceForward.Repositories.Log.Entitys;
|
||||
using InterfaceForward.Repositories.Log.ES;
|
||||
using InterfaceForward.Repositories.Log.ValueObjects;
|
||||
using Nest;
|
||||
using LogHelper = InterfaceForward.Domain.Shared.Helpers.LogHelper;
|
||||
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
|
||||
|
||||
namespace InterfaceForward.Repositories.Log.Services;
|
||||
|
||||
@ -14,12 +15,16 @@ namespace InterfaceForward.Repositories.Log.Services;
|
||||
/// </summary>
|
||||
public class LogRepository : ElasticSearchContextAbstract<LogIndex>, ILogRepository, IScopedDependency
|
||||
{
|
||||
public LogRepository(ConnectionFactory connection) : base(connection)
|
||||
private readonly IFeiShuNotificationService _feiShuNotificationService;
|
||||
|
||||
public LogRepository(ConnectionFactory connection, IFeiShuNotificationService feiShuNotificationService) :
|
||||
base(connection)
|
||||
{
|
||||
var guid = Guid.NewGuid();
|
||||
AppRequestLog = new RequestLogDto(guid);
|
||||
AppRequestLog.IsSuccess = true; // 只有接口通发生未处理异常才算应用失败
|
||||
ServiceProviderRequestLog = new RequestLogDto(guid);
|
||||
_feiShuNotificationService = feiShuNotificationService;
|
||||
}
|
||||
|
||||
public int AppId { get; set; }
|
||||
@ -28,7 +33,6 @@ public class LogRepository : ElasticSearchContextAbstract<LogIndex>, ILogReposit
|
||||
|
||||
public async Task WriteAppLog(Exception? exception = null)
|
||||
{
|
||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
||||
if (exception != null)
|
||||
{
|
||||
AppRequestLog.IsSuccess = false;
|
||||
@ -40,20 +44,20 @@ public class LogRepository : ElasticSearchContextAbstract<LogIndex>, ILogReposit
|
||||
await AddLogAsync(AppRequestLog);
|
||||
}
|
||||
|
||||
public void SendFeiShu(Exception contextException)
|
||||
public async Task SendFeiShuAsync(Exception contextException)
|
||||
{
|
||||
LogHelper.Error($"""
|
||||
日志请求id:{ServiceProviderRequestLog.RequestId}
|
||||
服务商--接口:{ServiceProviderRequestLog.Name}--{ServiceProviderRequestLog.InterfaceName}
|
||||
异常原因:{Truncate(contextException.ToString())}
|
||||
请求地址:{ServiceProviderRequestLog.Address}
|
||||
原始报文:
|
||||
{Truncate(AppRequestLog.Content)}
|
||||
请求报文:
|
||||
{Truncate(ServiceProviderRequestLog.Content)}
|
||||
响应报文:
|
||||
{Truncate(ServiceProviderRequestLog.Response)}
|
||||
""");
|
||||
await _feiShuNotificationService.SendAsync($"""
|
||||
日志请求id:{ServiceProviderRequestLog.RequestId}
|
||||
服务商--接口:{ServiceProviderRequestLog.Name}--{ServiceProviderRequestLog.InterfaceName}
|
||||
异常原因:{Truncate(contextException.ToString())}
|
||||
请求地址:{ServiceProviderRequestLog.Address}
|
||||
原始报文:
|
||||
{Truncate(AppRequestLog.Content)}
|
||||
请求报文:
|
||||
{Truncate(ServiceProviderRequestLog.Content)}
|
||||
响应报文:
|
||||
{Truncate(ServiceProviderRequestLog.Response)}
|
||||
""", LogLevel.Error, CancellationToken.None);
|
||||
}
|
||||
|
||||
|
||||
@ -85,11 +89,12 @@ public class LogRepository : ElasticSearchContextAbstract<LogIndex>, ILogReposit
|
||||
//await Context.IndexDocumentAsync(idx);
|
||||
IndexResponse response = await Context.IndexAsync(idx,
|
||||
request => request.Index($"interface_forward-{DateTime.Now:yyyy.MM.dd}"));
|
||||
if (!response.IsValid) LogHelper.Error("数据添加失败,原因:" + response.DebugInformation);
|
||||
if (!response.IsValid)
|
||||
await _feiShuNotificationService.SendAsync("数据添加失败,原因:" + response.DebugInformation, LogLevel.Error, CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.Error($"日志插入ES失败:{ex.Message}。{idx.ToJson()}");
|
||||
await _feiShuNotificationService.SendAsync($"日志插入ES失败:{ex.Message}。{idx.ToJson()}", LogLevel.Error, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
@ -234,7 +239,7 @@ public class LogRepository : ElasticSearchContextAbstract<LogIndex>, ILogReposit
|
||||
IsApp = x.IsApp
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
|
||||
private static string? Truncate(string? str, int maxLen = 500)
|
||||
{
|
||||
if (str.IsNullOrWhiteSpace()) return str;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user