diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 3729ff0c..00000000 --- a/.dockerignore +++ /dev/null @@ -1,25 +0,0 @@ -**/.classpath -**/.dockerignore -**/.env -**/.git -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/azds.yaml -**/bin -**/charts -**/docker-compose* -**/Dockerfile* -**/node_modules -**/npm-debug.log -**/obj -**/secrets.dev.yaml -**/values.dev.yaml -LICENSE -README.md \ No newline at end of file diff --git a/.env b/.env index e3a59bff..4c96b33e 100644 --- a/.env +++ b/.env @@ -7,19 +7,19 @@ ELASTIC_VERSION=8.7.1 # # Superuser role, full access to cluster management and data indices. # https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-users.html -ELASTIC_PASSWORD='admin123@' +ELASTIC_PASSWORD='changeme' # User 'logstash_internal' (custom) # # The user Logstash uses to connect and send data to Elasticsearch. # https://www.elastic.co/guide/en/logstash/current/ls-security.html -LOGSTASH_INTERNAL_PASSWORD='admin123@' +LOGSTASH_INTERNAL_PASSWORD='changeme' # User 'kibana_system' (built-in) # # The user Kibana uses to connect and communicate with Elasticsearch. # https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-users.html -KIBANA_SYSTEM_PASSWORD='admin123@' +KIBANA_SYSTEM_PASSWORD='changeme' # Users 'metricbeat_internal', 'filebeat_internal' and 'heartbeat_internal' (custom) # diff --git a/docker-compose.dcproj b/docker-compose.dcproj index 9e42c14b..22eaa2fd 100644 --- a/docker-compose.dcproj +++ b/docker-compose.dcproj @@ -14,7 +14,6 @@ docker-compose.yml - diff --git a/docker-compose.yml b/docker-compose.yml index 596a193e..eef025df 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,32 +8,32 @@ services: dockerfile: host/YANLib.HttpApi.Host/Dockerfile setup: + profiles: + - setup build: context: setup/ args: ELASTIC_VERSION: ${ELASTIC_VERSION} init: true - container_name: setup volumes: - ./setup/entrypoint.sh:/entrypoint.sh:ro,Z - ./setup/lib.sh:/lib.sh:ro,Z - ./setup/roles:/roles:ro,Z - - setup:/state:Z environment: ELASTIC_PASSWORD: ${ELASTIC_PASSWORD:-} + KIBANA_SYSTEM_PASSWORD: ${KIBANA_SYSTEM_PASSWORD:-} + BEATS_SYSTEM_PASSWORD: ${BEATS_SYSTEM_PASSWORD:-} LOGSTASH_INTERNAL_PASSWORD: ${LOGSTASH_INTERNAL_PASSWORD:-} METRICBEAT_INTERNAL_PASSWORD: ${METRICBEAT_INTERNAL_PASSWORD:-} FILEBEAT_INTERNAL_PASSWORD: ${FILEBEAT_INTERNAL_PASSWORD:-} HEARTBEAT_INTERNAL_PASSWORD: ${HEARTBEAT_INTERNAL_PASSWORD:-} MONITORING_INTERNAL_PASSWORD: ${MONITORING_INTERNAL_PASSWORD:-} - KIBANA_SYSTEM_PASSWORD: ${KIBANA_SYSTEM_PASSWORD:-} - BEATS_SYSTEM_PASSWORD: ${BEATS_SYSTEM_PASSWORD:-} RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER:-} RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS:-} KAFKA_CLIENT_USERS: ${KAFKA_CLIENT_USERS:-} KAFKA_CLIENT_PASSWORDS: ${KAFKA_CLIENT_PASSWORDS:-} networks: - - demo + - yanlib depends_on: - elasticsearch @@ -42,7 +42,6 @@ services: context: elasticsearch/ args: ELASTIC_VERSION: ${ELASTIC_VERSION} - container_name: elasticsearch volumes: - ./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml:ro,Z - elasticsearch:/usr/share/elasticsearch/data:Z @@ -50,15 +49,17 @@ services: - 9200:9200 - 9300:9300 environment: + node.name: elasticsearch ES_JAVA_OPTS: -Xms512m -Xmx512m # Bootstrap password. # Used to initialize the keystore during the initial startup of # Elasticsearch. Ignored on subsequent runs. ELASTIC_PASSWORD: ${ELASTIC_PASSWORD:-} # Use single node discovery in order to disable production mode and avoid bootstrap checks. - # see: https://www.elastic.co/guide/en/elasticsearch/reference/current/bootstrap-checks.html + # see: https://www.elastic.co/guide/en/elasticsearch/reference/7.17/bootstrap-checks.html + discovery.type: single-node networks: - - demo + - yanlib restart: unless-stopped logstash: @@ -66,7 +67,6 @@ services: context: logstash/ args: ELASTIC_VERSION: ${ELASTIC_VERSION} - container_name: logstash volumes: - ./logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml:ro,Z - ./logstash/pipeline:/usr/share/logstash/pipeline:ro,Z @@ -80,7 +80,7 @@ services: LS_JAVA_OPTS: -Xms256m -Xmx256m LOGSTASH_INTERNAL_PASSWORD: ${LOGSTASH_INTERNAL_PASSWORD:-} networks: - - demo + - yanlib depends_on: - elasticsearch restart: unless-stopped @@ -90,7 +90,6 @@ services: context: kibana/ args: ELASTIC_VERSION: ${ELASTIC_VERSION} - container_name: kibana volumes: - ./kibana/config/kibana.yml:/usr/share/kibana/config/kibana.yml:ro,Z ports: @@ -98,14 +97,13 @@ services: environment: KIBANA_SYSTEM_PASSWORD: ${KIBANA_SYSTEM_PASSWORD:-} networks: - - demo + - yanlib depends_on: - elasticsearch restart: unless-stopped rabbitmq: image: rabbitmq:3-management - container_name: rabbitmq ports: - 5672:5672 - 15672:15672 @@ -113,27 +111,25 @@ services: RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER:-} RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS:-} networks: - - demo + - yanlib depends_on: - logstash restart: unless-stopped zookeeper: image: bitnami/zookeeper:latest - container_name: zookeeper ports: - 2181:2181 environment: - ALLOW_ANONYMOUS_LOGIN=yes networks: - - demo + - yanlib depends_on: - logstash restart: unless-stopped kafka: image: bitnami/kafka:latest - container_name: kafka ports: - 9092:9092 - 9093:9093 @@ -150,14 +146,13 @@ services: KAFKA_CLIENT_USERS: ${KAFKA_CLIENT_USERS:-} KAFKA_CLIENT_PASSWORDS: ${KAFKA_CLIENT_PASSWORDS:-} networks: - - demo + - yanlib depends_on: - zookeeper restart: unless-stopped kafka-ui: image: provectuslabs/kafka-ui:latest - container_name: kafka-ui ports: - 8080:8080 environment: @@ -165,7 +160,7 @@ services: - KAFKA_CLUSTERS_0_ZOOKEEPER=zookeeper:2181 - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=kafka:9093 networks: - - demo + - yanlib depends_on: - zookeeper - kafka @@ -173,18 +168,17 @@ services: redis: image: redis:latest - container_name: redis command: redis-server --requirepass ${REDIS_PASS:-} ports: - 6379:6379 networks: - - demo + - yanlib restart: unless-stopped networks: - demo: + yanlib: driver: bridge volumes: setup: - elasticsearch: + elasticsearch: \ No newline at end of file diff --git a/elasticsearch/config/elasticsearch.yml b/elasticsearch/config/elasticsearch.yml index 427f4b4b..3cc34bbc 100644 --- a/elasticsearch/config/elasticsearch.yml +++ b/elasticsearch/config/elasticsearch.yml @@ -5,15 +5,11 @@ cluster.name: docker-cluster network.host: 0.0.0.0 -node.name: elasticsearch - -discovery.type: single-node - ## X-Pack settings ## see https://www.elastic.co/guide/en/elasticsearch/reference/current/security-settings.html # xpack.license.self_generated.type: trial -xpack.security.enabled: true +xpack.security.enabled: false ## Set the built-in users' passwords. # Run the following command from the Elasticsearch directory: diff --git a/host/YANLib.HttpApi.Host/Program.cs b/host/YANLib.HttpApi.Host/Program.cs index 0cf98504..ffecdb71 100644 --- a/host/YANLib.HttpApi.Host/Program.cs +++ b/host/YANLib.HttpApi.Host/Program.cs @@ -5,8 +5,6 @@ using System; using System.Threading.Tasks; using static Microsoft.AspNetCore.Builder.WebApplication; -using static Serilog.Events.LogEventLevel; -using static System.DateTime; namespace YANLib; @@ -14,20 +12,13 @@ public class Program { public async static Task Main(string[] args) { - Log.Logger = new LoggerConfiguration() -#if DEBUG - .MinimumLevel.Debug() -#else - .MinimumLevel.Information() -#endif - .MinimumLevel.Override("Microsoft", Information).MinimumLevel.Override("Microsoft.EntityFrameworkCore", Warning).Enrich.FromLogContext().WriteTo.Async(c => c.File($"Logs/{Now:yyyy-MM-dd}.log")).WriteTo.Async(c => c.Console()).CreateLogger(); - + Log.Logger = new LoggerConfiguration().MinimumLevel.Information().Enrich.FromLogContext().WriteTo.Async(c => c.Console()).CreateLogger(); try { Log.Information("Starting YANLib.HttpApi.Host."); var builder = CreateBuilder(args); - builder.Host.AddAppSettingsSecretsJson().UseAutofac().UseSerilog(); - await builder.AddApplicationAsync(); + _ = builder.Host.AddAppSettingsSecretsJson().UseAutofac().UseSerilog((t, f) => f.Enrich.FromLogContext().ReadFrom.Configuration(t.Configuration)); + _ = await builder.AddApplicationAsync(); var app = builder.Build(); await app.InitializeApplicationAsync(); await app.RunAsync(); diff --git a/host/YANLib.HttpApi.Host/YANLib.HttpApi.Host.csproj b/host/YANLib.HttpApi.Host/YANLib.HttpApi.Host.csproj index 7f139473..da75e30d 100644 --- a/host/YANLib.HttpApi.Host/YANLib.HttpApi.Host.csproj +++ b/host/YANLib.HttpApi.Host/YANLib.HttpApi.Host.csproj @@ -13,15 +13,24 @@ - + + + + + - - - + + + + + + + + - + diff --git a/host/YANLib.HttpApi.Host/YANLibHttpApiHostModule.cs b/host/YANLib.HttpApi.Host/YANLibHttpApiHostModule.cs index 9281a8dd..d53e399b 100644 --- a/host/YANLib.HttpApi.Host/YANLibHttpApiHostModule.cs +++ b/host/YANLib.HttpApi.Host/YANLibHttpApiHostModule.cs @@ -14,7 +14,10 @@ using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonXLite; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Serilog; +using Volo.Abp.Auditing; using Volo.Abp.Autofac; +using Volo.Abp.Caching.StackExchangeRedis; +using Volo.Abp.Http.Client; using Volo.Abp.Localization; using Volo.Abp.Modularity; using Volo.Abp.Swashbuckle; @@ -25,14 +28,16 @@ namespace YANLib; [DependsOn( -typeof(YANLibHttpApiModule), - typeof(AbpAutofacModule), - typeof(AbpAspNetCoreMultiTenancyModule), + typeof(YANLibHttpApiModule), typeof(YANLibApplicationModule), typeof(YANLibEntityFrameworkCoreModule), + typeof(AbpAutofacModule), + typeof(AbpAspNetCoreMultiTenancyModule), typeof(AbpAspNetCoreMvcUiLeptonXLiteThemeModule), typeof(AbpAspNetCoreSerilogModule), - typeof(AbpSwashbuckleModule) + typeof(AbpSwashbuckleModule), + typeof(AbpCachingStackExchangeRedisModule), + typeof(AbpHttpClientModule) )] public class YANLibHttpApiHostModule : AbpModule { diff --git a/host/YANLib.HttpApi.Host/appsettings.Development.json b/host/YANLib.HttpApi.Host/appsettings.Development.json index 9cd9daa0..dd26cc08 100644 --- a/host/YANLib.HttpApi.Host/appsettings.Development.json +++ b/host/YANLib.HttpApi.Host/appsettings.Development.json @@ -33,5 +33,70 @@ "TestApi": { "BaseUrl": "http://test-api.local/" } + }, + "Serilog": { + // Serilog.Settings.Configuration + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Information" + } + }, + "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ], + "WriteTo": [ + { + // Serilog.Sinks.Async + "Name": "Async", + "Args": { + "configure": [ + { + // Serilog.Sinks.Console + "Name": "Console" + }, + { + // Serilog.Sinks.File + "Name": "File", + "Args": { + "path": "Logs/.log", + "rollingInterval": "Hour", + "encoding": "System.Text.Encoding::UTF8" + } + }, + { + // Serilog.Formatting.Compact + "Name": "File", + "Args": { + "path": "Logs/.json", + "rollingInterval": "Hour", + "formatter": "Serilog.Formatting.Json.JsonFormatter, Serilog" + } + } + ] + } + }, + { + // Serilog.Sinks.Elasticsearch + "Name": "Elasticsearch", + "Args": { + "nodeUris": "http://localhost:9200", + "indexFormat": "log-dev-yanlib-{0:yyyy.MM.dd}", + "autoRegisterTemplate": true, + "autoRegisterTemplateVersion": "ESv8" + } + } + ] + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Information" + }, + "ElasticApm": { + "Enabled": true, + "ServerUrls": "http://localhost:8200", + "ServiceName": "YANLib", + "Environment": "Development", + "SecretToken": "" + } } } \ No newline at end of file diff --git a/setup/.dockerignore b/setup/.dockerignore index 02f22440..c5dd1c85 100644 --- a/setup/.dockerignore +++ b/setup/.dockerignore @@ -7,6 +7,3 @@ Dockerfile # Ignore Git files .gitignore - -# Ignore setup state -state/ diff --git a/src/YANLib.Application.Redis/Services/IRedisService.cs b/src/YANLib.Application.Redis/Services/IRedisService.cs index 6e7eaafb..7a2b09f1 100644 --- a/src/YANLib.Application.Redis/Services/IRedisService.cs +++ b/src/YANLib.Application.Redis/Services/IRedisService.cs @@ -4,12 +4,14 @@ namespace YANLib.Application.Redis.Services; public interface IRedisService : IApplicationService { - public ValueTask?> GetAll(string group); public ValueTask Get(string group, string key); - public ValueTask?> GetBulk(string group, params string[] keys); - public Task Set(string group, string key, T value); - public Task SetBulk(string group, IDictionary fields); - public ValueTask DeleteAll(string group); + public ValueTask?> GetBulk(string group, params string[] keys); + public ValueTask?> GetAll(string group); + public ValueTask Set(string group, string key, T value); + public ValueTask SetBulk(string group, IDictionary fields); public ValueTask Delete(string group, string key); public ValueTask DeleteBulk(string group, params string[] keys); + public ValueTask DeleteAll(string group); + public ValueTask?>?> GetGroup(string groupPreffix); + public ValueTask DeleteGroup(string groupPreffix); } diff --git a/src/YANLib.Application.Redis/Services/Implements/RedisService.cs b/src/YANLib.Application.Redis/Services/Implements/RedisService.cs index 0ae4c6e7..012cf3ce 100644 --- a/src/YANLib.Application.Redis/Services/Implements/RedisService.cs +++ b/src/YANLib.Application.Redis/Services/Implements/RedisService.cs @@ -1,104 +1,295 @@ -using StackExchange.Redis; +using Microsoft.Extensions.Logging; +using StackExchange.Redis; +using Volo.Abp; using YANLib.Application.Redis.ConnectionFactory; using YANLib.Dtos; +using YANLib.Exceptions; using static System.Text.Encoding; +using static System.Threading.Tasks.Task; +using static YANLib.Exceptions.ExceptionMessage; namespace YANLib.Application.Redis.Services.Implements; -public class RedisService : IRedisService +public class RedisService : IRedisService { #region Fields - private readonly IRedisConnectionFactory _redisConnectionFactory; + private readonly ILogger _logger; + private readonly IRedisConnectionFactory _connectionFactory; + private readonly ConnectionMultiplexer _connectionMultiplexer; private readonly IDatabase _database; #endregion #region Constructors - public RedisService(IRedisConnectionFactory redisConnectionFactory) + public RedisService(ILogger logger, IRedisConnectionFactory connectionFactory) { - _redisConnectionFactory = redisConnectionFactory; - _database = _redisConnectionFactory.Connection().GetDatabase(); + _logger = logger; + _connectionFactory = connectionFactory; + _connectionMultiplexer = _connectionFactory.Connection(); + _database = _connectionMultiplexer.GetDatabase(); } #endregion #region Implements - public async ValueTask?> GetAll(string group) + public async ValueTask Get(string group, string key) { - if (group.IsWhiteSpaceOrNull()) + try { - return default; + if (group.IsWhiteSpaceOrNull() || key.IsWhiteSpaceOrNull()) + { + throw new BusinessException(code: ExceptionCode.BAD_REQUEST, message: BAD_REQUEST); + } + var val = await _database.HashGetAsync((RedisKey)group.ToLowerInvariant(), (RedisValue)key.ToLowerInvariant()); + return val.HasValue ? UTF8.GetString(val!).Deserialize() : default; } - var rslts = new Dictionary(); - foreach (var item in (await _database.HashGetAllAsync(group.ToLowerInvariant())).Where(e => e.Name.HasValue && e.Value.HasValue)) + catch (Exception ex) + { + _logger.LogError(ex, "GetRedisService-Exception: {Group} ; {Key}", group, key); + throw; + } + } + + public async ValueTask?> GetBulk(string group, params string[] keys) + { + try { - var val = item.Value; - if (val.HasValue) + if (group.IsWhiteSpaceOrNull() || keys.AllWhiteSpaceOrNull()) { - rslts.Add(item.Name.ToString(), UTF8.GetString(val!).Deserialize()); + throw new BusinessException(code: ExceptionCode.BAD_REQUEST, message: BAD_REQUEST); } + var rslts = new Dictionary(); + var semSlim = new SemaphoreSlim(1); + await WhenAll(keys.Select(async k => + { + var val = await _database.HashGetAsync((RedisKey)group.ToLowerInvariant(), (RedisValue)k.ToLowerInvariant()); + if (val.HasValue) + { + await semSlim.WaitAsync(); + try + { + rslts.Add(k, UTF8.GetString(val!).Deserialize()); + } + finally + { + _ = semSlim.Release(); + } + } + })); + return rslts; + } + catch (Exception ex) + { + _logger.LogError(ex, "GetBulkRedisService-Exception: {Group} ; {Keys}", group, string.Join(", ", keys)); + throw; } - return rslts; } - public async ValueTask Get(string group, string key) + public async ValueTask?> GetAll(string group) { - if (group.IsWhiteSpaceOrNull() || key.IsWhiteSpaceOrNull()) + try { - return default; + if (group.IsWhiteSpaceOrNull()) + { + throw new BusinessException(code: ExceptionCode.BAD_REQUEST, message: BAD_REQUEST); + } + var rslts = new Dictionary(); + var semSlim = new SemaphoreSlim(1); + await WhenAll((await _database.HashGetAllAsync(group.ToLowerInvariant())).Where(e => e.Name.HasValue && e.Value.HasValue).Select(async x => + { + var val = x.Value; + if (val.HasValue) + { + await semSlim.WaitAsync(); + try + { + rslts.Add(x.Name.ToString(), UTF8.GetString(val!).Deserialize()); + } + finally + { + _ = semSlim.Release(); + } + } + })); + return rslts; + } + catch (Exception ex) + { + _logger.LogError(ex, "GetAllRedisService-Exception: {Group}", group); + throw; } - var val = await _database.HashGetAsync((RedisKey)group.ToLowerInvariant(), (RedisValue)key.ToLowerInvariant()); - return val.HasValue ? UTF8.GetString(val!).Deserialize() : default; } - public async ValueTask?> GetBulk(string group, params string[] keys) + public async ValueTask Set(string group, string key, JsonDto value) { - if (group.IsWhiteSpaceOrNull() || keys.AllWhiteSpaceOrNull()) + var jsonVal = value.CamelSerialize(); + try { - return default; + return group.IsWhiteSpaceOrNull() || key.IsWhiteSpaceOrNull() || value is null + ? throw new BusinessException(code: ExceptionCode.BAD_REQUEST, message: BAD_REQUEST) + : await Delete(group, key) && await _database.HashSetAsync((RedisKey)group.ToLowerInvariant(), (RedisValue)key.ToLowerInvariant(), jsonVal); } - var rslts = new Dictionary(); - foreach (var key in keys) + catch (Exception ex) { - var val = await _database.HashGetAsync((RedisKey)group.ToLowerInvariant(), (RedisValue)key.ToLowerInvariant()); - if (val.HasValue) + _logger.LogError(ex, "SetRedisService-Exception: {Group} ; {Key} ; {Value}", group, key, jsonVal); + throw; + } + } + + public async ValueTask SetBulk(string group, IDictionary fields) + { + try + { + if (group.IsWhiteSpaceOrNull() || fields.IsEmptyOrNull()) { - rslts.Add(key, UTF8.GetString(val!).Deserialize()); + throw new BusinessException(code: ExceptionCode.BAD_REQUEST, message: BAD_REQUEST); } + await _database.HashSetAsync(group.ToLowerInvariant(), fields.Select(p => new HashEntry(p.Key.ToLowerInvariant(), p.Value.CamelSerialize())).ToArray()); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "SetBulkRedisService-Exception: {Group} ; {Fields}", group, fields.CamelSerialize()); + throw; } - return rslts; } - public Task Set(string group, string key, RedisDto value) => _database.HashSetAsync((RedisKey)group.ToLowerInvariant(), (RedisValue)key.ToLowerInvariant(), value.CamelSerialize()); + public async ValueTask Delete(string group, string key) + { + try + { + return group.IsWhiteSpaceOrNull() || key.IsWhiteSpaceOrNull() + ? throw new BusinessException(code: ExceptionCode.BAD_REQUEST, message: BAD_REQUEST) + : await _database.HashDeleteAsync((RedisKey)group.ToLowerInvariant(), (RedisValue)key.ToLowerInvariant()); + } + catch (Exception ex) + { + _logger.LogError(ex, "DeleteRedisService-Exception: {Group} ; {Key}", group, key); + throw; + } + } - public async Task SetBulk(string group, IDictionary fields) + public async ValueTask DeleteBulk(string group, params string[] keys) { - if (group.IsNotWhiteSpaceAndNull() && fields.IsNotEmptyAndNull()) + try { - await _database.HashSetAsync(group.ToLowerInvariant(), fields.Select(p => new HashEntry(p.Key.ToLowerInvariant(), p.Value.CamelSerialize())).ToArray()); + if (group.IsWhiteSpaceOrNull() || keys.AllWhiteSpaceOrNull()) + { + throw new BusinessException(code: ExceptionCode.BAD_REQUEST, message: BAD_REQUEST); + } + var rslt = true; + var semSlim = new SemaphoreSlim(1); + await WhenAll(keys.Where(k => k.IsNotWhiteSpaceAndNull()).Select(async k => + { + await semSlim.WaitAsync(); + try + { + rslt = rslt && await _database.HashDeleteAsync((RedisKey)group.ToLowerInvariant(), (RedisValue)k.ToLowerInvariant()); + } + finally + { + _ = semSlim.Release(); + } + })); + return rslt; + } + catch (Exception ex) + { + _logger.LogError(ex, "DeleteBulkRedisService-Exception: {Group} ; {Keys}", group, string.Join(", ", keys)); + throw; } } public async ValueTask DeleteAll(string group) { - var dic = await GetAll(group); - return dic!.IsNotEmptyAndNull() && await DeleteBulk(group, dic!.Select(p => p.Key).ToArray()); + try + { + if (group.IsWhiteSpaceOrNull()) + { + throw new BusinessException(code: ExceptionCode.BAD_REQUEST, message: BAD_REQUEST); + } + var dic = await GetAll(group); + return dic!.IsEmptyOrNull() || await DeleteBulk(group, dic!.Select(p => p.Key).ToArray()); + } + catch (Exception ex) + { + _logger.LogError(ex, "DeleteAllRedisService-Exception: {Group}", group); + throw; + } } - public async ValueTask Delete(string group, string key) => group.IsNotWhiteSpaceAndNull() - && key.IsNotWhiteSpaceAndNull() - && await _database.HashDeleteAsync((RedisKey)group.ToLowerInvariant(), (RedisValue)key.ToLowerInvariant()); + public async ValueTask?>?> GetGroup(string groupPreffix) + { + try + { + var redisRslt = await GetGroupKeys(groupPreffix); + if (redisRslt is not null) + { + var keys = (RedisKey[])redisRslt!; + var rslts = new Dictionary?>(); + if (keys.IsNotEmptyAndNull()) + { + var semSlim = new SemaphoreSlim(1); + await WhenAll(keys.Select(async k => + { + var dic = await GetAll(k!); + if (dic!.IsNotEmptyAndNull()) + { + await semSlim.WaitAsync(); + try + { + rslts.Add(k.ToString().Split(":").Reverse().FirstOrDefault() ?? string.Empty, dic!); + } + finally + { + _ = semSlim.Release(); + } + } + })); + } + return rslts; + } + return default; + } + catch (Exception ex) + { + _logger.LogError(ex, "GetGroupRedisService-Exception: {GroupPreffix}", groupPreffix); + throw; + } + } - public async ValueTask DeleteBulk(string group, params string[] keys) + public async ValueTask DeleteGroup(string groupPreffix) { - if (group.IsWhiteSpaceOrNull() || keys.AllWhiteSpaceOrNull()) + try { + var redisRslt = await GetGroupKeys(groupPreffix); + if (redisRslt is not null) + { + var keys = (RedisKey[])redisRslt!; + return keys.IsEmptyOrNull() || await _database.KeyDeleteAsync(keys) > 0; + } return false; } - var rslt = true; - foreach (var key in keys.Where(k => k.IsNotWhiteSpaceAndNull()).Select(k => (RedisValue)k.ToLowerInvariant())) + catch (Exception ex) + { + _logger.LogError(ex, "DeleteGroupRedisService-Exception: {GroupPreffix}", groupPreffix); + throw; + } + } + #endregion + + #region Methods + private async ValueTask GetGroupKeys(string groupPreffix) + { + try + { + return groupPreffix.IsWhiteSpaceOrNull() + ? throw new BusinessException(code: ExceptionCode.BAD_REQUEST, message: BAD_REQUEST) + : await _database.ExecuteAsync("KEYS", $"{groupPreffix}*"); + } + catch (Exception ex) { - rslt = rslt && await _database.HashDeleteAsync((RedisKey)group.ToLowerInvariant(), key); + _logger.LogError(ex, "GetGroupKeysRedisService-Exception: {GroupPreffix}", groupPreffix); + throw; } - return rslt; } #endregion } diff --git a/src/YANLib.Application.Redis/YANLib.Application.Redis.csproj b/src/YANLib.Application.Redis/YANLib.Application.Redis.csproj index c62ff609..77079306 100644 --- a/src/YANLib.Application.Redis/YANLib.Application.Redis.csproj +++ b/src/YANLib.Application.Redis/YANLib.Application.Redis.csproj @@ -13,6 +13,7 @@ + diff --git a/src/YANLib.Application.Redis/YANLibApplicationRedisModule.cs b/src/YANLib.Application.Redis/YANLibApplicationRedisModule.cs index 137efc48..5ae61328 100644 --- a/src/YANLib.Application.Redis/YANLibApplicationRedisModule.cs +++ b/src/YANLib.Application.Redis/YANLibApplicationRedisModule.cs @@ -3,7 +3,8 @@ namespace YANLib.Application.Redis; [DependsOn( - typeof(YANLibApplicationContractsModule) + typeof(YANLibApplicationContractsModule), + typeof(YANLibDomainModule) )] public class YANLibApplicationRedisModule : AbpModule { diff --git a/src/YANLib.HttpApi/Controllers/YANJsonController.cs b/src/YANLib.HttpApi/Controllers/YANJsonController.cs index eafc21ee..b9c55ca8 100644 --- a/src/YANLib.HttpApi/Controllers/YANJsonController.cs +++ b/src/YANLib.HttpApi/Controllers/YANJsonController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Swashbuckle.AspNetCore.Annotations; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -15,17 +16,26 @@ namespace YANLib.Controllers; public class YANJsonController : YANLibController { #region Fields + private readonly ILogger _logger; private readonly IYANJsonService _service; #endregion #region Constructors - public YANJsonController(IYANJsonService service) => _service = service; + public YANJsonController(ILogger logger, IYANJsonService service) + { + _logger = logger; + _service = service; + } #endregion #region Methods [HttpGet("yan-vs-standards")] [SwaggerOperation(Summary = "Deserialize speed test (YAN vs Standards)")] - public async ValueTask YanVsStandards([Required] uint quantity = 10000, [Required] bool hideSystem = true) => Ok(await _service.YanVsStandards(quantity, hideSystem)); + public async ValueTask YanVsStandards([Required] uint quantity = 10000, [Required] bool hideSystem = true) + { + _logger.LogInformation("YanVsStandardsYANJsonController: {Quantity}, {HideSystem}", quantity, hideSystem); + return Ok(await _service.YanVsStandards(quantity, hideSystem)); + } [HttpPost("serialize")] [SwaggerOperation(Summary = "Serialize n-1 Pascal case")]