diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..76edfa61 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1 @@ +{"image":"mcr.microsoft.com/devcontainers/universal:2"} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index 88723dd9..5039a0f7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,19 @@ [*.cs] -# IDE0008: Use explicit type -csharp_style_var_elsewhere = true +# HAA0101: Array allocation for params parameter +dotnet_diagnostic.HAA0101.severity = none + +# HAA0601: Value type to reference type conversion causing boxing allocation +dotnet_diagnostic.HAA0601.severity = none + +# HAA0603: Delegate allocation from a method group +dotnet_diagnostic.HAA0603.severity = none + +# HAA0301: Closure Allocation Source +dotnet_diagnostic.HAA0301.severity = none + +# HAA0302: Display class allocation to capture closure +dotnet_diagnostic.HAA0302.severity = none + +# HAA0401: Possible allocation of reference type enumerator +dotnet_diagnostic.HAA0401.severity = none diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..69ef038e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: [bnayae] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/build-publish-v2.yml b/.github/workflows/build-publish-v2.yml index 6cd8ee3c..0f0642de 100644 --- a/.github/workflows/build-publish-v2.yml +++ b/.github/workflows/build-publish-v2.yml @@ -26,18 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - - name: Increment Version - run: | - perl -pi -e 's/([0-9]+)\.([0-9]+)\.([0-9]+)/"$1.$2.${\( $3+1 )}"/eg' Directory.Build.props - - name: Commit changes - uses: EndBug/add-and-commit@v7 - with: - author_name: CI/CD - author_email: bnaya@weknow.network.com - message: 'Increment Version' - add: 'Directory.Build.props' + - uses: actions/checkout@v2 - name: Setup .NET uses: actions/setup-dotnet@v1 @@ -50,7 +39,7 @@ jobs: - name: Build run: dotnet build --configuration ${{ env.BUILD_CONFIG }} --no-restore - name: Test - run: dotnet test Tests/Weknow.EventSource.Backbone.UnitTests --configuration ${{ env.BUILD_CONFIG }} --no-restore --no-build --verbosity normal + run: dotnet test Tests/EventSourcing.Backbone.UnitTests --configuration ${{ env.BUILD_CONFIG }} --no-restore --no-build --verbosity normal - name: Push generated package to GitHub registry run: dotnet nuget push ./**/*.nupkg -k ${{ secrets.NUGET_PUBLISH }} -s https://api.nuget.org/v3/index.json --skip-duplicate @@ -68,18 +57,6 @@ jobs: with: dotnet-version: ${{ env.DOTNET_VER }} include-prerelease: ${{ env.INCLUDE_PRERELEASE }} - - #- name: Increment Version - # run: | - # perl -pi -e 's/([0-9]+)\.([0-9]+)\.([0-9]+)/"$1.$2.${\( $3+1 )}"/eg' Directory.Build.props - # shell: bash - #- name: Commit changes - # uses: EndBug/add-and-commit@v7 - # with: - # author_name: CI/CD - # author_email: ${{ inputs.author-email }} - # message: "Increment Version" - # add: "Directory.Build.props" - name: Restore dependencies run: dotnet restore /property:Configuration=Gen diff --git a/.github/workflows/prepare-nuget.yml b/.github/workflows/prepare-nuget.yml index d09e0633..469d0955 100644 --- a/.github/workflows/prepare-nuget.yml +++ b/.github/workflows/prepare-nuget.yml @@ -7,8 +7,8 @@ name: Prepare on: push: branches: [ main ] - pull_request: - branches: [ main ] + # pull_request: + # branches: [ main ] workflow_dispatch: inputs: logLevel: @@ -20,6 +20,6 @@ on: jobs: version_increment: - uses: weknow-network/weknow-workflows/.github/workflows/dotnet-increment-version.yml@main + uses: bnayae/open-workflows/.github/workflows/dotnet-increment-version.yml@main \ No newline at end of file diff --git a/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/ConsumerChannelConstants.cs b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/ConsumerChannelConstants.cs new file mode 100644 index 00000000..3f1b0a7d --- /dev/null +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/ConsumerChannelConstants.cs @@ -0,0 +1,12 @@ +namespace EventSourcing.Backbone.Channels; + +/// +/// Constants +/// +internal static class ConsumerChannelConstants +{ + /// + /// The name of redis consumer channel source + /// + public const string REDIS_CHANNEL_SOURCE = "evt-src-redis-consumer-channel"; +} diff --git a/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/EventSourcing.Backbone.Channels.RedisConsumerProvider.csproj b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/EventSourcing.Backbone.Channels.RedisConsumerProvider.csproj new file mode 100644 index 00000000..7eccb988 --- /dev/null +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/EventSourcing.Backbone.Channels.RedisConsumerProvider.csproj @@ -0,0 +1,29 @@ + + + README.md + + + + + True + \ + + + + + + + + + + + + + + + + + + + + diff --git a/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/RedisConsumerBuilder.cs b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/RedisConsumerBuilder.cs new file mode 100644 index 00000000..20572904 --- /dev/null +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/RedisConsumerBuilder.cs @@ -0,0 +1,234 @@ +using EventSourcing.Backbone.Building; +using EventSourcing.Backbone.Channels.RedisProvider; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using StackExchange.Redis; + +namespace EventSourcing.Backbone; + +public static class RedisConsumerBuilder +{ + /// + /// Create REDIS consumer builder. + /// + /// The raw endpoint (not an environment variable). + /// The password (not an environment variable). + /// The configuration hook. + /// + public static IConsumerStoreStrategyBuilder Create( + string endpoint, + string? password = null, + Action? configurationHook = null) + { + var configuration = RedisClientFactory.CreateConfigurationOptions(endpoint, password, configurationHook); + return configuration.CreateRedisConsumerBuilder(); + } + /// + /// Create REDIS consumer builder. + /// + /// The configuration hook. + /// + public static IConsumerStoreStrategyBuilder Create( + Action? configurationHook = null) + { + var configuration = RedisClientFactory.CreateConfigurationOptions(configurationHook); + return configuration.CreateRedisConsumerBuilder(); + } + + /// + /// Create REDIS consumer builder. + /// + /// The redis configuration. + /// The setting. + /// + public static IConsumerStoreStrategyBuilder CreateRedisConsumerBuilder( + this ConfigurationOptions options, + RedisConsumerChannelSetting? setting = null) + { + var stg = setting ?? RedisConsumerChannelSetting.Default; + var builder = ConsumerBuilder.Empty; + var channelBuilder = builder.UseChannel(LocalCreate); + return channelBuilder; + + IConsumerChannelProvider LocalCreate(ILogger logger) + { + var channel = new RedisConsumerChannel( + logger, + options, + stg); + return channel; + } + } + + /// + /// Create REDIS consumer builder. + /// + /// The setting. + /// The configuration hook. + /// + public static IConsumerStoreStrategyBuilder CreateRedisConsumerBuilder( + this RedisConsumerChannelSetting setting, + Action? configurationHook = null) + { + var configuration = RedisClientFactory.CreateConfigurationOptions(configurationHook); + return configuration.CreateRedisConsumerBuilder(setting); + } + + /// + /// Create REDIS consumer builder. + /// + /// The setting. + /// The raw endpoint (not an environment variable). + /// The password (not an environment variable). + /// The configuration hook. + /// + public static IConsumerStoreStrategyBuilder CreateRedisConsumerBuilder( + this RedisConsumerChannelSetting setting, + string endpoint, + string? password = null, + Action? configurationHook = null) + { + var configuration = RedisClientFactory.CreateConfigurationOptions(endpoint, password, configurationHook); + return configuration.CreateRedisConsumerBuilder(setting); + } + + + /// + /// Create REDIS consumer builder. + /// + /// The credentials keys. + /// The setting. + /// The configuration hook. + /// + public static IConsumerStoreStrategyBuilder CreateRedisConsumerBuilder( + this RedisCredentialsEnvKeys credentialsKeys, + RedisConsumerChannelSetting? setting = null, + Action? configurationHook = null) + { + var configuration = credentialsKeys.CreateConfigurationOptions(configurationHook); + return configuration.CreateRedisConsumerBuilder(setting); + } + + /// + /// Uses REDIS consumer channel. + /// + /// The builder. + /// The setting. + /// The redis configuration. + /// + public static IConsumerStoreStrategyBuilder UseRedisChannel( + this IConsumerBuilder builder, + RedisConsumerChannelSetting? setting = null, + ConfigurationOptions? redisConfiguration = null) + { + var stg = setting ?? RedisConsumerChannelSetting.Default; + var channelBuilder = builder.UseChannel(LocalCreate); + return channelBuilder; + + IConsumerChannelProvider LocalCreate(ILogger logger) + { + var channel = new RedisConsumerChannel( + logger, + redisConfiguration, + stg); + return channel; + } + } + + /// + /// Uses REDIS consumer channel. + /// + /// The builder. + /// Environment keys of the credentials + /// The setting. + /// + public static IConsumerStoreStrategyBuilder UseRedisChannel( + this IConsumerBuilder builder, + RedisCredentialsEnvKeys credentialsKeys, + RedisConsumerChannelSetting? setting = null) + { + var channelBuilder = builder.UseChannel(LocalCreate); + return channelBuilder; + + IConsumerChannelProvider LocalCreate(ILogger logger) + { + var channel = new RedisConsumerChannel( + logger, + credentialsKeys, + setting); + return channel; + } + } + + /// + /// Uses REDIS consumer channel. + /// + /// The builder. + /// The redis client factory. + /// The setting. + /// + internal static IConsumerStoreStrategyBuilder UseRedisChannel( + this IConsumerBuilder builder, + IEventSourceRedisConnectionFactory redisClientFactory, + RedisConsumerChannelSetting? setting = null) + { + var channelBuilder = builder.UseChannel(LocalCreate); + return channelBuilder; + + IConsumerChannelProvider LocalCreate(ILogger logger) + { + var channel = new RedisConsumerChannel( + redisClientFactory, + logger, + setting); + return channel; + } + } + + /// + /// Uses REDIS consumer channel. + /// + /// The builder. + /// The service provider. + /// The setting. + /// + /// redisClient + public static IConsumerIocStoreStrategyBuilder ResolveRedisConsumerChannel( + this IConsumerBuilder builder, + IServiceProvider serviceProvider, + RedisConsumerChannelSetting? setting = null) + { + var channelBuilder = builder.UseChannel(serviceProvider, LocalCreate); + return channelBuilder; + + IConsumerChannelProvider LocalCreate(ILogger logger) + { + var connFactory = serviceProvider.GetService(); + if (connFactory == null) + throw new RedisConnectionException(ConnectionFailureType.None, $"{nameof(IEventSourceRedisConnectionFactory)} is not registered, use services.{nameof(RedisDiExtensions.AddEventSourceRedisConnection)} in order to register it at Setup stage."); + var channel = new RedisConsumerChannel( + connFactory, + logger, + setting); + return channel; + } + } + + /// + /// Uses REDIS consumer channel. + /// + /// The service provider. + /// The setting. + /// + /// redisClient + public static IConsumerIocStoreStrategyBuilder ResolveRedisConsumerChannel( + this IServiceProvider serviceProvider, + RedisConsumerChannelSetting? setting = null) + { + var result = ConsumerBuilder.Empty.ResolveRedisConsumerChannel(serviceProvider, setting); + return result; + } +} diff --git a/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/RedisConsumerChannel.cs b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/RedisConsumerChannel.cs new file mode 100644 index 00000000..4c30881f --- /dev/null +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/RedisConsumerChannel.cs @@ -0,0 +1,1182 @@ +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Net; +using System.Runtime.CompilerServices; +using System.Text.Json; + +using Bnaya.Extensions.Common.Disposables; + +using EventSourcing.Backbone.Building; +using EventSourcing.Backbone.Channels.RedisProvider.Common; +using EventSourcing.Backbone.Consumers; +using EventSourcing.Backbone.Private; + +using Microsoft.Extensions.Logging; + +using StackExchange.Redis; + +using static System.Math; +using static EventSourcing.Backbone.Channels.RedisProvider.Common.RedisChannelConstants; +using static EventSourcing.Backbone.Private.EventSourceTelemetry; + +// TODO: [bnaya 2021-07] MOVE TELEMETRY TO THE BASE CLASSES OF PRODUCER / CONSUME + +namespace EventSourcing.Backbone.Channels.RedisProvider; + +/// +/// The redis consumer channel. +/// +internal class RedisConsumerChannel : IConsumerChannelProvider +{ + private static readonly Counter StealCountCounter = EMeter.CreateCounter("evt-src.sys.consumer.events-stealing", "count", + "Attempt to get stale events (messages) from other consumer (which assumed malfunction)"); + private static readonly Counter StealAmountCounter = EMeter.CreateCounter("evt-src.sys.consumer.events-stealing.messages", "count", + "Attempt to get stale events (messages) from other consumer (which assumed malfunction)"); + private static readonly Counter ConcumeBatchCountCounter = EMeter.CreateCounter("evt-src.sys.consumer.batch", "count", + "count of the number of non empty consuming batches form the stream provider"); + private static readonly Counter ConcumeEventsCounter = EMeter.CreateCounter("evt-src.sys.consumer.events", "count", + "Sum of total consuming events (messages) before process"); + private static readonly Counter ConcumeEventsOperationCounter = EMeter.CreateCounter("evt-src.sys.consumer.events.operation", "count", + "Sum of total consuming events (messages) before process"); + private static readonly Counter ConcumeBatchFailureCounter = EMeter.CreateCounter("evt-src.sys.consumer.batch.failure", "count", + "batch reading failure"); + + private const string BEGIN_OF_STREAM = "0000000000000"; + /// + /// The read by identifier chunk size. + /// REDIS don't have option to read direct position (it read from a position, not includes the position itself), + /// therefore read should start before the actual position. + /// + private const int READ_BY_ID_CHUNK_SIZE = 10; + /// + /// Receiver max iterations + /// + private const int READ_BY_ID_ITERATIONS = 1000 / READ_BY_ID_CHUNK_SIZE; + + private readonly ILogger _logger; + private readonly RedisConsumerChannelSetting _setting; + private readonly IEventSourceRedisConnectionFactory _connFactory; + private readonly IConsumerStorageStrategy _defaultStorageStrategy; + private const string META_SLOT = "____"; + private const int INIT_RELEASE_DELAY = 100; + private const int MAX_RELEASE_DELAY = 1000 * 30; // 30 seconds + + #region Ctor + + /// + /// Initializes a new instance. + /// + /// The redis provider promise. + /// The logger. + /// The setting. + public RedisConsumerChannel( + IEventSourceRedisConnectionFactory redisConnFactory, + ILogger logger, + RedisConsumerChannelSetting? setting = null) + { + _logger = logger; + _connFactory = redisConnFactory; + _defaultStorageStrategy = new RedisHashStorageStrategy(redisConnFactory); + _setting = setting ?? RedisConsumerChannelSetting.Default; + } + + /// + /// Initializes a new instance. + /// + /// The logger. + /// The configuration. + /// The setting. + public RedisConsumerChannel( + ILogger logger, + ConfigurationOptions? configuration = null, + RedisConsumerChannelSetting? setting = null) : this( + EventSourceRedisConnectionFactory.Create( + logger, + configuration), + logger, + setting) + { + } + + /// + /// Initializes a new instance. + /// + /// The logger. + /// Environment keys of the credentials + /// The setting. + /// The configuration hook. + public RedisConsumerChannel( + ILogger logger, + IRedisCredentials credentialsKeys, + RedisConsumerChannelSetting? setting = null, + Action? configurationHook = null) : this( + EventSourceRedisConnectionFactory.Create( + credentialsKeys, + logger, + configurationHook), + logger, + setting) + { + } + + /// + /// Initializes a new instance. + /// + /// The logger. + /// The raw endpoint (not an environment variable). + /// The password (not an environment variable). + /// The setting. + /// The configuration hook. + public RedisConsumerChannel( + ILogger logger, + string endpoint, + string? password = null, + RedisConsumerChannelSetting? setting = null, + Action? configurationHook = null) : this( + EventSourceRedisConnectionFactory.Create( + logger, + endpoint, + password, + configurationHook), + logger, + setting) + { + } + + #endregion // Ctor + + #region SubsribeAsync + + /// + /// Subscribe to the channel for specific metadata. + /// + /// The consumer plan. + /// The function. + /// The cancellation token. + /// + /// When completed + /// + public async ValueTask SubscribeAsync( + IConsumerPlan plan, + Func> func, + CancellationToken cancellationToken) + { + var joinCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(plan.Cancellation, cancellationToken); + var joinCancellation = joinCancellationSource.Token; + ConsumerOptions options = plan.Options; + + ILogger? logger = _logger ?? plan.Logger; + logger.LogInformation("REDIS EVENT-SOURCE | SUBSCRIBE key: [{key}], consumer-group: [{consumer-group}], consumer-name: [{consumer-name}]", plan.FullUri(), plan.ConsumerGroup, plan.ConsumerName); + + while (!joinCancellation.IsCancellationRequested) + { + try + { + await SubsribeToSingleAsync(plan, func, options, joinCancellation); + // TODO: [bnaya 2023-05-22] think of the api for multi stream subscription (by partial uri * pattern) -> var keys = GetKeysUnsafeAsync(pattern: $"{partition}:*").WithCancellation(cancellationToken) + + if (options.FetchUntilUnixDateOrEmpty != null) + break; + } + #region Exception Handling + + catch (OperationCanceledException) + { + if (_logger == null) + Console.WriteLine($"Subscribe cancellation [{plan.FullUri()}] event stream (may have reach the messages limit)"); + else + _logger.LogError("Subscribe cancellation [{uri}] event stream (may have reach the messages limit)", + plan.Uri); + joinCancellationSource.CancelSafe(); + } + catch (Exception ex) + { + if (_logger == null) + Console.WriteLine($"Fail to subscribe into the [{plan.FullUri()}] event stream"); + else + _logger.LogError(ex, "Fail to subscribe into the [{uri}] event stream", + plan.Uri); + throw; + } + + #endregion // Exception Handling + } + } + + #endregion // SubsribeAsync + + #region SubsribeToSingleAsync + + /// + /// Subscribe to specific shard. + /// + /// The consumer plan. + /// The function. + /// The options. + /// The cancellation token. + private async Task SubsribeToSingleAsync( + IConsumerPlan plan, + Func> func, + ConsumerOptions options, + CancellationToken cancellationToken) + { + var claimingTrigger = options.ClaimingTrigger; + var minIdleTime = (int)options.ClaimingTrigger.MinIdleTime.TotalMilliseconds; + + var env = plan.Environment.ToDash(); + string uri = plan.Uri.ToDash(); + string fullUri = plan.FullUri(); + string consumerGroup = plan.ConsumerGroup.ToDash(); + + bool isFirstBatchOrFailure = true; + + CommandFlags flags = CommandFlags.None; + string? fetchUntil = options.FetchUntilUnixDateOrEmpty?.ToString(); + + ILogger logger = plan.Logger ?? _logger; + + #region await CreateConsumerGroupIfNotExistsAsync(...) + + await _connFactory.CreateConsumerGroupIfNotExistsAsync( + plan, + RedisChannelConstants.NONE_CONSUMER, + logger, + cancellationToken); + + await _connFactory.CreateConsumerGroupIfNotExistsAsync( + plan, + plan.ConsumerGroup, + logger, + cancellationToken); + + #endregion // await CreateConsumerGroupIfNotExistsAsync(...) + + int releaseDelay = INIT_RELEASE_DELAY; + int bachSize = options.BatchSize; + + TimeSpan delay = TimeSpan.Zero; + int emptyBatchCount = 0; + //using (ETracer.StartActivity("consumer.loop", ActivityKind.Server)) + //{ + while (!cancellationToken.IsCancellationRequested) + { + var proceed = await HandleBatchAsync(); + if (!proceed) + break; + } + //} + + #region HandleBatchAsync + + // Handle single batch + async ValueTask HandleBatchAsync() + { + var policy = _setting.Policy.Policy; + return await policy.ExecuteAsync(HandleBatchBreakerAsync, cancellationToken); + } + + #endregion // HandleBatchAsync + + #region HandleBatchBreakerAsync + + async Task HandleBatchBreakerAsync(CancellationToken ct) + { + ct.ThrowIfCancellationRequested(); + + StreamEntry[] results = await ReadBatchAsync(); + emptyBatchCount = results.Length == 0 ? emptyBatchCount + 1 : 0; + results = await ClaimStaleMessages(emptyBatchCount, results, ct); + + if (results.Length == 0) + { + if (fetchUntil == null) + delay = await DelayIfEmpty(delay, cancellationToken); + return fetchUntil == null; + } + + ct.ThrowIfCancellationRequested(); + + try + { + var batchCancellation = new CancellationTokenSource(); + int i = 0; + batchCancellation.Token.Register(async () => + { + // TODO: [bnaya 2023-06-19 #RELEASE] committed id should be captured + RedisValue[] freeTargets = results[i..].Select(m => m.Id).ToArray(); + await ReleaseAsync(freeTargets); + }); + // TODO: [bnaya 2023-06-19] enable parallel consuming (when order doesn't matters) See #RELEASE + for (; i < results.Length && !batchCancellation.IsCancellationRequested; i++) + { + StreamEntry result = results[i]; + #region Metadata meta = ... + + Dictionary channelMeta = result.Values.ToDictionary(m => m.Name, m => m.Value); + + Metadata meta; + string? metaJson = channelMeta[META_SLOT]; + string eventKey = ((string?)result.Id) ?? throw new ArgumentException(nameof(MetadataExtensions.Empty.EventKey)); + if (string.IsNullOrEmpty(metaJson)) + { // backward comparability + + string channelType = ((string?)channelMeta[nameof(MetadataExtensions.Empty.ChannelType)]) ?? throw new EventSourcingException(nameof(MetadataExtensions.Empty.ChannelType)); + + if (channelType != CHANNEL_TYPE) + { + // TODO: [bnaya 2021-07] send metrics + logger.LogWarning($"{nameof(RedisConsumerChannel)} [{CHANNEL_TYPE}] omit handling message of type '{channelType}'"); + await AckAsync(result.Id); + continue; + } + + string id = ((string?)channelMeta[nameof(MetadataExtensions.Empty.MessageId)]) ?? throw new EventSourcingException(nameof(MetadataExtensions.Empty.MessageId)); + string operation = ((string?)channelMeta[nameof(MetadataExtensions.Empty.Operation)]) ?? throw new EventSourcingException(nameof(MetadataExtensions.Empty.Operation)); + long producedAtUnix = (long)channelMeta[nameof(MetadataExtensions.Empty.ProducedAt)]; + DateTimeOffset producedAt = DateTimeOffset.FromUnixTimeSeconds(producedAtUnix); + if (fetchUntil != null && string.Compare(fetchUntil, result.Id) < 0) + return false; + meta = new Metadata + { + MessageId = id, + EventKey = eventKey, + Environment = plan.Environment, + Uri = plan.Uri, + Operation = operation, + ProducedAt = producedAt + }; + + } + else + { + meta = JsonSerializer.Deserialize(metaJson, EventSourceOptions.FullSerializerOptions) ?? throw new EventSourcingException(nameof(Metadata)); + meta = meta with { EventKey = eventKey }; + + } + + #endregion // Metadata meta = ... + + ActivityContext parentContext = EventSourceTelemetryExtensions.ExtractSpan(channelMeta, ExtractTraceContext); + using var activity = ETracer.StartConsumerTrace(meta, parentContext); + + #region IEnumerable ExtractTraceContext(Dictionary entries, string key) + + IEnumerable ExtractTraceContext(Dictionary entries, string key) + { + try + { + if (entries.TryGetValue(key, out var value)) + { + if (string.IsNullOrEmpty(value)) + return Array.Empty(); + return new[] { value.ToString() }; + } + } + #region Exception Handling + + catch (Exception ex) + { + Exception err = ex.FormatLazy(); + _logger.LogError(err, "Failed to extract trace context: {error}", err); + } + + #endregion // Exception Handling + + return Enumerable.Empty(); + } + + #endregion // IEnumerable ExtractTraceContext(Dictionary entries, string key) + + int local = i; + var cancellableIds = results[local..].Select(m => m.Id); + var ack = new AckOnce( + fullUri, + async (cause) => + { + Activity.Current?.AddEvent("consumer.event.ack", + t => PrepareTrace(t).Add("cause", cause)); + await AckAsync(result.Id); + }, + plan.Options.AckBehavior, logger, + async (cause) => + { + Activity.Current?.AddEvent("consumer.event.cancel", + t => PrepareTrace(t).Add("cause", cause)); + batchCancellation.CancelSafe(); // cancel forward + await CancelAsync(cancellableIds); + }); + + #region OriginFilter + + MessageOrigin originFilter = plan.Options.OriginFilter; + if (originFilter != MessageOrigin.None && (originFilter & meta.Origin) == MessageOrigin.None) + { + Ack.Set(ack); + #region Log + + _logger.LogInformation("Event Source skip consuming of event [{event-key}] because if origin is [{origin}] while the origin filter is sets to [{origin-filter}], Operation:[{operation}], Stream:[{stream}]", meta.EventKey, meta.Origin, originFilter, meta.Operation, meta.FullUri()); + + #endregion // Log + continue; + } + + #endregion // OriginFilter + + #region var announcement = new Announcement(...) + + Bucket segmets = await GetBucketAsync(plan, channelMeta, meta, EventBucketCategories.Segments); + Bucket interceptions = await GetBucketAsync(plan, channelMeta, meta, EventBucketCategories.Interceptions); + + var announcement = new Announcement + { + Metadata = meta, + Segments = segmets, + InterceptorsData = interceptions + }; + + #endregion // var announcement = new Announcement(...) + + bool succeed; + ConcumeEventsOperationCounter.WithEnvUriOperation(meta).Add(1); + Activity? execActivity = null; + if (ETracer.HasListeners()) + execActivity = ETracer.StartInternalTrace($"{env}.{uri}.{meta.Operation.ToDash()}.invoke"); + using (execActivity) + { + succeed = await func(announcement, ack); + execActivity?.SetTag("succeed", succeed); + } + if (succeed) + { + releaseDelay = INIT_RELEASE_DELAY; + bachSize = options.BatchSize; + } + else + { + // TODO: [bnaya 2023-06-19 #RELEASE] committed id should be captured + if (options.PartialBehavior == Enums.PartialConsumerBehavior.Sequential) + { + using (ETracer.StartInternalTrace("consumer.release-events-on-failure")) + { + RedisValue[] freeTargets = results[i..].Select(m => m.Id).ToArray(); + await ReleaseAsync(freeTargets); // release the rest of the batch which doesn't processed yet + } + } + } + } + } + catch + { + isFirstBatchOrFailure = true; + } + return true; + } + + #endregion // HandleBatchBreakerAsync + + #region ReadBatchAsync + + // read batch entities from REDIS + async Task ReadBatchAsync() + { + // TBD: circuit-breaker + try + { + var r = await _setting.Policy.Policy.ExecuteAsync(async (ct) => + { + ct.ThrowIfCancellationRequested(); + StreamEntry[] values = Array.Empty(); + values = await ReadSelfPending(); + + if (values.Length == 0) + { + isFirstBatchOrFailure = false; + string group = plan.ConsumerGroup; + using var activity = ETracer.StartInternalTrace("consumer.read-batch", + t => PrepareTrace(t) + .Add("consumer-group", group)); + + IConnectionMultiplexer conn = await _connFactory.GetAsync(cancellationToken); + IDatabaseAsync db = conn.GetDatabase(); + try + { + values = await db.StreamReadGroupAsync( + fullUri, + group, + plan.ConsumerName, + position: StreamPosition.NewMessages, + count: bachSize, + flags: flags) + .WithCancellation(ct, () => Array.Empty()) + .WithCancellation(cancellationToken, () => Array.Empty()); + PrepareMeter(ConcumeBatchCountCounter).Add(1); + activity?.SetTag("count", values.Length); + PrepareMeter(ConcumeEventsCounter).Add(values.Length); + } + #region Exception Handling + + catch (RedisServerException ex) when (ex.Message.StartsWith("NOGROUP")) + { + PrepareMeter(ConcumeBatchFailureCounter).Add(1); + logger.LogWarning(ex, ex.Message); + await _connFactory.CreateConsumerGroupIfNotExistsAsync( + plan, + plan.ConsumerGroup, + logger, cancellationToken); + } + catch (RedisServerException ex) + { + PrepareMeter(ConcumeBatchFailureCounter).Add(1); + logger.LogWarning(ex, ex.Message); + await _connFactory.CreateConsumerGroupIfNotExistsAsync( + plan, + plan.ConsumerGroup, + logger, cancellationToken); + } + + #endregion // Exception Handling + } + StreamEntry[] results = values ?? Array.Empty(); + return results; + }, cancellationToken); + return r; + } + #region Exception Handling + + catch (RedisTimeoutException ex) + { + logger.LogWarning(ex, "Event source [{source}] by [{consumer}]: Timeout", fullUri, plan.ConsumerName); + return Array.Empty(); + } + catch (Exception ex) + { + logger.LogError(ex, "Fail to read from event source [{source}] by [{consumer}]", fullUri, plan.ConsumerName); + return Array.Empty(); + } + + #endregion // Exception Handling + } + + #endregion // ReadBatchAsync + + #region ReadSelfPending + + // Check for pending messages of the current consumer (crash scenario) + async Task ReadSelfPending() + { + + StreamEntry[] values = Array.Empty(); + if (!isFirstBatchOrFailure) + return values; + + using var _ = ETracer.StartInternalTrace("consumer.self-pending"); + + IConnectionMultiplexer conn = await _connFactory.GetAsync(cancellationToken); + IDatabaseAsync db = conn.GetDatabase(); + try + { + StreamPendingMessageInfo[] pendMsgInfo = await db.StreamPendingMessagesAsync( + fullUri, + plan.ConsumerGroup, + options.BatchSize, + plan.ConsumerName, + flags: CommandFlags.DemandMaster); + if (pendMsgInfo != null && pendMsgInfo.Length != 0) + { + var ids = pendMsgInfo + .Select(m => m.MessageId).ToArray(); + if (ids.Length != 0) + { + values = await db.StreamClaimAsync(fullUri, + plan.ConsumerGroup, + plan.ConsumerName, + 0, + ids, + flags: CommandFlags.DemandMaster); + values = values ?? Array.Empty(); + _logger.LogInformation("Claimed messages: {ids}", ids); + } + } + + return values; + } + #region Exception Handling + + catch (RedisServerException ex) when (ex.Message.StartsWith("NOGROUP")) + { + await _connFactory.CreateConsumerGroupIfNotExistsAsync( + plan, + plan.ConsumerGroup, + logger, cancellationToken); + return Array.Empty(); + } + + #endregion // Exception Handling + } + + #endregion // ReadSelfPending + + #region ClaimStaleMessages + + // Taking work from other consumers which have log-time's pending messages + async Task ClaimStaleMessages( + int emptyBatchCount, + StreamEntry[] values, + CancellationToken ct) + { + var logger = plan.Logger ?? _logger; + ct.ThrowIfCancellationRequested(); + if (values.Length != 0) return values; + if (emptyBatchCount < claimingTrigger.EmptyBatchCount) + return values; + using var _ = ETracer.StartInternalTrace("consumer.stale-events"); + try + { + IDatabaseAsync db = await _connFactory.GetDatabaseAsync(ct); + StreamPendingInfo pendingInfo; + using (ETracer.StartInternalTrace("consumer.events-stealing.pending")) + { + pendingInfo = await db.StreamPendingAsync(fullUri, plan.ConsumerGroup, flags: CommandFlags.DemandMaster); + } + PrepareMeter(StealCountCounter).Add(1); + foreach (var c in pendingInfo.Consumers) + { + var self = c.Name == plan.ConsumerName; + if (self) continue; + try + { + StreamPendingMessageInfo[] pendMsgInfo; + using (ETracer.StartInternalTrace("consumer.events-stealing.pending-events", + t => PrepareTrace(t).Add("from-consumer", c.Name))) + { + pendMsgInfo = await db.StreamPendingMessagesAsync( + fullUri, + plan.ConsumerGroup, + 10, + c.Name, + pendingInfo.LowestPendingMessageId, + pendingInfo.HighestPendingMessageId, + flags: CommandFlags.DemandMaster); + } + + RedisValue[] ids = pendMsgInfo + .Where(x => x.IdleTimeInMilliseconds > minIdleTime) + .Select(m => m.MessageId).ToArray(); + if (ids.Length == 0) + continue; + + #region Log + logger.LogInformation("Event Source Consumer [{name}]: Claimed {count} events, from Consumer [{name}]", plan.ConsumerName, c.PendingMessageCount, c.Name); + + #endregion // Log + + int count = ids.Length; + PrepareMeter(StealAmountCounter).WithTag("from-consumer", c.Name) + .Add(count); + // will claim events only if older than _setting.ClaimingTrigger.MinIdleTime + using (ETracer.StartInternalTrace("consumer.events-stealing.claim", + t => PrepareTrace(t) + .Add("from-consumer", c.Name) + .Add("message-count", count))) + { + values = await db.StreamClaimAsync(fullUri, + plan.ConsumerGroup, + c.Name, + minIdleTime, + ids, + flags: CommandFlags.DemandMaster); + } + if (values.Length != 0) + logger.LogInformation("Event Source Consumer [{name}]: Claimed {count} messages, from Consumer [{name}]", plan.ConsumerName, c.PendingMessageCount, c.Name); + } + #region Exception Handling + + catch (RedisTimeoutException ex) + { + logger.LogWarning(ex, "Timeout (handle pending): {name}{self}", c.Name, self); + continue; + } + + catch (Exception ex) + { + logger.LogError(ex, "Fail to claim pending: {name}{self}", c.Name, self); + } + + #endregion // Exception Handling + + if (values != null && values.Length != 0) + return values; + } + } + #region Exception Handling + + catch (RedisConnectionException ex) + { + _logger.LogWarning(ex, "Fail to claim REDIS's pending"); + } + + catch (Exception ex) + { + _logger.LogError(ex, "Fail to claim pending"); + } + + #endregion // Exception Handling + + return Array.Empty(); + } + + #endregion // ClaimStaleMessages + + #region AckAsync + + // Acknowledge event handling (prevent re-consuming of the message). + async ValueTask AckAsync(RedisValue messageId) + { + try + { + IConnectionMultiplexer conn = await _connFactory.GetAsync(cancellationToken); + IDatabaseAsync db = conn.GetDatabase(); + // release the event (won't handle again in the future) + await db.StreamAcknowledgeAsync(fullUri, + plan.ConsumerGroup, + messageId, + flags: CommandFlags.DemandMaster); + } + catch (Exception ex) + { + // TODO: [bnaya 2020-10] do better handling (re-throw / swallow + reason) currently logged at the wrapping class + logger.LogWarning(ex.FormatLazy(), $"Fail to acknowledge message [{messageId}]"); + throw; + } + } + + #endregion // AckAsync + + #region CancelAsync + + // Cancels the asynchronous. + ValueTask CancelAsync(IEnumerable messageIds) + { + // no way to release consumed item back to the stream + //try + //{ + // // release the event (won't handle again in the future) + // await db.StreamClaimIdsOnlyAsync(key, + // plan.ConsumerGroup, + // RedisValue.Null, + // 0, + // messageIds.ToArray(), + // flags: CommandFlags.DemandMaster); + //} + //catch (Exception) + //{ // TODO: [bnaya 2020-10] do better handling (re-throw / swallow + reason) currently logged at the wrapping class + // throw; + //} + return ValueTask.CompletedTask; + + } + + #endregion // CancelAsync + + #region ReleaseAsync + + + // Releases the messages (work around). + async Task ReleaseAsync(RedisValue[] freeTargets) + { + IConnectionMultiplexer conn = await _connFactory.GetAsync(cancellationToken); + IDatabaseAsync db = conn.GetDatabase(); + try + { + using (ETracer.StartInternalTrace("consumer.release-ownership", + t => PrepareTrace(t).Add("consumer-group", plan.ConsumerGroup))) + { + await db.StreamClaimAsync(fullUri, + plan.ConsumerGroup, + RedisChannelConstants.NONE_CONSUMER, + 1, + freeTargets, + flags: CommandFlags.DemandMaster); + } + using (ETracer.StartInternalTrace("consumer.release.delay", + t => PrepareTrace(t).Add("delay", releaseDelay))) + { + // let other potential consumer the chance of getting ownership + await Task.Delay(releaseDelay, cancellationToken); + } + if (releaseDelay < MAX_RELEASE_DELAY) + releaseDelay = Math.Min(releaseDelay * 2, MAX_RELEASE_DELAY); + + if (bachSize == options.BatchSize) + bachSize = 1; + else + bachSize = Math.Min(bachSize * 2, options.BatchSize); + } + #region Exception Handling + + catch (RedisServerException ex) when (ex.Message.StartsWith("NOGROUP")) + { + await _connFactory.CreateConsumerGroupIfNotExistsAsync( + plan, + plan.ConsumerGroup, + logger, cancellationToken); + } + + #endregion // Exception Handling + } + + #endregion // ReleaseAsync + + ITagAddition PrepareTrace(ITagAddition t) => t.Add("uri", uri).Add("env", env) + .Add("group-name", consumerGroup); + ICounterBuilder PrepareMeter(Counter t) => t.WithTag("uri", uri) + .WithTag("env", env) + .WithTag("group-name", consumerGroup); + } + + #endregion // SubsribeToSingleAsync + + #region GetByIdAsync + + /// + /// Gets announcement data by id. + /// + /// The entry identifier. + /// The plan. + /// The cancellation token. + /// + async ValueTask IConsumerChannelProvider.GetByIdAsync( + EventKey entryId, + IConsumerPlan plan, + CancellationToken cancellationToken) + { + string mtdName = $"{nameof(IConsumerChannelProvider)}.{nameof(IConsumerChannelProvider.GetByIdAsync)}"; + + try + { + IConnectionMultiplexer conn = await _connFactory.GetAsync(cancellationToken); + IDatabaseAsync db = conn.GetDatabase(); + StreamEntry entry = await FindAsync(entryId); + + #region var announcement = new Announcement(...) + + Dictionary channelMeta = entry.Values.ToDictionary(m => m.Name, m => m.Value); + string channelType = GetMeta(nameof(MetadataExtensions.Empty.ChannelType)); + string id = GetMeta(nameof(MetadataExtensions.Empty.MessageId)); + string operation = GetMeta(nameof(MetadataExtensions.Empty.Operation)); + long producedAtUnix = (long)channelMeta[nameof(MetadataExtensions.Empty.ProducedAt)]; + + #region string GetMeta(string propKey) + + string GetMeta(string propKey) + { + string? result = channelMeta[propKey]; + if (result == null) throw new ArgumentNullException(propKey); + return result; + } + + #endregion // string GetMeta(string propKey) + + DateTimeOffset producedAt = DateTimeOffset.FromUnixTimeSeconds(producedAtUnix); +#pragma warning disable CS8601 // Possible null reference assignment. + var meta = new Metadata + { + MessageId = id, + EventKey = entry.Id, + Environment = plan.Environment, + Uri = plan.Uri, + Operation = operation, + ProducedAt = producedAt, + ChannelType = channelType + }; +#pragma warning restore CS8601 // Possible null reference assignment. + + Bucket segmets = await GetBucketAsync(plan, channelMeta, meta, EventBucketCategories.Segments); + Bucket interceptions = await GetBucketAsync(plan, channelMeta, meta, EventBucketCategories.Interceptions); + + var announcement = new Announcement + { + Metadata = meta, + Segments = segmets, + InterceptorsData = interceptions + }; + + #endregion // var announcement = new Announcement(...) + + return announcement; + + #region FindAsync + + async Task FindAsync(EventKey entryId) + { + string lookForId = (string)entryId; + string fullUri = plan.FullUri(); + + string originId = lookForId; + int len = originId.IndexOf('-'); + string fromPrefix = originId.Substring(0, len); + long start = long.Parse(fromPrefix); + string startPosition = (start - 1).ToString(); + int iteration = 0; + for (int i = 0; i < READ_BY_ID_ITERATIONS; i++) // up to 1000 items + { + iteration++; + StreamEntry[] entries = await db.StreamReadAsync( + fullUri, + startPosition, + READ_BY_ID_CHUNK_SIZE, + CommandFlags.DemandMaster); + if (entries.Length == 0) + throw new KeyNotFoundException($"{mtdName} of [{lookForId}] from [{fullUri}] return nothing, start at ({startPosition}, iteration = {iteration})."); + string k = string.Empty; + foreach (StreamEntry e in entries) + { +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. +#pragma warning disable CS8602 // Dereference of a possibly null reference. + k = e.Id; + string ePrefix = k.Substring(0, len); +#pragma warning restore CS8602 // Dereference of a possibly null reference. +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. + long comp = long.Parse(ePrefix); + if (comp < start) + continue; // not there yet + if (k == lookForId) + { + return e; + } + if (ePrefix != fromPrefix) + throw new KeyNotFoundException($"{mtdName} of [{lookForId}] from [{fullUri}] return not exists."); + } + startPosition = k; // next batch will start from last entry + } + throw new KeyNotFoundException($"{mtdName} of [{lookForId}] from [{fullUri}] return not found."); + } + + #endregion // FindAsync + } + #region Exception Handling + + catch (Exception ex) + { + string key = plan.FullUri(); + _logger.LogError(ex.FormatLazy(), "{method} Failed: Entry [{entryId}] from [{key}] event stream", + mtdName, entryId, key); + throw; + } + + #endregion // Exception Handling + } + + #endregion // GetByIdAsync + + #region GetAsyncEnumerable + + /// + /// Gets asynchronous enumerable of announcements. + /// + /// The plan. + /// The options. + /// The cancellation token. + /// + async IAsyncEnumerable IConsumerChannelProvider.GetAsyncEnumerable( + IConsumerPlan plan, + ConsumerAsyncEnumerableOptions? options, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + IConnectionMultiplexer conn = await _connFactory.GetAsync(cancellationToken); + IDatabaseAsync db = conn.GetDatabase(); + var loop = AsyncLoop().WithCancellation(cancellationToken); + await foreach (StreamEntry entry in loop) + { + if (cancellationToken.IsCancellationRequested) yield break; + + #region var announcement = new Announcement(...) + + Dictionary channelMeta = entry.Values.ToDictionary(m => m.Name, m => m.Value); +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. +#pragma warning disable CS8601 // Possible null reference assignment. + string id = channelMeta[nameof(MetadataExtensions.Empty.MessageId)]; + string operation = channelMeta[nameof(MetadataExtensions.Empty.Operation)]; + long producedAtUnix = (long)channelMeta[nameof(MetadataExtensions.Empty.ProducedAt)]; + DateTimeOffset producedAt = DateTimeOffset.FromUnixTimeSeconds(producedAtUnix); + var meta = new Metadata + { + MessageId = id, + EventKey = entry.Id, + Environment = plan.Environment, + Uri = plan.Uri, + Operation = operation, + ProducedAt = producedAt + }; +#pragma warning restore CS8601 // Possible null reference assignment. +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. + var filter = options?.OperationFilter; + if (filter != null && !filter(meta)) + continue; + + Bucket segmets = await GetBucketAsync(plan, channelMeta, meta, EventBucketCategories.Segments); + Bucket interceptions = await GetBucketAsync(plan, channelMeta, meta, EventBucketCategories.Interceptions); + + var announcement = new Announcement + { + Metadata = meta, + Segments = segmets, + InterceptorsData = interceptions + }; + + #endregion // var announcement = new Announcement(...) + + yield return announcement; + } + + #region AsyncLoop + + async IAsyncEnumerable AsyncLoop() + { + string fullUri = plan.FullUri(); + + int iteration = 0; + RedisValue startPosition = options?.From ?? BEGIN_OF_STREAM; + TimeSpan delay = TimeSpan.Zero; + while (true) + { + if (cancellationToken.IsCancellationRequested) yield break; + + iteration++; + StreamEntry[] entries = await db.StreamReadAsync( + fullUri, + startPosition, + READ_BY_ID_CHUNK_SIZE, + CommandFlags.DemandMaster); + if (entries.Length == 0) + { + if (options?.ExitWhenEmpty ?? true) yield break; + delay = await DelayIfEmpty(delay, cancellationToken); + continue; + } + string k = string.Empty; + foreach (StreamEntry e in entries) + { + if (cancellationToken.IsCancellationRequested) yield break; + +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. + k = e.Id; +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. + if (options?.To != null && string.Compare(options?.To, k) < 0) + yield break; + yield return e; + } + startPosition = k; // next batch will start from last entry + } + } + + #endregion // AsyncLoop + } + + #endregion // GetAsyncEnumerable + + #region ValueTask GetBucketAsync(StorageType storageType) // local function + + /// + /// Gets a data bucket. + /// + /// The plan. + /// The channel meta. + /// The meta. + /// Type of the storage. + /// + private async ValueTask GetBucketAsync( + IConsumerPlan plan, + Dictionary channelMeta, + Metadata meta, + EventBucketCategories storageType) + { + + IEnumerable strategies = await plan.StorageStrategiesAsync; + strategies = strategies.Where(m => m.IsOfTargetType(storageType)); + Bucket bucket = Bucket.Empty; + if (strategies.Any()) + { + foreach (var strategy in strategies) + { + using (ETracer.StartInternalTrace($"consumer.{strategy.Name}-storage.{storageType}.get")) + { + bucket = await strategy.LoadBucketAsync(meta, bucket, storageType, LocalGetProperty); + } + } + } + else + { + using (ETracer.StartInternalTrace($"consumer.{_defaultStorageStrategy.Name}-storage.{storageType}.get")) + { + bucket = await _defaultStorageStrategy.LoadBucketAsync(meta, bucket, storageType, LocalGetProperty); + } + } + + return bucket; + +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. +#pragma warning disable CS8603 // Possible null reference return. + string LocalGetProperty(string k) => (string)channelMeta[k]; +#pragma warning restore CS8603 // Possible null reference return. +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. + } + + #endregion // ValueTask StoreBucketAsync(StorageType storageType) // local function + + #region DelayIfEmpty + + // avoiding system hit when empty (mitigation of self DDoS) + private async Task DelayIfEmpty(TimeSpan previousDelay, CancellationToken cancellationToken) + { + var cfg = _setting.DelayWhenEmptyBehavior; + var newDelay = cfg.CalcNextDelay(previousDelay, cfg); + var limitDelay = Min(cfg.MaxDelay.TotalMilliseconds, newDelay.TotalMilliseconds); + newDelay = TimeSpan.FromMilliseconds(limitDelay); + using (ETracer.StartInternalTrace("consumer.delay.when-empty-queue", + t => t.Add("delay", newDelay))) + { + await Task.Delay(newDelay, cancellationToken); + } + return newDelay; + } + + #endregion // DelayIfEmpty + + #region GetKeysUnsafeAsync + + /// + /// Gets the keys unsafe asynchronous. + /// + /// The pattern. + /// The cancellation token. + /// + public async IAsyncEnumerable GetKeysUnsafeAsync( + string pattern, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + IConnectionMultiplexer multiplexer = await _connFactory.GetAsync(cancellationToken); + var distict = new HashSet(); + while (!cancellationToken.IsCancellationRequested) + { + foreach (EndPoint endpoint in multiplexer.GetEndPoints()) + { + IServer server = multiplexer.GetServer(endpoint); + // TODO: [bnaya 2020_09] check the pagination behavior +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. +#pragma warning disable CS8604 // Possible null reference argument. + await foreach (string key in server.KeysAsync(pattern: pattern)) + { + if (distict.Contains(key)) + continue; + distict.Add(key); + yield return key; + } +#pragma warning restore CS8604 // Possible null reference argument. +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. + } + } + } + + #endregion // GetKeysUnsafeAsync +} diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/Setting/DelayWhenEmptyBehavior.cs b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/Setting/DelayWhenEmptyBehavior.cs similarity index 56% rename from Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/Setting/DelayWhenEmptyBehavior.cs rename to Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/Setting/DelayWhenEmptyBehavior.cs index d27513f4..0b9936eb 100644 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/Setting/DelayWhenEmptyBehavior.cs +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/Setting/DelayWhenEmptyBehavior.cs @@ -1,13 +1,14 @@ using static System.Math; -namespace Weknow.EventSource.Backbone.Channels.RedisProvider +namespace EventSourcing.Backbone.Channels.RedisProvider { /// /// Behavior of delay when empty /// public record DelayWhenEmptyBehavior { + private const int MIN_DELAY_MILLI = 2; public static readonly DelayWhenEmptyBehavior Default = new DelayWhenEmptyBehavior(); /// @@ -15,21 +16,28 @@ public record DelayWhenEmptyBehavior /// public TimeSpan MaxDelay { get; init; } = TimeSpan.FromSeconds(5); + /// + /// The increment factor when increasing the delay (hang on empty). + /// The previous delay will multiply by this factor + Ceiling to endure increment. + /// + public double DelayFactor { get; init; } = 1.2; + /// /// Gets or sets the next delay. /// - public Func CalcNextDelay { get; init; } = DefaultCalcNextDelay; + public Func CalcNextDelay { get; init; } = DefaultCalcNextDelay; /// /// Default calculation of next delay. /// /// The previous delay. + /// The setting. /// - private static TimeSpan DefaultCalcNextDelay(TimeSpan previous) + private static TimeSpan DefaultCalcNextDelay(TimeSpan previous, DelayWhenEmptyBehavior setting) { var prevMilli = previous.TotalMilliseconds; - var milli = Max(prevMilli * 2, 10); + var milli = Max(Math.Ceiling(prevMilli * setting.DelayFactor), MIN_DELAY_MILLI); return TimeSpan.FromMilliseconds(milli); } } diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/Setting/RedisConsumerChannelSetting.cs b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/Setting/RedisConsumerChannelSetting.cs similarity index 95% rename from Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/Setting/RedisConsumerChannelSetting.cs rename to Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/Setting/RedisConsumerChannelSetting.cs index 47f987b5..b2ce638a 100644 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/Setting/RedisConsumerChannelSetting.cs +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/Setting/RedisConsumerChannelSetting.cs @@ -1,7 +1,7 @@ using Polly; -namespace Weknow.EventSource.Backbone.Channels.RedisProvider +namespace EventSourcing.Backbone.Channels.RedisProvider { /// /// Represent specific setting of the consumer channel diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/Setting/ResiliencePolicies.cs b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/Setting/ResiliencePolicies.cs similarity index 98% rename from Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/Setting/ResiliencePolicies.cs rename to Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/Setting/ResiliencePolicies.cs index aab1487f..3daad743 100644 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/Setting/ResiliencePolicies.cs +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/Setting/ResiliencePolicies.cs @@ -1,7 +1,7 @@ using Polly; using Polly.CircuitBreaker; -namespace Weknow.EventSource.Backbone.Channels.RedisProvider +namespace EventSourcing.Backbone.Channels.RedisProvider { /// /// Define when to claim stale (long waiting) messages from other consumers diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/Setting/StaleMessagesClaimingTrigger.cs b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/Setting/StaleMessagesClaimingTrigger.cs similarity index 94% rename from Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/Setting/StaleMessagesClaimingTrigger.cs rename to Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/Setting/StaleMessagesClaimingTrigger.cs index ee660a75..a32babbc 100644 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/Setting/StaleMessagesClaimingTrigger.cs +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/Setting/StaleMessagesClaimingTrigger.cs @@ -1,6 +1,6 @@ // TODO: [bnaya 2021-02] use Record -namespace Weknow.EventSource.Backbone.Channels.RedisProvider +namespace EventSourcing.Backbone.Channels.RedisProvider { /// /// Define when to claim stale (long waiting) messages from other consumers diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/StorageStrategies/RedisHashStorageStrategy.cs b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/StorageStrategies/RedisHashStorageStrategy.cs similarity index 87% rename from Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/StorageStrategies/RedisHashStorageStrategy.cs rename to Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/StorageStrategies/RedisHashStorageStrategy.cs index bc13d8e0..11d441a9 100644 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/StorageStrategies/RedisHashStorageStrategy.cs +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/StorageStrategies/RedisHashStorageStrategy.cs @@ -1,6 +1,6 @@ using StackExchange.Redis; -namespace Weknow.EventSource.Backbone.Channels +namespace EventSourcing.Backbone.Channels { /// /// Responsible to save information to REDIS hash storage. @@ -11,17 +11,22 @@ namespace Weknow.EventSource.Backbone.Channels /// internal class RedisHashStorageStrategy : IConsumerStorageStrategy { - private readonly IEventSourceRedisConnectionFacroty _connFactory; + private readonly IEventSourceRedisConnectionFactory _connFactory; /// /// Initializes a new instance. /// /// The database task. - public RedisHashStorageStrategy(IEventSourceRedisConnectionFacroty connFactory) + public RedisHashStorageStrategy(IEventSourceRedisConnectionFactory connFactory) { _connFactory = connFactory; } + /// + /// Gets the name of the storage provider. + /// + public string Name { get; } = "Redis"; + /// /// Load the bucket information. /// @@ -41,9 +46,9 @@ async ValueTask IConsumerStorageStrategy.LoadBucketAsync( Func getProperty, CancellationToken cancellation) { - string key = $"{meta.Key()}:{type}:{meta.MessageId}"; + string key = $"{meta.FullUri()}:{type}:{meta.MessageId}"; - IConnectionMultiplexer conn = await _connFactory.GetAsync(); + IConnectionMultiplexer conn = await _connFactory.GetAsync(cancellation); IDatabaseAsync db = conn.GetDatabase(); HashEntry[] entities; try diff --git a/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/icon.png b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisConsumerProvider/icon.png differ diff --git a/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProducerProvider/EventSourcing.Backbone.Channels.RedisProducerProvider.csproj b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProducerProvider/EventSourcing.Backbone.Channels.RedisProducerProvider.csproj new file mode 100644 index 00000000..e2689aab --- /dev/null +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProducerProvider/EventSourcing.Backbone.Channels.RedisProducerProvider.csproj @@ -0,0 +1,32 @@ + + + + README.md + + + + + True + \ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProducerProvider/RedisProducerBuilder.cs b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProducerProvider/RedisProducerBuilder.cs new file mode 100644 index 00000000..e9046b8c --- /dev/null +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProducerProvider/RedisProducerBuilder.cs @@ -0,0 +1,190 @@ +using EventSourcing.Backbone.Channels.RedisProvider; +using EventSourcing.Backbone.Private; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using OpenTelemetry.Trace; + +using Polly; + +using StackExchange.Redis; + +namespace EventSourcing.Backbone +{ + public static class RedisProducerBuilder + { + /// + /// Adds the event producer telemetry source (will result in tracing the producer). + /// + /// The builder. + /// + public static TracerProviderBuilder AddEventProducerTelemetry(this TracerProviderBuilder builder) => builder.AddSource(nameof(RedisProducerChannel)); + + /// + /// Uses REDIS producer channel. + /// + /// The resilience policy. + /// The configuration hook. + /// + public static IProducerStoreStrategyBuilder Create( + AsyncPolicy? resiliencePolicy = null, + Action? configurationHook = null) + { + var configuration = RedisClientFactory.CreateConfigurationOptions(configurationHook); + return CreateRedisProducerBuilder(configuration, resiliencePolicy); + } + + /// + /// Uses REDIS producer channel. + /// + /// + /// Environment key of the end-point, if missing it use a default ('REDIS_EVENT_SOURCE_ENDPOINT'). + /// If the environment variable doesn't exists, It assumed that the value represent an actual end-point and use it. + /// + /// + /// Environment key of the password, if missing it use a default ('REDIS_EVENT_SOURCE_PASS'). + /// If the environment variable doesn't exists, It assumed that the value represent an actual password and use it. + /// + /// The resilience policy. + /// The configuration hook. + /// + public static IProducerStoreStrategyBuilder Create( + string endpoint, + string? password = null, + AsyncPolicy? resiliencePolicy = null, + Action? configurationHook = null) + { + var configuration = RedisClientFactory.CreateConfigurationOptions(endpoint, password, configurationHook); + return CreateRedisProducerBuilder(configuration, resiliencePolicy); + } + + /// + /// Uses REDIS producer channel. + /// + /// The credential. + /// The resilience policy. + /// The configuration hook. + /// + public static IProducerStoreStrategyBuilder CreateRedisProducerBuilder( + this IRedisCredentials credential, + AsyncPolicy? resiliencePolicy = null, + Action? configurationHook = null) + { + var configuration = credential.CreateConfigurationOptions(configurationHook); + return CreateRedisProducerBuilder(configuration, resiliencePolicy); + } + + /// + /// Uses REDIS producer channel. + /// + /// The configuration. + /// The resilience policy. + /// + /// + public static IProducerStoreStrategyBuilder CreateRedisProducerBuilder( + this ConfigurationOptions configuration, + AsyncPolicy? resiliencePolicy = null) + { + var builder = ProducerBuilder.Empty; + + return builder.UseRedisChannel(configuration, resiliencePolicy); + } + + /// + /// Uses REDIS producer channel. + /// + /// The builder. + /// The configuration. + /// The resilience policy. + /// + public static IProducerStoreStrategyBuilder UseRedisChannel( + this IProducerBuilder builder, + ConfigurationOptions? configuration = null, + AsyncPolicy? resiliencePolicy = null) + { + var result = builder.UseChannel(LocalCreate); + return result; + + IProducerChannelProvider LocalCreate(ILogger logger) + { + var connFactory = EventSourceRedisConnectionFactory.Create(logger, configuration); + var channel = new RedisProducerChannel( + connFactory, + logger ?? EventSourceFallbakLogger.Default, + resiliencePolicy); + return channel; + } + } + + /// + /// Uses REDIS producer channel. + /// This overload is used by the DI + /// + /// The builder. + /// The redis database. + /// The resilience policy. + /// + /// + internal static IProducerStoreStrategyBuilder UseRedisChannel( + this IProducerBuilder builder, + IEventSourceRedisConnectionFactory redisConnectionFactory, + AsyncPolicy? resiliencePolicy = null) + { + var result = builder.UseChannel(LocalCreate); + return result; + + IProducerChannelProvider LocalCreate(ILogger logger) + { + var channel = new RedisProducerChannel( + redisConnectionFactory, + logger ?? EventSourceFallbakLogger.Default, + resiliencePolicy); + return channel; + } + } + + /// + /// Uses REDIS producer channel by resolving it as a dependency injection from the service-provider. + /// + /// The builder. + /// The service provider. + /// The resilience policy. + /// + public static IProducerIocStoreStrategyBuilder ResolveRedisProducerChannel( + this IProducerBuilder builder, + IServiceProvider serviceProvider, + AsyncPolicy? resiliencePolicy = null) + { + var result = builder.UseChannel(serviceProvider, LocalCreate); + return result; + + IProducerChannelProvider LocalCreate(ILogger logger) + { + var connFactory = serviceProvider.GetService(); + if (connFactory == null) + throw new RedisConnectionException(ConnectionFailureType.None, $"{nameof(IEventSourceRedisConnectionFactory)} is not registered, use services.{nameof(RedisDiExtensions.AddEventSourceRedisConnection)} in order to register it at Setup stage."); + var channel = new RedisProducerChannel( + connFactory, + logger ?? EventSourceFallbakLogger.Default, + resiliencePolicy); + return channel; + } + } + + /// + /// Uses REDIS producer channel by resolving it as a dependency injection from the service-provider. + /// + /// The service provider. + /// The resilience policy. + /// + public static IProducerIocStoreStrategyBuilder ResolveRedisProducerChannel( + this IServiceProvider serviceProvider, + AsyncPolicy? resiliencePolicy = null) + { + var result = ProducerBuilder.Empty.ResolveRedisProducerChannel(serviceProvider, resiliencePolicy); + return result; + } + } +} diff --git a/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProducerProvider/RedisProducerChannel.cs b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProducerProvider/RedisProducerChannel.cs new file mode 100644 index 00000000..2f417c41 --- /dev/null +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProducerProvider/RedisProducerChannel.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Text.Json; + +using EventSourcing.Backbone.Producers; + +using Microsoft.Extensions.Logging; + +using OpenTelemetry; + +using Polly; + +using StackExchange.Redis; + +using static EventSourcing.Backbone.Channels.RedisProvider.Common.RedisChannelConstants; + +using static EventSourcing.Backbone.Private.EventSourceTelemetry; + +namespace EventSourcing.Backbone.Channels.RedisProvider; + +internal class RedisProducerChannel : IProducerChannelProvider +{ + private readonly ILogger _logger; + private readonly AsyncPolicy _resiliencePolicy; + private readonly IEventSourceRedisConnectionFactory _connFactory; + private readonly IProducerStorageStrategy _defaultStorageStrategy; + private const string META_SLOT = "____"; + + private static readonly Counter ProduceEventsCounter = EMeter.CreateCounter("evt-src.sys.produce.events", "count", + "Sum of total produced events (messages)"); + + #region Ctor + + /// + /// Initializes a new instance. + /// + /// The redis database promise. + /// The logger. + /// The resilience policy for retry. + public RedisProducerChannel( + IEventSourceRedisConnectionFactory redisFactory, + ILogger logger, + AsyncPolicy? resiliencePolicy) + { + _connFactory = redisFactory; + _logger = logger; + _resiliencePolicy = resiliencePolicy ?? + Policy.Handle() + .RetryAsync(3); + _defaultStorageStrategy = new RedisHashStorageStrategy(_connFactory, logger); + } + + + #endregion // Ctor + + #region SendAsync + + /// + /// Sends raw announcement. + /// + /// The raw announcement data. + /// The storage strategy. + /// + /// Return the message id + /// + public async ValueTask SendAsync( + Announcement payload, + ImmutableArray storageStrategy) + { + Metadata meta = payload.Metadata; + string id = meta.MessageId; + string env = meta.Environment.ToDash(); + string uri = meta.UriDash; + using var activity = ETracer.StartInternalTrace($"producer.{meta.Operation}.process", + t => t.Add("env", env) + .Add("uri", uri) + .Add("message-id", id)); + + #region var entries = new NameValueEntry[]{...} + + string metaJson = JsonSerializer.Serialize(meta, EventSourceOptions.FullSerializerOptions); + + // local method + NameValueEntry KV(RedisValue key, RedisValue value) => new NameValueEntry(key, value); + ImmutableArray commonEntries = ImmutableArray.Create( + KV(nameof(meta.MessageId), id), + KV(nameof(meta.Operation), meta.Operation), + KV(nameof(meta.ProducedAt), meta.ProducedAt.ToUnixTimeSeconds()), + KV(nameof(meta.ChannelType), CHANNEL_TYPE), + KV(nameof(meta.Origin), meta.Origin.ToString()), + KV(META_SLOT, metaJson) + ); + + #endregion // var entries = new NameValueEntry[]{...} + + RedisValue messageId = await _resiliencePolicy.ExecuteAsync(LocalStreamAddAsync); + + return (string?)messageId ?? "0000000000000-0"; + + #region LocalStreamAddAsync + + async Task LocalStreamAddAsync() + { + await LocalStoreBucketAsync(EventBucketCategories.Segments); + await LocalStoreBucketAsync(EventBucketCategories.Interceptions); + + var telemetryBuilder = commonEntries.ToBuilder(); + using Activity? activity = ETracer.StartProducerTrace(meta); + activity.InjectSpan(telemetryBuilder, LocalInjectTelemetry); + var entries = telemetryBuilder.ToArray(); + + try + { + IConnectionMultiplexer conn = await _connFactory.GetAsync(CancellationToken.None); + IDatabaseAsync db = conn.GetDatabase(); + // using var scope = SuppressInstrumentationScope.Begin(); + var k = meta.FullUri(); + ProduceEventsCounter.WithTag("uri", uri).WithTag("env", env).Add(1); + var result = await db.StreamAddAsync(k, entries, + flags: CommandFlags.DemandMaster); + return result; + } + #region Exception Handling + + catch (RedisConnectionException ex) + { + _logger.LogError(ex, "REDIS Connection Failure: push event [{id}] into the [{env}:{URI}] stream: {operation}", + meta.MessageId, env, uri, meta.Operation); + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Fail to push event [{id}] into the [{env}:{URI}] stream: {operation}", + meta.MessageId, env, uri, meta.Operation); + throw; + } + + #endregion // Exception Handling + + #region ValueTask StoreBucketAsync(StorageType storageType) // local function + + async ValueTask LocalStoreBucketAsync(EventBucketCategories storageType) + { + var strategies = storageStrategy.Where(m => m.IsOfTargetType(storageType)); + Bucket bucket = storageType == EventBucketCategories.Segments ? payload.Segments : payload.InterceptorsData; + if (strategies.Any()) + { + foreach (var strategy in strategies) + { + await SaveBucketAsync(strategy); + } + } + else + { + await SaveBucketAsync(_defaultStorageStrategy); + } + + async ValueTask SaveBucketAsync(IProducerStorageStrategy strategy) + { + using (ETracer.StartInternalTrace($"evt-src.producer.{strategy.Name}-storage.{storageType}.set")) + { + IImmutableDictionary metaItems = + await strategy.SaveBucketAsync(id, bucket, storageType, meta); + foreach (var item in metaItems) + { + commonEntries = commonEntries.Add(KV(item.Key, item.Value)); + } + } + } + } + + #endregion // ValueTask StoreBucketAsync(StorageType storageType) // local function + + #region LocalInjectTelemetry + + void LocalInjectTelemetry( + ImmutableArray.Builder builder, + string key, + string value) + { + builder.Add(KV(key, value)); + } + + #endregion // LocalInjectTelemetry + } + + #endregion // LocalStreamAddAsync + } + + #endregion // SendAsync +} diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProducerProvider/StorageStrategies/RedisHashStorageStrategy.cs b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProducerProvider/StorageStrategies/RedisHashStorageStrategy.cs similarity index 82% rename from Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProducerProvider/StorageStrategies/RedisHashStorageStrategy.cs rename to Channels/REDIS/EventSourcing.Backbone.Channels.RedisProducerProvider/StorageStrategies/RedisHashStorageStrategy.cs index 00235a55..82518f7f 100644 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProducerProvider/StorageStrategies/RedisHashStorageStrategy.cs +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProducerProvider/StorageStrategies/RedisHashStorageStrategy.cs @@ -4,7 +4,7 @@ using StackExchange.Redis; -namespace Weknow.EventSource.Backbone.Channels +namespace EventSourcing.Backbone.Channels { /// /// Responsible to save information to REDIS hash storage. @@ -15,7 +15,7 @@ namespace Weknow.EventSource.Backbone.Channels /// internal class RedisHashStorageStrategy : IProducerStorageStrategy { - private readonly IEventSourceRedisConnectionFacroty _connFactory; + private readonly IEventSourceRedisConnectionFactory _connFactory; private readonly ILogger _logger; #region Ctor @@ -26,7 +26,7 @@ internal class RedisHashStorageStrategy : IProducerStorageStrategy /// The connection factory. /// The logger. public RedisHashStorageStrategy( - IEventSourceRedisConnectionFacroty connFactory, + IEventSourceRedisConnectionFactory connFactory, ILogger logger) { _connFactory = connFactory; @@ -35,6 +35,11 @@ public RedisHashStorageStrategy( #endregion // Ctor + /// + /// Gets the name of the storage provider. + /// + public string Name { get; } = "Redis"; + /// /// Saves the bucket information. /// @@ -53,7 +58,7 @@ async ValueTask> IProducerStorageStrategy.S Metadata meta, CancellationToken cancellation) { - var conn = await _connFactory.GetAsync(); + var conn = await _connFactory.GetAsync(cancellation); try { IDatabaseAsync db = conn.GetDatabase(); @@ -62,15 +67,15 @@ async ValueTask> IProducerStorageStrategy.S .Select(sgm => new HashEntry(sgm.Key, sgm.Value)) .ToArray(); - var key = $"{meta.Key()}:{type}:{id}"; + var key = $"{meta.FullUri()}:{type}:{id}"; await db.HashSetAsync(key, segmentsEntities); - return ImmutableDictionary.Empty; // .Add($"redis:{type}:key", key); + return ImmutableDictionary.Empty; } catch (Exception ex) { - _logger.LogError(ex, "Fail to Save event's [{id}] buckets [{type}], into the [{partition}->{shard}] stream: {operation}, IsConnecting: {connecting}", - id, type, meta.Partition, meta.Shard, meta.Operation, conn.IsConnecting); + _logger.LogError(ex, "Fail to Save event's [{id}] buckets [{type}], into the [{URI}] stream: {operation}, IsConnecting: {connecting}", + id, type, meta.Uri, meta.Operation, conn.IsConnecting); throw; } } diff --git a/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProducerProvider/icon.png b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProducerProvider/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProducerProvider/icon.png differ diff --git a/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/EventSourcing.Backbone.Channels.RedisProvider.Common.csproj b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/EventSourcing.Backbone.Channels.RedisProvider.Common.csproj new file mode 100644 index 00000000..c4880379 --- /dev/null +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/EventSourcing.Backbone.Channels.RedisProvider.Common.csproj @@ -0,0 +1,26 @@ + + + + README.md + + + + + True + \ + + + + + + + + + + + + + + + + diff --git a/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/Factory/EventSourceRedisConnectionFactory.cs b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/Factory/EventSourceRedisConnectionFactory.cs new file mode 100644 index 00000000..c7b2df9b --- /dev/null +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/Factory/EventSourceRedisConnectionFactory.cs @@ -0,0 +1,289 @@ +using System.Diagnostics; +using System.Diagnostics.Metrics; + +using Microsoft.Extensions.Logging; + +using StackExchange.Redis; + +using static EventSourcing.Backbone.Channels.RedisProvider.Common.RedisChannelConstants; +using static EventSourcing.Backbone.Private.EventSourceTelemetry; + +#pragma warning disable S3881 // "IDisposable" should be implemented correctly +#pragma warning disable S2953 // Methods named "Dispose" should implement "IDisposable.Dispose" + + +namespace EventSourcing.Backbone +{ + /// + /// Event Source connection (for IoC) + /// Because IConnectionMultiplexer may be used by other component, + /// It's more clear to wrap the IConnectionMultiplexer for easier resove by IoC. + /// This factory is also responsible of the connection health. + /// It will return same connection as long as it healthy. + /// + public class EventSourceRedisConnectionFactory : IEventSourceRedisConnectionFactory, IDisposable, IAsyncDisposable + { + private const int CLOSE_DELEY_MILLISECONDS = 5000; + private Task _redisTask; + private readonly ILogger _logger; + private readonly ConfigurationOptions _configuration; + private readonly AsyncLock _lock = new AsyncLock(TimeSpan.FromSeconds(CLOSE_DELEY_MILLISECONDS)); + private DateTime _lastResetConnection = DateTime.Now; + private int _reconnectTry = 0; + private const string CHANGE_CONN = "redis-change-connection"; + private static readonly Counter ReConnectCounter = EMeter.CreateCounter(CHANGE_CONN, "count", + "count how many time the connection was re-create"); + + #region Ctor + + /// + /// Constructor + /// + /// The logger. + /// The configuration. + public EventSourceRedisConnectionFactory( + ILogger logger, + ConfigurationOptions? configuration = null) + : this((ILogger)logger, configuration) + { + } + + /// + /// Constructor + /// + /// The logger. + /// The configuration. + private EventSourceRedisConnectionFactory( + ILogger logger, + ConfigurationOptions? configuration = null) + { + _logger = logger; + if (configuration == null) + { + var cred = new RedisCredentialsEnvKeys(); + _configuration = cred.CreateConfigurationOptions(); + } + else + { + _configuration = configuration; + } + _redisTask = RedisClientFactory.CreateProviderAsync(_configuration, logger); + } + + #endregion // Ctor + + #region Create + + /// + /// Create instance + /// + /// The configuration. + /// The logger. + /// + public static IEventSourceRedisConnectionFactory Create( + ILogger logger, + ConfigurationOptions? configuration = null) + { + return new EventSourceRedisConnectionFactory(logger, configuration); + } + + /// + /// Create instance + /// + /// The credential. + /// The logger. + /// The configuration hook. + /// + public static IEventSourceRedisConnectionFactory Create( + IRedisCredentials credential, + ILogger logger, + Action? configurationHook = null) + { + var configuration = credential.CreateConfigurationOptions(); + return new EventSourceRedisConnectionFactory(logger, configuration); + } + + /// + /// Create instance + /// + /// The logger. + /// The raw endpoint (not an environment variable). + /// The password (not an environment variable). + /// The configuration hook. + /// + public static IEventSourceRedisConnectionFactory Create( + ILogger logger, + string endpoint, + string? password = null, + Action? configurationHook = null) + { + var credential = new RedisCredentialsRaw(endpoint, password); + return Create(credential, logger, configurationHook); + } + + /// + /// Create instance from environment variable + /// + /// The logger. + /// The endpoint. + /// The password. + /// The configuration hook. + /// + public static IEventSourceRedisConnectionFactory CreateFromEnv( + ILogger logger, + string endpointEnvKey, + string passwordEnvKey = PASSWORD_KEY, + Action? configurationHook = null) + { + var credential = new RedisCredentialsEnvKeys(endpointEnvKey, passwordEnvKey); + return Create(credential, logger, configurationHook); + } + + #endregion // Create + + #region Kind + + /// + /// Gets the kind. + /// + protected virtual string Kind { get; } = "Event-Sourcing"; + + #endregion // Kind + + #region GetAsync + + /// + /// Get a valid connection + /// + /// The cancellation token. + /// + async Task IEventSourceRedisConnectionFactory.GetAsync(CancellationToken cancellationToken) + { + var conn = await _redisTask; + if (conn.IsConnected) + return conn; + string status = conn.GetStatus(); + _logger.LogWarning("REDIS Connection [{kind}] [{ClientName}]: status = [{status}]", + Kind, + conn.ClientName, status); + var disp = await _lock.AcquireAsync(cancellationToken); + using (disp) + { + conn = await _redisTask; + if (conn.IsConnected) + return conn; + int tryNumber = Interlocked.Increment(ref _reconnectTry); + _logger.LogWarning("[{kind}] Reconnecting to REDIS: try=[{tryNumber}], client name=[{clientName}]", + Kind, tryNumber, conn.ClientName); + var duration = DateTime.Now - _lastResetConnection; + if (duration > TimeSpan.FromSeconds(5)) + { + _lastResetConnection = DateTime.Now; + var cn = conn; + Activity.Current?.AddEvent(CHANGE_CONN, t => t.Add("redis.operation-kind", Kind)); + ReConnectCounter.WithTag("redis.operation-kind", Kind).Add(1); + Task _ = Task.Delay(CLOSE_DELEY_MILLISECONDS).ContinueWith(_ => cn.CloseAsync()); + _redisTask = _configuration.CreateProviderAsync(_logger); + var newConn = await _redisTask; + return newConn; + } + return conn; + } + } + + #endregion // GetAsync + + #region GetDatabaseAsync + + /// + /// Get database + /// + /// The cancellation token. + /// + async Task IEventSourceRedisConnectionFactory.GetDatabaseAsync(CancellationToken cancellationToken) + { + IEventSourceRedisConnectionFactory self = this; + IConnectionMultiplexer conn = await self.GetAsync(cancellationToken); + IDatabaseAsync db = conn.GetDatabase(); + return db; + } + + #endregion // GetDatabaseAsync + + #region Dispose (pattern) + + /// + /// Disposed indication + /// + public bool Disposed { get; private set; } + + /// + /// Dispose + /// + /// + private void Dispose(bool disposing) + { + try + { + _logger.LogWarning("REDIS [{kind}]: Disposing connection", Kind); + } + catch { } + try + { + if (!Disposed) + { + var conn = _redisTask.Result; + conn.Dispose(); + Disposed = true; + OnDispose(disposing); + } + } + catch { } + } + + /// + /// Dispose + /// + void IDisposable.Dispose() + { + GC.SuppressFinalize(this); + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + } + + /// + /// Called when [dispose]. + /// + /// if set to true [disposing]. + /// + protected virtual void OnDispose(bool disposing) { } + + /// + /// Dispose + /// + /// + public async ValueTask DisposeAsync() + { + GC.SuppressFinalize(this); + try + { + _logger.LogWarning("REDIS [{kind}]: Disposing connection (async)", Kind); + } + catch { } + var redis = await _redisTask; + redis.Dispose(); + OnDispose(true); + } + + /// + /// Finalizer + /// + ~EventSourceRedisConnectionFactory() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + #endregion // Dispose (pattern) + } +} diff --git a/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/Factory/IEventSourceRedisConnectionFactory.cs b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/Factory/IEventSourceRedisConnectionFactory.cs new file mode 100644 index 00000000..6d54f837 --- /dev/null +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/Factory/IEventSourceRedisConnectionFactory.cs @@ -0,0 +1,23 @@ +using StackExchange.Redis; + +namespace EventSourcing.Backbone +{ + /// + /// Connection factory + /// + public interface IEventSourceRedisConnectionFactory + { + /// + /// Get a valid connection + /// + /// The cancellation token. + /// + Task GetAsync(CancellationToken cancellationToken); + /// + /// Get database + /// + /// The cancellation token. + /// + Task GetDatabaseAsync(CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/Factory/IRedisCredentials.cs b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/Factory/IRedisCredentials.cs new file mode 100644 index 00000000..87728c96 --- /dev/null +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/Factory/IRedisCredentials.cs @@ -0,0 +1,11 @@ +namespace EventSourcing.Backbone +{ + /// + /// Redis credentials abstraction + /// + public interface IRedisCredentials + { + string? Endpoint { get; } + string? Password { get; } + } +} \ No newline at end of file diff --git a/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/Factory/RedisClientFactory.cs b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/Factory/RedisClientFactory.cs new file mode 100644 index 00000000..3937b0aa --- /dev/null +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/Factory/RedisClientFactory.cs @@ -0,0 +1,263 @@ +using System.Reflection; +using System.Text; + +using Microsoft.Extensions.Logging; + +using StackExchange.Redis; + +using static EventSourcing.Backbone.Channels.RedisProvider.Common.RedisChannelConstants; + +namespace EventSourcing.Backbone +{ + /// + /// REDIS client factory + /// + public static class RedisClientFactory + { + private static int _index = 0; + private const string CONNECTION_NAME_PATTERN = "ev-src:{0}:{1}:{2}"; + private static readonly string? ASSEMBLY_NAME = Assembly.GetEntryAssembly()?.GetName()?.Name?.ToDash(); + private static readonly Version? ASSEMBLY_VERSION = Assembly.GetEntryAssembly()?.GetName()?.Version; + + #region CreateConfigurationOptions + + /// + /// Create REDIS configuration options. + /// + /// A configuration hook. + /// + public static ConfigurationOptions CreateConfigurationOptions( + Action? configurationHook = null) + { + IRedisCredentials credential = new RedisCredentialsEnvKeys(); + var redis = credential.CreateConfigurationOptions(configurationHook); + return redis; + } + + /// + /// Create REDIS configuration options. + /// + /// The raw endpoint (not an environment variable). + /// The password (not an environment variable). + /// A configuration hook. + /// + public static ConfigurationOptions CreateConfigurationOptions( + string endpoint, + string? password = null, + Action? configurationHook = null) + { + IRedisCredentials credential = new RedisCredentialsRaw(endpoint, password); + var redis = credential.CreateConfigurationOptions(configurationHook); + return redis; + } + + /// + /// Create REDIS configuration options. + /// + /// The credential's environment keys. + /// A configuration hook. + /// + public static ConfigurationOptions CreateConfigurationOptions( + this IRedisCredentials credential, + Action? configurationHook = null) + { + var (endpoint, password) = credential switch + { + RedisCredentialsRaw raw => (raw.Endpoint, raw.Password), + RedisCredentialsEnvKeys env => ( + Environment.GetEnvironmentVariable(env.Endpoint ?? END_POINT_KEY), + Environment.GetEnvironmentVariable(env.Password ?? PASSWORD_KEY) + ), + _ => throw new InvalidOperationException(credential?.GetType()?.Name) + }; + + #region Validation + + if (string.IsNullOrEmpty(endpoint)) + throw new InvalidOperationException($"{nameof(endpoint)} is null"); + + #endregion // Validation + + // https://stackexchange.github.io/StackExchange.Redis/Configuration.html + var configuration = ConfigurationOptions.Parse(endpoint); + configuration.Password = password; + if (configurationHook != null) + configuration.Apply(configurationHook); + + return configuration; + } + + #endregion // CreateConfigurationOptions + + #region CreateProviderAsync + + /// + /// Create REDIS client. + /// + /// The logger. + /// A configuration hook. + /// + public static async Task CreateProviderAsync( + ILogger? logger = null, + Action? configurationHook = null) + { + IRedisCredentials credential = new RedisCredentialsEnvKeys(); + var redis = await credential.CreateProviderAsync(logger, configurationHook); + return redis; + } + + /// + /// Create REDIS client. + /// + /// The raw endpoint (not an environment variable). + /// The password (not an environment variable). + /// The logger. + /// A configuration hook. + /// + public static async Task CreateProviderAsync( + string endpoint, + string? password = null, + ILogger? logger = null, + Action? configurationHook = null) + { + IRedisCredentials credential = new RedisCredentialsRaw(endpoint, password); + var redis = await credential.CreateProviderAsync(logger, configurationHook); + return redis; + } + + /// + /// Create REDIS client. + /// + /// The credential's environment keys. + /// The logger. + /// A configuration hook. + /// + public static async Task CreateProviderAsync( + this IRedisCredentials credential, + ILogger? logger = null, + Action? configurationHook = null) + { + try + { + // https://stackexchange.github.io/StackExchange.Redis/Configuration.html + var configuration = credential.CreateConfigurationOptions(configurationHook); + var redis = await configuration.CreateProviderAsync(logger); + return redis; + } + catch (Exception ex) + { + if (logger != null) + logger.LogError(ex.FormatLazy(), "REDIS CONNECTION Setting ERROR"); + else + Console.WriteLine($"REDIS CONNECTION Setting ERROR: {ex.FormatLazy()}"); + throw; + } + } + + /// + /// Create REDIS client. + /// + /// The logger. + /// The configuration. + /// + /// + /// + /// Fail to establish REDIS connection + /// Fail to establish REDIS connection + public static async Task CreateProviderAsync( + this ConfigurationOptions configuration, + ILogger? logger = null) + { + try + { + var sb = new StringBuilder(); + var writer = new StringWriter(sb); + + // https://stackexchange.github.io/StackExchange.Redis/Configuration.html + configuration.ClientName = string.Format( + CONNECTION_NAME_PATTERN, + ASSEMBLY_NAME, + ASSEMBLY_VERSION, + Interlocked.Increment(ref _index)); + + configuration = configuration.Apply(cfg => + { + // keep retry to get connection on failure + cfg.AbortOnConnectFail = false; +#pragma warning disable S125 + /* + cfg.ConnectTimeout = 15; + cfg.SyncTimeout = 10; + cfg.AsyncTimeout = 10; + cfg.DefaultDatabase = Debugger.IsAttached ? 1 : null; + */ +#pragma warning restore S125 + } +); + + + IConnectionMultiplexer redis = await ConnectionMultiplexer.ConnectAsync(configuration, writer); + string endpoints = string.Join(";", configuration.EndPoints); + if (logger != null) + logger.LogInformation("REDIS Connection [{envKey}]: {info} succeed", + endpoints, + sb); + else + Console.WriteLine($"REDIS Connection [{endpoints}] succeed: {sb}"); + redis.ConnectionFailed += OnConnectionFailed; + redis.ErrorMessage += OnConnErrorMessage; + redis.InternalError += OnInternalConnError; + + return redis; + } + catch (Exception ex) + { + if (logger != null) + logger.LogError(ex.FormatLazy(), "REDIS CONNECTION ERROR"); + else + Console.WriteLine($"REDIS CONNECTION ERROR: {ex.FormatLazy()}"); + throw; + } + + #region Event Handlers + + void OnInternalConnError(object? sender, InternalErrorEventArgs e) + { + if (logger != null) + { + logger.LogError(e.Exception, "REDIS Connection internal failure: Failure type = {typeOfConnection}, Origin = {typeOfFailure}", + e.ConnectionType, e.Origin); + } + else + Console.WriteLine($"REDIS Connection internal failure: Failure type = {e.ConnectionType}, Origin = {e.Origin}"); + } + + void OnConnErrorMessage(object? sender, RedisErrorEventArgs e) + { + if (logger != null) + { + logger.LogWarning("REDIS Connection error: {message}", + e.Message); + } + else + Console.WriteLine($"REDIS Connection error: {e.Message}"); + } + + + void OnConnectionFailed(object? sender, ConnectionFailedEventArgs e) + { + if (logger != null) + { + logger.LogError(e.Exception, "REDIS Connection failure: Failure type = {typeOfConnection}, Failure type = {typeOfFailure}", e.ConnectionType, e.FailureType); + } + else + Console.WriteLine($"REDIS Connection failure: Failure type = {e.ConnectionType}, Failure type = {e.FailureType}"); + } + + #endregion // Event Handlers + + } + + #endregion // CreateProviderAsync + } +} diff --git a/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/Factory/RedisCredentialsEnvKeys.cs b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/Factory/RedisCredentialsEnvKeys.cs new file mode 100644 index 00000000..41c898b3 --- /dev/null +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/Factory/RedisCredentialsEnvKeys.cs @@ -0,0 +1,10 @@ +using static EventSourcing.Backbone.Channels.RedisProvider.Common.RedisChannelConstants; + + +namespace EventSourcing.Backbone +{ + /// + /// Environment keys for REDIS's credentials + /// + public record RedisCredentialsEnvKeys(string Endpoint = END_POINT_KEY, string Password = PASSWORD_KEY) : IRedisCredentials; +} diff --git a/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/Factory/RedisCredentialsRaw.cs b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/Factory/RedisCredentialsRaw.cs new file mode 100644 index 00000000..85b1db87 --- /dev/null +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/Factory/RedisCredentialsRaw.cs @@ -0,0 +1,33 @@ +namespace EventSourcing.Backbone +{ + /// + /// Raw keys for REDIS's credentials + /// + public record RedisCredentialsRaw : IRedisCredentials + { + #region Ctor + + /// + /// Initializes a new instance of the class. + /// + /// The raw endpoint (not an environment variable). + /// The password (not an environment variable). + /// endpoint + public RedisCredentialsRaw(string endpoint, string? password = null) + { + Endpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint)); + Password = password; + } + + #endregion // Ctor + + /// + /// The raw endpoint (not an environment variable) + /// + public string? Endpoint { get; } + /// + /// The password (not an environment variable). + /// + public string? Password { get; } + } +} diff --git a/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/RedisChannelConstants.cs b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/RedisChannelConstants.cs new file mode 100644 index 00000000..0ba27877 --- /dev/null +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/RedisChannelConstants.cs @@ -0,0 +1,26 @@ +namespace EventSourcing.Backbone.Channels.RedisProvider.Common; + +public static class RedisChannelConstants +{ + public const string CHANNEL_TYPE = "REDIS Channel V1"; + public const string META_ARRAY_SEPARATOR = "~|~"; + + public const string END_POINT_KEY = "REDIS_EVENT_SOURCE_ENDPOINT"; + public const string PASSWORD_KEY = "REDIS_EVENT_SOURCE_PASS"; + + /// + /// a work around used to release messages back to the stream (consumer) + /// + public const string NONE_CONSUMER = "__NONE_CUNSUMER__"; + + public static class MetaKeys + { + public const string SegmentsKeys = "segments-keys"; + public const string InterceptorsKeys = "interceptors-keys"; + public const string TelemetryBaggage = "telemetry-baggage"; + public const string TelemetrySpanId = "telemetry-span-id"; + public const string TelemetryTraceId = "telemetry-trace-id"; + } + +} + diff --git a/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/RedisCommonProviderExtensions.cs b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/RedisCommonProviderExtensions.cs new file mode 100644 index 00000000..ff54c3e7 --- /dev/null +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/RedisCommonProviderExtensions.cs @@ -0,0 +1,223 @@ +using System.Diagnostics; +using System.Diagnostics.Metrics; + +using Microsoft.Extensions.Logging; + +using StackExchange.Redis; + +using static EventSourcing.Backbone.Private.EventSourceTelemetry; + +namespace EventSourcing.Backbone.Private +{ + /// + /// Redis common provider extensions + /// + public static class RedisCommonProviderExtensions + { + private const int DELAY_ON_MISSING_KEY = 5; + private const int MIN_DELAY = 2; + private const int SPIN_LIMIT = 30; + private const int MAX_DELAY = 3_000; + private static Counter KeyMissingCounter = EMeter.CreateCounter("evt-src.sys.key-missing", "count", "count missing key events"); + private static Counter CreateConsumerGroupCounter = EMeter.CreateCounter("evt-src.sys.create-consumer-group", "count", "creating a consumer group"); + private static Counter CreateConsumerGroupRetryCounter = EMeter.CreateCounter("evt-src.sys.create-consumer-group-retry", "count", "retries of creating a consumer group"); + + private static readonly AsyncLock _lock = new AsyncLock(TimeSpan.FromSeconds(20)); + + #region CreateConsumerGroupIfNotExistsAsync + + /// + /// Creates the consumer group if not exists asynchronous. + /// + /// The connection factory. + /// The plan. + /// The consumer group. + /// The logger. + /// The cancellation token. + /// + public static async Task CreateConsumerGroupIfNotExistsAsync( + this IEventSourceRedisConnectionFactory connFactory, + IConsumerPlan plan, + string consumerGroup, + ILogger logger, + CancellationToken cancellationToken) + { + Env env = plan.Environment; + string uri = plan.UriDash; + string fullUri = plan.FullUri(); + + StreamGroupInfo[] groupsInfo = Array.Empty(); + using var track = ETracer.StartInternalTrace("consumer.create-consumer-group", + t => PrepareTrace(t)); + + PrepareMeter(CreateConsumerGroupCounter).Add(1); + int delay = MIN_DELAY; + bool exists = false; + int tryNumber = 0; + var retryCounter = PrepareMeter(CreateConsumerGroupRetryCounter); + var missingCounter = PrepareMeter(KeyMissingCounter); + while (groupsInfo.Length == 0) + { + if (tryNumber != 0) + retryCounter.Add(1); + tryNumber++; + + IConnectionMultiplexer conn = await connFactory.GetAsync(cancellationToken); + IDatabaseAsync db = conn.GetDatabase(); + try + { + #region delay on retry + + if (tryNumber > SPIN_LIMIT) + { + delay = Math.Min(delay * 2, MAX_DELAY); + using (ETracer.StartInternalTrace("consumer.delay.key-not-exists", + t => PrepareTrace(t) + .Add("delay", delay) + .Add("try-number", tryNumber))) + { + await Task.Delay(delay); + } + if (tryNumber % 10 == 0) + { + logger.LogWarning("Create Consumer Group If Not Exists: still waiting {info}", CurrentInfo()); + } + } + + #endregion // delay on retry + + #region Validation (if key exists) + + if (!await db.KeyExistsAsync(fullUri, + flags: CommandFlags.DemandMaster)) + { + missingCounter.Add(1); + await Task.Delay(DELAY_ON_MISSING_KEY); + if (tryNumber == 0 || tryNumber > SPIN_LIMIT) + logger.LogDebug("Key not exists (yet): {info}", CurrentInfo()); + continue; + } + + #endregion // Validation (if key exists) + + using (ETracer.StartInternalTrace("consumer.get-consumer-group-info", + t => PrepareTrace(t) + .Add("try-number", tryNumber))) + { + using var lk = await _lock.AcquireAsync(cancellationToken); + groupsInfo = await db.StreamGroupInfoAsync( + fullUri, + flags: CommandFlags.DemandMaster); + exists = groupsInfo.Any(m => m.Name == consumerGroup); + } + } + #region Exception Handling + + catch (RedisServerException ex) + { + if (await db.KeyExistsAsync(fullUri, + flags: CommandFlags.DemandMaster)) + { + logger.LogWarning(ex, "Create Consumer Group If Not Exists: failed. {info}", CurrentInfo()); + } + else + { + logger.LogDebug(ex, "Create Consumer Group If Not Exists: failed. {info}", CurrentInfo()); + } + } + catch (RedisConnectionException ex) + { + logger.LogWarning(ex.FormatLazy(), "Create Consumer Group If Not Exists: connection failure. {info}", CurrentInfo()); + } + catch (RedisTimeoutException ex) + { + logger.LogWarning(ex.FormatLazy(), "Create Consumer Group If Not Exists: timeout failure. {info}", CurrentInfo()); + } + catch (Exception ex) + { + logger.LogWarning(ex.FormatLazy(), "Create Consumer Group If Not Exists: unexpected failure. {info}", CurrentInfo()); + } + + #endregion // Exception Handling + if (!exists) + { + try + { + using (ETracer.StartInternalTrace("consumer.create-consumer-group", + t => PrepareTrace(t) + .Add("try-number", tryNumber))) + { + using var lk = await _lock.AcquireAsync(cancellationToken); + if (await db.StreamCreateConsumerGroupAsync(fullUri, + consumerGroup, + StreamPosition.Beginning, + flags: CommandFlags.DemandMaster)) + { + break; + } + } + } + #region Exception Handling + + catch (RedisServerException ex) + { + logger.LogWarning(ex.FormatLazy(), $""" + {nameof(CreateConsumerGroupIfNotExistsAsync)}.StreamCreateConsumerGroupAsync: + failed & still waiting + {CurrentInfo()} + """); + } + catch (RedisConnectionException ex) + { + logger.LogWarning(ex.FormatLazy(), $""" + {nameof(CreateConsumerGroupIfNotExistsAsync)}.StreamCreateConsumerGroupAsync: + Connection failure + {CurrentInfo()} + """); + } + catch (ObjectDisposedException) + { + logger.LogWarning($""" + {nameof(CreateConsumerGroupIfNotExistsAsync)}.StreamCreateConsumerGroupAsync: + Connection might not being available + {CurrentInfo()} + """); + } + + catch (Exception ex) + { + logger.LogWarning(ex, $""" + {nameof(CreateConsumerGroupIfNotExistsAsync)}.StreamCreateConsumerGroupAsync: + unexpected failure + {CurrentInfo()} + """); + } + + #endregion // Exception Handling + } + + #region string CurrentInfo() + + string CurrentInfo() => @$" +Try number: {tryNumber} +Stream key: {uri} +Consumer Group: {consumerGroup} +Is Connected: {db.Multiplexer.IsConnected} +Configuration: {db.Multiplexer.Configuration} +"; + + #endregion // string CurrentInfo() + } + + + ITagAddition PrepareTrace(ITagAddition t) => t.Add("uri", uri) + .Add("env", env) + .Add("group-name", consumerGroup); + ICounterBuilder PrepareMeter(Counter t) => t.WithTag("uri", uri) + .WithTag("env", env) + .WithTag("group-name", consumerGroup); + } + + #endregion // CreateConsumerGroupIfNotExistsAsync + } +} diff --git a/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/RedisDiExtensions.cs b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/RedisDiExtensions.cs new file mode 100644 index 00000000..01cb70b9 --- /dev/null +++ b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/RedisDiExtensions.cs @@ -0,0 +1,84 @@ +using EventSourcing.Backbone; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using static EventSourcing.Backbone.Channels.RedisProvider.Common.RedisChannelConstants; + +namespace Microsoft.Extensions.Configuration +{ + /// + /// The redis DI extensions. + /// + public static class RedisDiExtensions + { + /// + /// Adds the event source redis connection to the DI. + /// + /// The services. + /// An IServiceCollection. + public static IServiceCollection AddEventSourceRedisConnection( + this IServiceCollection services) + { + services.AddSingleton(); + + return services; + } + + /// + /// Adds the event source redis connection to the DI. + /// + /// The services. + /// The raw endpoint (not an environment variable). + /// The password (not an environment variable). + /// + /// An IServiceCollection. + /// + public static IServiceCollection AddEventSourceRedisConnection( + this IServiceCollection services, + string endpoint, + string? password = null) + { + services.AddSingleton( + sp => + { + ILogger logger = sp.GetService>() ?? + throw new EventSourcingException( + $"{nameof(AddEventSourceRedisConnection)}: Cannot resolve a logger"); + + var factory = EventSourceRedisConnectionFactory.Create(logger, endpoint, password); + return factory; + }); + + return services; + } + + /// + /// Adds the event source redis connection to the DI. + /// + /// The services. + /// The environment variable key of the endpoint. + /// The environment variable key of the password. + /// + /// An IServiceCollection. + /// + public static IServiceCollection AddEventSourceRedisConnectionFromEnv( + this IServiceCollection services, + string endpointEnvKey, + string passwordEnvKey = PASSWORD_KEY) + { + services.AddSingleton( + sp => + { + ILogger logger = sp.GetService>() ?? + throw new EventSourcingException( + $"{nameof(AddEventSourceRedisConnectionFromEnv)}: Cannot resolve a logger"); + + var factory = EventSourceRedisConnectionFactory.CreateFromEnv(logger, endpointEnvKey, passwordEnvKey); + return factory; + }); + + return services; + } + } +} diff --git a/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/icon.png b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/Channels/REDIS/EventSourcing.Backbone.Channels.RedisProvider.Common/icon.png differ diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/RedisConsumerChannel.cs b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/RedisConsumerChannel.cs deleted file mode 100644 index 6d6ff9f5..00000000 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/RedisConsumerChannel.cs +++ /dev/null @@ -1,1077 +0,0 @@ -using System.Diagnostics; -using System.Net; -using System.Runtime.CompilerServices; -using System.Text.Json; - -using Microsoft.Extensions.Logging; - -using StackExchange.Redis; - -using Weknow.EventSource.Backbone.Building; -using Weknow.EventSource.Backbone.Channels.RedisProvider.Common; -using Weknow.EventSource.Backbone.Private; - -using static System.Math; -using static Weknow.EventSource.Backbone.Channels.RedisProvider.Common.RedisChannelConstants; - -// TODO: [bnaya 2021-07] MOVE TELEMETRY TO THE BASE CLASSES OF PRODUCER / CONSUME - -namespace Weknow.EventSource.Backbone.Channels.RedisProvider -{ - /// - /// The redis consumer channel. - /// - internal class RedisConsumerChannel : IConsumerChannelProvider - { - private const string BEGIN_OF_STREAM = "0000000000000"; - /// - /// Max delay - /// - private const int MAX_DELAY = 5000; - /// - /// The read by identifier chunk size. - /// REDIS don't have option to read direct position (it read from a position, not includes the position itself), - /// therefore read should start before the actual position. - /// - private const int READ_BY_ID_CHUNK_SIZE = 10; - /// - /// Receiver max iterations - /// - private const int READ_BY_ID_ITERATIONS = 1000 / READ_BY_ID_CHUNK_SIZE; - private static readonly ActivitySource ACTIVITY_SOURCE = new ActivitySource(EventSourceConstants.REDIS_CONSUMER_CHANNEL_SOURCE); - - private readonly ILogger _logger; - private readonly RedisConsumerChannelSetting _setting; - private readonly IEventSourceRedisConnectionFacroty _connFactory; - private readonly IConsumerStorageStrategy _defaultStorageStrategy; - private const string META_SLOT = "____"; - private const int INIT_RELEASE_DELAY = 100; - private const int MAX_RELEASE_DELAY = 1000 * 30; // 30 seconds - - #region Ctor - - /// - /// Initializes a new instance. - /// - /// The redis provider promise. - /// The logger. - /// The setting. - public RedisConsumerChannel( - IEventSourceRedisConnectionFacroty redisConnFactory, - ILogger logger, - RedisConsumerChannelSetting? setting = null) - { - _logger = logger; - _connFactory = redisConnFactory; - _defaultStorageStrategy = new RedisHashStorageStrategy(redisConnFactory); - _setting = setting ?? RedisConsumerChannelSetting.Default; - } - - /// - /// Initializes a new instance. - /// - /// The logger. - /// The configuration. - /// The setting. - /// Environment keys of the credentials - public RedisConsumerChannel( - ILogger logger, - Action? configuration = null, - RedisConsumerChannelSetting? setting = null, - RedisCredentialsKeys credentialsKeys = default) : this( - new EventSourceRedisConnectionFacroty( - logger, - configuration, - credentialsKeys), - logger, - setting) - { - } - - #endregion // Ctor - - #region SubsribeAsync - - /// - /// Subscribe to the channel for specific metadata. - /// - /// The consumer plan. - /// The function. - /// The cancellation token. - /// - /// When completed - /// - public async ValueTask SubsribeAsync( - IConsumerPlan plan, - Func> func, - CancellationToken cancellationToken) - { - var joinCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(plan.Cancellation, cancellationToken); - var joinCancellation = joinCancellationSource.Token; - ConsumerOptions options = plan.Options; - - ILogger? logger = _logger ?? plan.Logger; - logger.LogInformation("REDIS EVENT-SOURCE | SUBSCRIBE key: [{key}], consumer-group: [{consumer-group}], consumer-name: [{consumer-name}]", plan.Key(), plan.ConsumerGroup, plan.ConsumerName); - - while (!joinCancellation.IsCancellationRequested) - { - try - { - if (plan.Shard != string.Empty) - await SubsribeShardAsync(plan, func, options, joinCancellation); - else - await SubsribePartitionAsync(plan, func, options, joinCancellation); - - if (options.FetchUntilUnixDateOrEmpty != null) - break; - } - #region Exception Handling - - catch (OperationCanceledException) - { - if (_logger == null) - Console.WriteLine($"Subscribe cancellation [{plan.Key()}] event stream (may have reach the messages limit)"); - else - _logger.LogError("Subscribe cancellation [{partition}->{shard}] event stream (may have reach the messages limit)", - plan.Partition, plan.Shard); - joinCancellationSource.CancelSafe(); - } - catch (Exception ex) - { - if (_logger == null) - Console.WriteLine($"Fail to subscribe into the [{plan.Key()}] event stream"); - else - _logger.LogError(ex, "Fail to subscribe into the [{partition}->{shard}] event stream", - plan.Partition, plan.Shard); - throw; - } - - #endregion // Exception Handling - } - } - - #endregion // SubsribeAsync - - #region SubsribePartitionAsync - - /// - /// Subscribe to all shards under a partition. - /// - /// The consumer plan. - /// The function. - /// The options. - /// The cancellation token. - /// - /// When completed - /// - private async ValueTask SubsribePartitionAsync( - IConsumerPlan plan, - Func> func, - ConsumerOptions options, - CancellationToken cancellationToken) - { - var subscriptions = new Queue(); - int delay = 1; - string partition = plan.Partition; - int partitionSplit = partition.Length + 1; - while (!cancellationToken.IsCancellationRequested) - { // loop for error cases - try - { - // infinite until cancellation (return unique shareds) - var keys = GetKeysUnsafeAsync(pattern: $"{partition}:*") - .WithCancellation(cancellationToken); - - await foreach (string key in keys) - { - string shard = key.Substring(partitionSplit); - IConsumerPlan p = plan.WithShard(shard); - // infinite task (until cancellation) - Task subscription = SubsribeShardAsync(plan, func, options, cancellationToken); - subscriptions.Enqueue(subscription); - } - - break; - } - catch (Exception ex) - { - plan.Logger.LogError(ex, "Partition subscription"); - await DelayIfRetry(); - } - } - - // run until cancellation or error - await Task.WhenAll(subscriptions); - - #region DelayIfRetry - - async Task DelayIfRetry() - { - await Task.Delay(delay, cancellationToken); - delay *= Max(delay, 2); - delay = Min(MAX_DELAY, delay); - } - - #endregion // DelayIfRetry - - } - - #endregion // SubsribePartitionAsync - - #region SubsribeShardAsync - - /// - /// Subscribe to specific shard. - /// - /// The consumer plan. - /// The function. - /// The options. - /// The cancellation token. - private async Task SubsribeShardAsync( - IConsumerPlan plan, - Func> func, - ConsumerOptions options, - CancellationToken cancellationToken) - { - var claimingTrigger = options.ClaimingTrigger; - var minIdleTime = (int)options.ClaimingTrigger.MinIdleTime.TotalMilliseconds; - - string key = plan.Key(); // $"{plan.Partition}:{plan.Shard}"; - bool isFirstBatchOrFailure = true; - - CommandFlags flags = CommandFlags.None; - string? fetchUntil = options.FetchUntilUnixDateOrEmpty?.ToString(); - - ILogger logger = plan.Logger ?? _logger; - - #region await db.CreateConsumerGroupIfNotExistsAsync(...) - - await _connFactory.CreateConsumerGroupIfNotExistsAsync( - key, - RedisChannelConstants.NONE_CONSUMER, - logger); - - await _connFactory.CreateConsumerGroupIfNotExistsAsync( - key, - plan.ConsumerGroup, - logger); - - #endregion // await db.CreateConsumerGroupIfNotExistsAsync(...) - - int releaseDelay = INIT_RELEASE_DELAY; - int bachSize = options.BatchSize; - - TimeSpan delay = TimeSpan.Zero; - int emptyBatchCount = 0; - while (!cancellationToken.IsCancellationRequested && await HandleBatchAsync()) - { - } - - #region HandleBatchAsync - - // Handle single batch - async ValueTask HandleBatchAsync() - { - var policy = _setting.Policy.Policy; - return await policy.ExecuteAsync(HandleBatchBreakerAsync, cancellationToken); - } - - - async Task HandleBatchBreakerAsync(CancellationToken ct) - { - ct.ThrowIfCancellationRequested(); - - StreamEntry[] results = await ReadBatchAsync(); - emptyBatchCount = results.Length == 0 ? emptyBatchCount + 1 : 0; - results = await ClaimStaleMessages(emptyBatchCount, results, ct); - - if (results.Length == 0) - { - if (fetchUntil == null) - delay = await DelayIfEmpty(delay, cancellationToken); - return fetchUntil == null; - } - - ct.ThrowIfCancellationRequested(); - - try - { - var batchCancellation = new CancellationTokenSource(); - int i = 0; - batchCancellation.Token.Register(async () => - { - RedisValue[] freeTargets = results[i..].Select(m => m.Id).ToArray(); - await ReleaseAsync(freeTargets); - }); - for (; i < results.Length && !batchCancellation.IsCancellationRequested; i++) - { - StreamEntry result = results[i]; - - #region Metadata meta = ... - - Dictionary channelMeta = result.Values.ToDictionary(m => m.Name, m => m.Value); - Metadata meta; - string? metaJson = channelMeta[META_SLOT]; - string eventKey = ((string?)result.Id) ?? throw new ArgumentException(nameof(MetadataExtensions.Empty.EventKey)); - if (string.IsNullOrEmpty(metaJson)) - { // backward comparability - - string channelType = ((string?)channelMeta[nameof(MetadataExtensions.Empty.ChannelType)]) ?? throw new ArgumentNullException(nameof(MetadataExtensions.Empty.ChannelType)); - - if (channelType != CHANNEL_TYPE) - { - // TODO: [bnaya 2021-07] send metrics - logger.LogWarning($"{nameof(RedisConsumerChannel)} [{CHANNEL_TYPE}] omit handling message of type '{channelType}'"); - await AckAsync(result.Id); - continue; - } - - string id = ((string?)channelMeta[nameof(MetadataExtensions.Empty.MessageId)]) ?? throw new ArgumentNullException(nameof(MetadataExtensions.Empty.MessageId)); - string operation = ((string?)channelMeta[nameof(MetadataExtensions.Empty.Operation)]) ?? throw new ArgumentNullException(nameof(MetadataExtensions.Empty.Operation)); - long producedAtUnix = (long)channelMeta[nameof(MetadataExtensions.Empty.ProducedAt)]; - DateTimeOffset producedAt = DateTimeOffset.FromUnixTimeSeconds(producedAtUnix); - if (fetchUntil != null && string.Compare(fetchUntil, result.Id) < 0) - return false; - meta = new Metadata - { - MessageId = id, - EventKey = eventKey, - Environment = plan.Environment, - Partition = plan.Partition, - Shard = plan.Shard, - Operation = operation, - ProducedAt = producedAt - }; - - } - else - { - //byte[] metabytes = Convert.FromBase64String(meta64); - //string metaJson = Encoding.UTF8.GetString(metabytes); - meta = JsonSerializer.Deserialize(metaJson, EventSourceOptions.FullSerializerOptions) ?? throw new ArgumentNullException(nameof(Metadata)); //, EventSourceJsonContext..Metadata); - meta = meta with { EventKey = eventKey }; - - } - - #endregion // Metadata meta = ... - - int local = i; - var cancellableIds = results[local..].Select(m => m.Id); - var ack = new AckOnce( - () => AckAsync(result.Id), - plan.Options.AckBehavior, logger, - async () => - { - batchCancellation.CancelSafe(); // cancel forward - await CancelAsync(cancellableIds); - }); - - #region OriginFilter - - MessageOrigin originFilter = plan.Options.OriginFilter; - if (originFilter != MessageOrigin.None && (originFilter & meta.Origin) == MessageOrigin.None) - { - Ack.Set(ack); - #region Log - - _logger.LogInformation("Event Source skip consuming of event [{event-key}] because if origin is [{origin}] while the origin filter is sets to [{origin-filter}], Operation:[{operation}], Stream:[{stream}]", meta.EventKey, meta.Origin, originFilter, meta.Operation, meta.Key()); - - #endregion // Log - continue; - } - - #endregion // OriginFilter - - #region var announcement = new Announcement(...) - - Bucket segmets = await GetBucketAsync(plan, channelMeta, meta, EventBucketCategories.Segments); - Bucket interceptions = await GetBucketAsync(plan, channelMeta, meta, EventBucketCategories.Interceptions); - - var announcement = new Announcement - { - Metadata = meta, - Segments = segmets, - InterceptorsData = interceptions - }; - - #endregion // var announcement = new Announcement(...) - - #region Start Telemetry Span - - ActivityContext parentContext = meta.ExtractSpan(channelMeta, ExtractTraceContext); - // Start an activity with a name following the semantic convention of the OpenTelemetry messaging specification. - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/messaging.md#span-name - var activityName = $"{meta.Operation} consume"; - - bool traceAsParent = (DateTimeOffset.UtcNow - meta.ProducedAt) < plan.Options.TraceAsParent; - ActivityContext parentActivityContext = traceAsParent ? parentContext : default; - using var activity = ACTIVITY_SOURCE.StartActivity( - activityName, - ActivityKind.Consumer, - parentActivityContext, links: new[] { new ActivityLink(parentContext) }); - meta.InjectTelemetryTags(activity); - - #region IEnumerable ExtractTraceContext(Dictionary entries, string key) - - IEnumerable ExtractTraceContext(Dictionary entries, string key) - { - try - { - if (entries.TryGetValue(key, out var value)) - { - if (string.IsNullOrEmpty(value)) - return Array.Empty(); - return new[] { value.ToString() }; - } - } - #region Exception Handling - - catch (Exception ex) - { - Exception err = ex.FormatLazy(); - _logger.LogError(err, "Failed to extract trace context: {error}", err); - } - - #endregion // Exception Handling - - return Enumerable.Empty(); - } - - #endregion // IEnumerable ExtractTraceContext(Dictionary entries, string key) - - #endregion // Start Telemetry Span - - bool succeed = await func(announcement, ack); - if (succeed) - { - releaseDelay = INIT_RELEASE_DELAY; - bachSize = options.BatchSize; - } - else - { - if (options.PartialBehavior == Enums.PartialConsumerBehavior.Sequential) - { - RedisValue[] freeTargets = results[i..].Select(m => m.Id).ToArray(); - await ReleaseAsync(freeTargets); - await Task.Delay(1000, ct); - } - } - } - } - catch - { - isFirstBatchOrFailure = true; - } - return true; - } - - #endregion // HandleBatchAsync - - #region ReadBatchAsync - - // read batch entities from REDIS - async Task ReadBatchAsync() - { - // TBD: circuit-breaker - try - { - var r = await _setting.Policy.Policy.ExecuteAsync(async (ct) => - { - ct.ThrowIfCancellationRequested(); - StreamEntry[] values = Array.Empty(); - values = await ReadSelfPending(); - - if (values.Length == 0) - { - isFirstBatchOrFailure = false; - - IConnectionMultiplexer conn = await _connFactory.GetAsync(); - IDatabaseAsync db = conn.GetDatabase(); - - values = await db.StreamReadGroupAsync( - key, - plan.ConsumerGroup, - plan.ConsumerName, - position: StreamPosition.NewMessages, - count: bachSize, - flags: flags) - .WithCancellation(ct, () => Array.Empty()) - .WithCancellation(cancellationToken, () => Array.Empty()); - } - StreamEntry[] results = values ?? Array.Empty(); - return results; - }, cancellationToken); - return r; - } - #region Exception Handling - - catch (RedisTimeoutException ex) - { - logger.LogWarning(ex, "Event source [{source}] by [{consumer}]: Timeout", key, plan.ConsumerName); - return Array.Empty(); - } - catch (Exception ex) - { - logger.LogError(ex, "Fail to read from event source [{source}] by [{consumer}]", key, plan.ConsumerName); - return Array.Empty(); - } - - #endregion // Exception Handling - } - - #endregion // ReadBatchAsync - - #region ReadSelfPending - - // Check for pending messages of the current consumer (crash scenario) - async Task ReadSelfPending() - { - StreamEntry[] values = Array.Empty(); - if (!isFirstBatchOrFailure) - return values; - - - IConnectionMultiplexer conn = await _connFactory.GetAsync(); - IDatabaseAsync db = conn.GetDatabase(); - StreamPendingMessageInfo[] pendMsgInfo = await db.StreamPendingMessagesAsync( - key, - plan.ConsumerGroup, - options.BatchSize, - plan.ConsumerName, - flags: CommandFlags.DemandMaster); - if (pendMsgInfo != null && pendMsgInfo.Length != 0) - { - var ids = pendMsgInfo - .Select(m => m.MessageId).ToArray(); - if (ids.Length != 0) - { - values = await db.StreamClaimAsync(key, - plan.ConsumerGroup, - plan.ConsumerName, - 0, - ids, - flags: CommandFlags.DemandMaster); - values = values ?? Array.Empty(); - _logger.LogInformation("Claimed messages: {ids}", ids); - } - } - - return values; - } - - #endregion // ReadSelfPending - - #region ClaimStaleMessages - - // Taking work from other consumers which have log-time's pending messages - async Task ClaimStaleMessages( - int emptyBatchCount, - StreamEntry[] values, - CancellationToken ct) - { - var logger = plan.Logger ?? _logger; - ct.ThrowIfCancellationRequested(); - if (values.Length != 0) return values; - if (emptyBatchCount < claimingTrigger.EmptyBatchCount) - return values; - try - { - IDatabaseAsync db = await _connFactory.GetDatabaseAsync(); - StreamPendingInfo pendingInfo = await db.StreamPendingAsync(key, plan.ConsumerGroup, flags: CommandFlags.DemandMaster); - foreach (var c in pendingInfo.Consumers) - { - var self = c.Name == plan.ConsumerName; - if (self) continue; - try - { - var pendMsgInfo = await db.StreamPendingMessagesAsync( - key, - plan.ConsumerGroup, - 10, - c.Name, - pendingInfo.LowestPendingMessageId, - pendingInfo.HighestPendingMessageId, - flags: CommandFlags.DemandMaster); - - - - RedisValue[] ids = pendMsgInfo - .Where(x => x.IdleTimeInMilliseconds > minIdleTime) - .Select(m => m.MessageId).ToArray(); - if (ids.Length == 0) - continue; - - #region Log - logger.LogInformation("Event Source Consumer [{name}]: Claimed {count} messages, from Consumer [{name}]", plan.ConsumerName, c.PendingMessageCount, c.Name); - - #endregion // Log - - // will claim messages only if older than _setting.ClaimingTrigger.MinIdleTime - values = await db.StreamClaimAsync(key, - plan.ConsumerGroup, - c.Name, - minIdleTime, - ids, - flags: CommandFlags.DemandMaster); - if (values.Length != 0) - logger.LogInformation("Event Source Consumer [{name}]: Claimed {count} messages, from Consumer [{name}]", plan.ConsumerName, c.PendingMessageCount, c.Name); - } - #region Exception Handling - - catch (RedisTimeoutException ex) - { - logger.LogWarning(ex, "Timeout (handle pending): {name}{self}", c.Name, self); - continue; - } - - catch (Exception ex) - { - logger.LogError(ex, "Fail to claim pending: {name}{self}", c.Name, self); - } - - #endregion // Exception Handling - - if (values != null && values.Length != 0) - return values; - } - } - #region Exception Handling - - catch (RedisConnectionException ex) - { - _logger.LogWarning(ex, "Fail to claim REDIS's pending"); - } - - catch (Exception ex) - { - _logger.LogError(ex, "Fail to claim pending"); - } - - #endregion // Exception Handling - - return Array.Empty(); - } - - #endregion // ClaimStaleMessages - - #region AckAsync - - // Acknowledge event handling (prevent re-consuming of the message). - async ValueTask AckAsync(RedisValue messageId) - { - try - { - IConnectionMultiplexer conn = await _connFactory.GetAsync(); - IDatabaseAsync db = conn.GetDatabase(); - // release the event (won't handle again in the future) - long id = await db.StreamAcknowledgeAsync(key, - plan.ConsumerGroup, - messageId, - flags: CommandFlags.DemandMaster); - } - catch (Exception) - { // TODO: [bnaya 2020-10] do better handling (re-throw / swallow + reason) currently logged at the wrapping class - throw; - } - } - - #endregion // AckAsync - - #region CancelAsync - - // Cancels the asynchronous. - ValueTask CancelAsync(IEnumerable messageIds) - { - // no way to release consumed item back to the stream - //try - //{ - // // release the event (won't handle again in the future) - // await db.StreamClaimIdsOnlyAsync(key, - // plan.ConsumerGroup, - // RedisValue.Null, - // 0, - // messageIds.ToArray(), - // flags: CommandFlags.DemandMaster); - //} - //catch (Exception) - //{ // TODO: [bnaya 2020-10] do better handling (re-throw / swallow + reason) currently logged at the wrapping class - // throw; - //} - return ValueTask.CompletedTask; - - } - - #endregion // CancelAsync - - #region ReleaseAsync - - - // Releases the messages (work around). - async Task ReleaseAsync(RedisValue[] freeTargets) - { - IConnectionMultiplexer conn = await _connFactory.GetAsync(); - IDatabaseAsync db = conn.GetDatabase(); - await db.StreamClaimAsync(plan.Key(), - plan.ConsumerGroup, - RedisChannelConstants.NONE_CONSUMER, - 1, - freeTargets, - flags: CommandFlags.DemandMaster); - await Task.Delay(releaseDelay, cancellationToken); - if (releaseDelay < MAX_RELEASE_DELAY) - releaseDelay = Math.Min(releaseDelay * 2, MAX_RELEASE_DELAY); - - if (bachSize == options.BatchSize) - bachSize = 1; - else - bachSize = Math.Min(bachSize * 2, options.BatchSize); - } - - #endregion // ReleaseAsync - } - - #endregion // SubsribeShardAsync - - #region GetByIdAsync - - /// - /// Gets announcement data by id. - /// - /// The entry identifier. - /// The plan. - /// The cancellation token. - /// - /// IConsumerChannelProvider.GetAsync of [{entryId}] from [{plan.Partition}->{plan.Shard}] return nothing. - /// IConsumerChannelProvider.GetAsync of [{entryId}] from [{plan.Partition}->{plan.Shard}] was expecting single result but got [{entries.Length}] results - async ValueTask IConsumerChannelProvider.GetByIdAsync( - EventKey entryId, - IConsumerPlan plan, - CancellationToken cancellationToken) - { - string mtdName = $"{nameof(IConsumerChannelProvider)}.{nameof(IConsumerChannelProvider.GetByIdAsync)}"; - - try - { - IConnectionMultiplexer conn = await _connFactory.GetAsync(); - IDatabaseAsync db = conn.GetDatabase(); - ILogger logger = plan.Logger; - StreamEntry entry = await FindAsync(entryId); - - #region var announcement = new Announcement(...) - - Dictionary channelMeta = entry.Values.ToDictionary(m => m.Name, m => m.Value); - string channelType = GetMeta(nameof(MetadataExtensions.Empty.ChannelType)); - string id = GetMeta(nameof(MetadataExtensions.Empty.MessageId)); - string operation = GetMeta(nameof(MetadataExtensions.Empty.Operation)); - long producedAtUnix = (long)channelMeta[nameof(MetadataExtensions.Empty.ProducedAt)]; - - #region string GetMeta(string propKey) - - string GetMeta(string propKey) - { - string? result = channelMeta[propKey]; - if (result == null) throw new ArgumentNullException(propKey); - return result; - } - - #endregion // string GetMeta(string propKey) - - DateTimeOffset producedAt = DateTimeOffset.FromUnixTimeSeconds(producedAtUnix); -#pragma warning disable CS8601 // Possible null reference assignment. - var meta = new Metadata - { - MessageId = id, - EventKey = entry.Id, - Environment = plan.Environment, - Partition = plan.Partition, - Shard = plan.Shard, - Operation = operation, - ProducedAt = producedAt, - ChannelType = channelType - }; -#pragma warning restore CS8601 // Possible null reference assignment. - - Bucket segmets = await GetBucketAsync(plan, channelMeta, meta, EventBucketCategories.Segments); - Bucket interceptions = await GetBucketAsync(plan, channelMeta, meta, EventBucketCategories.Interceptions); - - var announcement = new Announcement - { - Metadata = meta, - Segments = segmets, - InterceptorsData = interceptions - }; - - #endregion // var announcement = new Announcement(...) - - return announcement; - - #region FindAsync - - async Task FindAsync(EventKey entryId) - { - string lookForId = (string)entryId; - string key = plan.Key(); - - string originId = lookForId; - int len = originId.IndexOf('-'); - string fromPrefix = originId.Substring(0, len); - long start = long.Parse(fromPrefix); - string startPosition = (start - 1).ToString(); - int iteration = 0; - for (int i = 0; i < READ_BY_ID_ITERATIONS; i++) // up to 1000 items - { - iteration++; - StreamEntry[] entries = await db.StreamReadAsync( - key, - startPosition, - READ_BY_ID_CHUNK_SIZE, - CommandFlags.DemandMaster); - if (entries.Length == 0) - throw new KeyNotFoundException($"{mtdName} of [{lookForId}] from [{key}] return nothing, start at ({startPosition}, iteration = {iteration})."); - string k = string.Empty; - foreach (StreamEntry e in entries) - { -#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. -#pragma warning disable CS8602 // Dereference of a possibly null reference. - k = e.Id; - string ePrefix = k.Substring(0, len); -#pragma warning restore CS8602 // Dereference of a possibly null reference. -#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. - long comp = long.Parse(ePrefix); - if (comp < start) - continue; // not there yet - if (k == lookForId) - { - return e; - } - if (ePrefix != fromPrefix) - throw new KeyNotFoundException($"{mtdName} of [{lookForId}] from [{key}] return not exists."); - } - startPosition = k; // next batch will start from last entry - } - throw new KeyNotFoundException($"{mtdName} of [{lookForId}] from [{key}] return not found."); - } - - #endregion // FindAsync - } - #region Exception Handling - - catch (Exception ex) - { - string key = plan.Key(); - _logger.LogError(ex.FormatLazy(), "{mtd} Failed: Entry [{entryId}] from [{key}] event stream", - mtdName, entryId, key); - throw; - } - - #endregion // Exception Handling - } - - #endregion // GetByIdAsync - - #region GetAsyncEnumerable - - /// - /// Gets asynchronous enumerable of announcements. - /// - /// The plan. - /// The options. - /// The cancellation token. - /// - async IAsyncEnumerable IConsumerChannelProvider.GetAsyncEnumerable( - IConsumerPlan plan, - ConsumerAsyncEnumerableOptions? options, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - string mtdName = $"{nameof(IConsumerChannelProvider)}.{nameof(IConsumerChannelProvider.GetAsyncEnumerable)}"; - - IConnectionMultiplexer conn = await _connFactory.GetAsync(); - IDatabaseAsync db = conn.GetDatabase(); - ILogger logger = plan.Logger; - var loop = AsyncLoop().WithCancellation(cancellationToken); - await foreach (StreamEntry entry in loop) - { - if (cancellationToken.IsCancellationRequested) yield break; - - #region var announcement = new Announcement(...) - - Dictionary channelMeta = entry.Values.ToDictionary(m => m.Name, m => m.Value); -#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. -#pragma warning disable CS8601 // Possible null reference assignment. - string channelType = channelMeta[nameof(MetadataExtensions.Empty.ChannelType)]; - string id = channelMeta[nameof(MetadataExtensions.Empty.MessageId)]; - string operation = channelMeta[nameof(MetadataExtensions.Empty.Operation)]; - long producedAtUnix = (long)channelMeta[nameof(MetadataExtensions.Empty.ProducedAt)]; - DateTimeOffset producedAt = DateTimeOffset.FromUnixTimeSeconds(producedAtUnix); - var meta = new Metadata - { - MessageId = id, - EventKey = entry.Id, - Environment = plan.Environment, - Partition = plan.Partition, - Shard = plan.Shard, - Operation = operation, - ProducedAt = producedAt - }; -#pragma warning restore CS8601 // Possible null reference assignment. -#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. - var filter = options?.OperationFilter; - if (filter != null && !filter(meta)) - continue; - - Bucket segmets = await GetBucketAsync(plan, channelMeta, meta, EventBucketCategories.Segments); - Bucket interceptions = await GetBucketAsync(plan, channelMeta, meta, EventBucketCategories.Interceptions); - - var announcement = new Announcement - { - Metadata = meta, - Segments = segmets, - InterceptorsData = interceptions - }; - - #endregion // var announcement = new Announcement(...) - - yield return announcement; - }; - - #region AsyncLoop - - async IAsyncEnumerable AsyncLoop() - { - string key = plan.Key(); - - int iteration = 0; - RedisValue startPosition = options?.From ?? BEGIN_OF_STREAM; - TimeSpan delay = TimeSpan.Zero; - while (true) - { - if (cancellationToken.IsCancellationRequested) yield break; - - iteration++; - StreamEntry[] entries = await db.StreamReadAsync( - key, - startPosition, - READ_BY_ID_CHUNK_SIZE, - CommandFlags.DemandMaster); - if (entries.Length == 0) - { - if (options?.ExitWhenEmpty ?? true) yield break; - delay = await DelayIfEmpty(delay, cancellationToken); - } - string k = string.Empty; - foreach (StreamEntry e in entries) - { - if (cancellationToken.IsCancellationRequested) yield break; - -#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. - k = e.Id; -#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. - if (options?.To != null && string.Compare(options?.To, k) < 0) - yield break; - yield return e; - } - startPosition = k; // next batch will start from last entry - } - } - - #endregion // AsyncLoop - } - - #endregion // GetAsyncEnumerable - - #region ValueTask GetBucketAsync(StorageType storageType) // local function - - /// - /// Gets a data bucket. - /// - /// The plan. - /// The channel meta. - /// The meta. - /// Type of the storage. - /// - private async ValueTask GetBucketAsync( - IConsumerPlan plan, - Dictionary channelMeta, - Metadata meta, - EventBucketCategories storageType) - { - - IEnumerable strategies = await plan.StorageStrategiesAsync; - strategies = strategies.Where(m => m.IsOfTargetType(storageType)); - Bucket bucket = Bucket.Empty; - if (strategies.Any()) - { - foreach (var strategy in strategies) - { - bucket = await strategy.LoadBucketAsync(meta, bucket, storageType, LocalGetProperty); - } - } - else - { - bucket = await _defaultStorageStrategy.LoadBucketAsync(meta, bucket, storageType, LocalGetProperty); - } - - return bucket; - -#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. -#pragma warning disable CS8603 // Possible null reference return. - string LocalGetProperty(string k) => (string)channelMeta[k]; -#pragma warning restore CS8603 // Possible null reference return. -#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. - } - - #endregion // ValueTask StoreBucketAsync(StorageType storageType) // local function - - #region DelayIfEmpty - - // avoiding system hit when empty (mitigation of self DDoS) - private async Task DelayIfEmpty(TimeSpan previousDelay, CancellationToken cancellationToken) - { - var cfg = _setting.DelayWhenEmptyBehavior; - var newDelay = cfg.CalcNextDelay(previousDelay); - var limitDelay = Min(cfg.MaxDelay.TotalMilliseconds, newDelay.TotalMilliseconds); - newDelay = TimeSpan.FromMilliseconds(limitDelay); - await Task.Delay(newDelay, cancellationToken); - return newDelay; - } - - #endregion // DelayIfEmpty - - #region GetKeysUnsafeAsync - - /// - /// Gets the keys unsafe asynchronous. - /// - /// The pattern. - /// The cancellation token. - /// - public async IAsyncEnumerable GetKeysUnsafeAsync( - string pattern, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - IConnectionMultiplexer multiplexer = await _connFactory.GetAsync(); - var distict = new HashSet(); - while (!cancellationToken.IsCancellationRequested) - { - foreach (EndPoint endpoint in multiplexer.GetEndPoints()) - { - IServer server = multiplexer.GetServer(endpoint); - // TODO: [bnaya 2020_09] check the pagination behavior -#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. -#pragma warning disable CS8604 // Possible null reference argument. - await foreach (string key in server.KeysAsync(pattern: pattern)) - { - if (distict.Contains(key)) - continue; - distict.Add(key); - yield return key; - } -#pragma warning restore CS8604 // Possible null reference argument. -#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. - } - } - } - - #endregion // GetKeysUnsafeAsync - } -} diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/RedisConsumerProviderExtensions.cs b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/RedisConsumerProviderExtensions.cs deleted file mode 100644 index ca951683..00000000 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/RedisConsumerProviderExtensions.cs +++ /dev/null @@ -1,115 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -using StackExchange.Redis; - -using Weknow.EventSource.Backbone.Building; -using Weknow.EventSource.Backbone.Channels.RedisProvider; - -namespace Weknow.EventSource.Backbone -{ - public static class RedisConsumerProviderExtensions - { - /// - /// Uses REDIS consumer channel. - /// - /// The builder. - /// The setting. - /// The redis configuration. - /// Environment keys of the credentials - /// - public static IConsumerStoreStrategyBuilder UseRedisChannel( - this IConsumerBuilder builder, - Func setting, - Action? redisConfiguration = null, - RedisCredentialsKeys credentialsKeys = default) - { - var stg = setting?.Invoke(RedisConsumerChannelSetting.Default); - var channelBuilder = builder.UseChannel(LocalCreate); - return channelBuilder; - - IConsumerChannelProvider LocalCreate(ILogger logger) - { - var channel = new RedisConsumerChannel( - logger, - redisConfiguration, - stg, - credentialsKeys); - return channel; - } - } - - /// - /// Uses REDIS consumer channel. - /// - /// The builder. - /// The setting. - /// Environment keys of the credentials - /// - public static IConsumerStoreStrategyBuilder UseRedisChannel( - this IConsumerBuilder builder, - RedisConsumerChannelSetting? setting = null, - RedisCredentialsKeys credentialsKeys = default) - { - var cfg = setting ?? RedisConsumerChannelSetting.Default; - var channelBuilder = builder.UseChannel(LocalCreate); - return channelBuilder; - - IConsumerChannelProvider LocalCreate(ILogger logger) - { - var channel = new RedisConsumerChannel( - logger, - setting: cfg, - credentialsKeys: credentialsKeys); - return channel; - } - } - - /// - /// Uses REDIS consumer channel. - /// - /// The builder. - /// The redis client factory. - /// The setting. - /// - public static IConsumerStoreStrategyBuilder UseRedisChannel( - this IConsumerBuilder builder, - IEventSourceRedisConnectionFacroty redisClientFactory, - RedisConsumerChannelSetting? setting = null) - { - var cfg = setting ?? RedisConsumerChannelSetting.Default; - var channelBuilder = builder.UseChannel(LocalCreate); - return channelBuilder; - - IConsumerChannelProvider LocalCreate(ILogger logger) - { - var channel = new RedisConsumerChannel( - redisClientFactory, - logger, - setting); - return channel; - } - } - - /// - /// Uses REDIS consumer channel. - /// - /// The builder. - /// The service provider. - /// The setting. - /// - /// redisClient - public static IConsumerStoreStrategyBuilder UseRedisChannelInjection( - this IConsumerBuilder builder, - IServiceProvider serviceProvider, - RedisConsumerChannelSetting? setting = null) - { - var connFactory = serviceProvider.GetService(); - if (connFactory == null) - throw new RedisConnectionException(ConnectionFailureType.None, $"{nameof(IEventSourceRedisConnectionFacroty)} is not registerd, use services.{nameof(RedisDiExtensions.AddEventSourceRedisConnection)} in order to register it at Setup stage."); - return builder.UseRedisChannel(connFactory, setting); - } - - } -} diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider.csproj b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider.csproj deleted file mode 100644 index 32920e42..00000000 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/Weknow.EventSource.Backbone.Channels.RedisConsumerProviderr.xml b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/Weknow.EventSource.Backbone.Channels.RedisConsumerProviderr.xml deleted file mode 100644 index 6c625420..00000000 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/Weknow.EventSource.Backbone.Channels.RedisConsumerProviderr.xml +++ /dev/null @@ -1,286 +0,0 @@ - - - - Weknow.EventSource.Backbone.Channels.RedisConsumerProvider - - - - - The read by identifier chunk size. - REDIS don't have option to read direct position (it read from a position, not includes the position itself), - therefore read should start before the actual position. - - - - - Initializes a new instance. - - The redis provider promise. - The logger. - The setting. - - - - Initializes a new instance. - - The redis database. - The logger. - The setting. - - - - Initializes a new instance. - - The logger. - The configuration. - The setting. - The endpoint env key. - The password env key. - - - - Subscribe to the channel for specific metadata. - - The consumer plan. - The function. - The options. - The cancellation token. - - When completed - - - - - Subscribe to all shards under a partition. - - The database. - The consumer plan. - The function. - The options. - The cancellation token. - - When completed - - - - - Subscribe to specific shard. - - The database. - The consumer plan. - The function. - The options. - The cancellation token. - - - - Gets announcement data by id. - - The entry identifier. - The plan. - The cancellation token. - - IConsumerChannelProvider.GetAsync of [{entryId}] from [{plan.Partition}->{plan.Shard}] return nothing. - IConsumerChannelProvider.GetAsync of [{entryId}] from [{plan.Partition}->{plan.Shard}] was expecting single result but got [{entries.Length}] results - - - - Gets a data bucket. - - The plan. - The channel meta. - The meta. - Type of the storage. - - - - - Gets the keys unsafe asynchronous. - - The pattern. - The cancellation token. - - - - - Behavior of delay when empty - - - - - Gets or sets the maximum delay. - - - - - Gets or sets the next delay. - - - - - Represent specific setting of the consumer channel - - - - - Define when to claim stale (long waiting) messages from other consumers - - - - - Gets or sets the resilience policy. - - - - - Behavior of delay when empty - - - - - Performs an implicit conversion. - - The policy. - - The result of the conversion. - - - - - Define when to claim stale (long waiting) messages from other consumers - - - - - Initializes a new instance. - - The policy. - - - - Initializes a new instance. - - The on break. - The on reset. - The on half open. - The on retry. - - - - Gets or sets the batch reading policy. - - - - - Performs an implicit conversion. - - The policy. - - The result of the conversion. - - - - - Performs an implicit conversion. - - The instance. - - The result of the conversion. - - - - - Define when to claim stale (long waiting) messages from other consumers - - - - - Empty batch count define number of empty fetching cycle in a row - which will trigger operation of trying to get stale messages from other consumers. - - - - - The minimum message idle time to allow the reassignment of the message(s). - - - - - Responsible to save information to REDIS hash storage. - The information can be either Segmentation or Interception. - When adding it via the builder it can be arrange in a chain in order of having - 'Chain of Responsibility' for saving different parts into different storage (For example GDPR's PII). - Alternative, chain can serve as a cache layer. - - - - - Initializes a new instance. - - The database task. - - - - Load the bucket information. - - The meta fetch provider. - The current bucket (previous item in the chain). - The type of the storage. - The get property. - The cancellation. - - Either Segments or Interceptions. - - - - - - Uses REDIS consumer channel. - - The builder. - The setting. - The redis configuration. - The endpoint env key. - The password env key. - - - - - Uses REDIS consumer channel. - - The builder. - The setting. - The endpoint env key. - The password env key. - - - - - Uses REDIS consumer channel. - - The builder. - The redis client. - The setting. - - - - - Uses REDIS consumer channel. - - The builder. - The redis client. - The setting. - - - - - Uses REDIS consumer channel. - - The builder. - The service provider. - The setting. - - redisClient - - - diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/icon.png b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/icon.png deleted file mode 100644 index d6811ad8..00000000 Binary files a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisConsumerProvider/icon.png and /dev/null differ diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProducerProvider/RedisProducerChannel.cs b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProducerProvider/RedisProducerChannel.cs deleted file mode 100644 index 7cfe0213..00000000 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProducerProvider/RedisProducerChannel.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System.Collections.Immutable; -using System.Diagnostics; -using System.Text.Json; - -using Microsoft.Extensions.Logging; - -using OpenTelemetry; - -using Polly; - -using StackExchange.Redis; - -using static Weknow.EventSource.Backbone.Channels.RedisProvider.Common.RedisChannelConstants; - -namespace Weknow.EventSource.Backbone.Channels.RedisProvider -{ - internal class RedisProducerChannel : IProducerChannelProvider - { - private static readonly ActivitySource ACTIVITY_SOURCE = new ActivitySource(EventSourceConstants.REDIS_PRODUCER_CHANNEL_SOURCE); - private readonly ILogger _logger; - private readonly AsyncPolicy _resiliencePolicy; - private readonly IEventSourceRedisConnectionFacroty _connFactory; - private readonly IProducerStorageStrategy _defaultStorageStrategy; - private const string META_SLOT = "____"; - - #region Ctor - - /// - /// Initializes a new instance. - /// - /// The redis database promise. - /// The logger. - /// The resilience policy for retry. - public RedisProducerChannel( - IEventSourceRedisConnectionFacroty redisFactory, - ILogger logger, - AsyncPolicy? resiliencePolicy) - { - _connFactory = redisFactory; - _logger = logger; - _resiliencePolicy = resiliencePolicy ?? - Policy.Handle() - .RetryAsync(3); - _defaultStorageStrategy = new RedisHashStorageStrategy(_connFactory, logger); - } - - - #endregion // Ctor - - #region GetDB - - /// - /// Gets the database. - /// - /// The redis connection factory. - /// - private static async Task GetDB(IEventSourceRedisConnectionFacroty redisConnFactory) - { - var mp = await redisConnFactory.GetAsync(); - return mp.GetDatabase(); - } - - #endregion // GetDB - - #region SendAsync - - /// - /// Sends raw announcement. - /// - /// The raw announcement data. - /// The storage strategy. - /// - /// Return the message id - /// - public async ValueTask SendAsync( - Announcement payload, - ImmutableArray storageStrategy) - { - Metadata meta = payload.Metadata; - string id = meta.MessageId; - - #region var entries = new NameValueEntry[]{...} - - string metaJson = JsonSerializer.Serialize(meta, EventSourceOptions.FullSerializerOptions); - - // local method - NameValueEntry KV(RedisValue key, RedisValue value) => new NameValueEntry(key, value); - ImmutableArray commonEntries = ImmutableArray.Create( - KV(nameof(meta.MessageId), id), - KV(nameof(meta.Operation), meta.Operation), - KV(nameof(meta.ProducedAt), meta.ProducedAt.ToUnixTimeSeconds()), - KV(nameof(meta.ChannelType), CHANNEL_TYPE), - KV(nameof(meta.Origin), meta.Origin.ToString()), - KV(META_SLOT, metaJson) - ); - - #endregion // var entries = new NameValueEntry[]{...} - - RedisValue messageId = await _resiliencePolicy.ExecuteAsync(LocalStreamAddAsync); - - return (string?)messageId ?? "0000000000000-0"; - - #region LocalStreamAddAsync - - async Task LocalStreamAddAsync() - { - await LocalStoreBucketAsync(EventBucketCategories.Segments); - await LocalStoreBucketAsync(EventBucketCategories.Interceptions); - - var telemetryBuilder = commonEntries.ToBuilder(); - var activityName = $"{meta.Operation} produce"; - using Activity? activity = ACTIVITY_SOURCE.StartActivity(activityName, ActivityKind.Producer); - activity.InjectSpan(meta, telemetryBuilder, LocalInjectTelemetry); - meta.InjectTelemetryTags(activity); - var entries = telemetryBuilder.ToArray(); - - try - { - IConnectionMultiplexer conn = await _connFactory.GetAsync(); - IDatabaseAsync db = conn.GetDatabase(); - using var scope = SuppressInstrumentationScope.Begin(); - var k = meta.Key(); - var result = await db.StreamAddAsync(k, entries, - flags: CommandFlags.DemandMaster); - return result; - } - #region Exception Handling - - catch (RedisConnectionException ex) - { - _logger.LogError(ex, "REDIS Connection Failure: push event [{id}] into the [{partition}->{shard}] stream: {operation}", - meta.MessageId, meta.Partition, meta.Shard, meta.Operation); - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Fail to push event [{id}] into the [{partition}->{shard}] stream: {operation}", - meta.MessageId, meta.Partition, meta.Shard, meta.Operation); - throw; - } - - #endregion // Exception Handling - - #region ValueTask StoreBucketAsync(StorageType storageType) // local function - - async ValueTask LocalStoreBucketAsync(EventBucketCategories storageType) - { - var strategies = storageStrategy.Where(m => m.IsOfTargetType(storageType)); - Bucket bucket = storageType == EventBucketCategories.Segments ? payload.Segments : payload.InterceptorsData; - if (strategies.Any()) - { - foreach (var strategy in strategies) - { - await SaveBucketAsync(strategy); - } - } - else - { - await SaveBucketAsync(_defaultStorageStrategy); - } - - async ValueTask SaveBucketAsync(IProducerStorageStrategy strategy) - { - IImmutableDictionary metaItems = - await strategy.SaveBucketAsync(id, bucket, storageType, meta); - foreach (var item in metaItems) - { - commonEntries = commonEntries.Add(KV(item.Key, item.Value)); - } - - } - } - - #endregion // ValueTask StoreBucketAsync(StorageType storageType) // local function - #region LocalInjectTelemetry - - void LocalInjectTelemetry( - ImmutableArray.Builder builder, - string key, - string value) - { - builder.Add(KV(key, value)); - } - - #endregion // LocalInjectTelemetry - } - - #endregion // LocalStreamAddAsync - } - - #endregion // SendAsync - } -} diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProducerProvider/RedisProviderExtensions.cs b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProducerProvider/RedisProviderExtensions.cs deleted file mode 100644 index fb5f58e9..00000000 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProducerProvider/RedisProviderExtensions.cs +++ /dev/null @@ -1,100 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -using OpenTelemetry.Trace; - -using Polly; - -using StackExchange.Redis; - -using Weknow.EventSource.Backbone.Channels.RedisProvider; -using Weknow.EventSource.Backbone.Private; - -namespace Weknow.EventSource.Backbone -{ - public static class RedisProviderExtensions - { - /// - /// Adds the event producer telemetry source (will result in tracing the producer). - /// - /// The builder. - /// - public static TracerProviderBuilder AddEventProducerTelemetry(this TracerProviderBuilder builder) => builder.AddSource(nameof(RedisProducerChannel)); - - /// - /// Uses REDIS producer channel. - /// - /// The builder. - /// The configuration. - /// The resilience policy. - /// Environment keys of the credentials - /// - /// - public static IProducerStoreStrategyBuilder UseRedisChannel( - this IProducerBuilder builder, - Action? configuration = null, - AsyncPolicy? resiliencePolicy = null, - RedisCredentialsKeys credentialsKeys = default) - { - var result = builder.UseChannel(LocalCreate); - return result; - - IProducerChannelProvider LocalCreate(ILogger logger) - { - var connFactory = new EventSourceRedisConnectionFacroty(logger, configuration, credentialsKeys); - var channel = new RedisProducerChannel( - connFactory, - logger ?? EventSourceFallbakLogger.Default, - resiliencePolicy); - return channel; - } - } - - /// - /// Uses REDIS producer channel. - /// - /// The builder. - /// The redis database. - /// The resilience policy. - /// - /// - public static IProducerStoreStrategyBuilder UseRedisChannel( - this IProducerBuilder builder, - IEventSourceRedisConnectionFacroty redisConnectionFactory, - AsyncPolicy? resiliencePolicy = null) - { - var result = builder.UseChannel(LocalCreate); - return result; - - IProducerChannelProvider LocalCreate(ILogger logger) - { - var channel = new RedisProducerChannel( - redisConnectionFactory, - logger ?? EventSourceFallbakLogger.Default, - resiliencePolicy); - return channel; - } - } - - /// - /// Uses REDIS producer channel. - /// - /// The builder. - /// The service provider. - /// The resilience policy. - /// - public static IProducerStoreStrategyBuilder UseRedisChannelInjection( - this IProducerBuilder builder, - IServiceProvider serviceProvider, - AsyncPolicy? resiliencePolicy = null) - { - ILogger logger = serviceProvider.GetService>() ?? throw new ArgumentNullException(); - - var connFactory = serviceProvider.GetService(); - if (connFactory == null) - throw new RedisConnectionException(ConnectionFailureType.None, $"{nameof(IEventSourceRedisConnectionFacroty)} is not registerd, use services.{nameof(RedisDiExtensions.AddEventSourceRedisConnection)} in order to register it at Setup stage."); - return builder.UseRedisChannel(connFactory, resiliencePolicy); - } - } -} diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProducerProvider/Weknow.EventSource.Backbone.Channels.RedisProducerProvider.csproj b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProducerProvider/Weknow.EventSource.Backbone.Channels.RedisProducerProvider.csproj deleted file mode 100644 index ea775aff..00000000 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProducerProvider/Weknow.EventSource.Backbone.Channels.RedisProducerProvider.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProducerProvider/Weknow.EventSource.Backbone.Channels.RedisProducerProvider.xml b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProducerProvider/Weknow.EventSource.Backbone.Channels.RedisProducerProvider.xml deleted file mode 100644 index df46b984..00000000 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProducerProvider/Weknow.EventSource.Backbone.Channels.RedisProducerProvider.xml +++ /dev/null @@ -1,136 +0,0 @@ - - - - Weknow.EventSource.Backbone.Channels.RedisProducerProvider - - - - - Initializes a new instance. - - The redis database promise. - The logger. - The resilience policy for retry. - - - - Initializes a new instance. - - The redis database promise. - The logger. - The resilience policy for retry. - - - - Initializes a new instance. - - The redis database. - The logger. - The resilience policy for retry. - - - - Initializes a new instance. - - The logger. - The configuration. - The resilience policy for retry. - The endpoint env key. - The password env key. - - - - Gets the database. - - The redis. - - - - - Sends raw announcement. - - The raw announcement data. - The storage strategy. - - Return the message id - - - - - Responsible to save information to REDIS hash storage. - The information can be either Segmentation or Interception. - When adding it via the builder it can be arrange in a chain in order of having - 'Chain of Responsibility' for saving different parts into different storage (For example GDPR's PII). - Alternative, chain can serve as a cache layer. - - - - - Initializes a new instance. - - The database task. - The logger. - - - - Saves the bucket information. - - The identifier. - Either Segments or Interceptions. - The type. - The metadata. - The cancellation. - - Array of metadata entries which can be used by the consumer side storage strategy, in order to fetch the data. - - - - - Adds the event producer telemetry source (will result in tracing the producer). - - The builder. - - - - - Uses REDIS producer channel. - - The builder. - The configuration. - The resilience policy. - The endpoint env key. - The password env key. - - - - - - Uses REDIS producer channel. - - The builder. - The redis database. - The resilience policy. - - - - - - Uses REDIS producer channel. - - The builder. - The redis database. - The resilience policy. - - - - - - Uses REDIS producer channel. - - The builder. - The service provider. - The resilience policy. - - - - diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProducerProvider/icon.png b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProducerProvider/icon.png deleted file mode 100644 index d6811ad8..00000000 Binary files a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProducerProvider/icon.png and /dev/null differ diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Factory/EventSourceRedisConnectionFacroty.cs b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Factory/EventSourceRedisConnectionFacroty.cs deleted file mode 100644 index 9eade314..00000000 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Factory/EventSourceRedisConnectionFacroty.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Microsoft.Extensions.Logging; - -using StackExchange.Redis; - - -namespace Weknow.EventSource.Backbone -{ - /// - /// Event Source connection (for IoC) - /// Because IConnectionMultiplexer may be used by other component, - /// It's more clear to wrap the IConnectionMultiplexer for easier resove by IoC. - /// This factory is also responsible of the connection health. - /// It will return same connection as long as it healthy. - /// - public sealed class EventSourceRedisConnectionFacroty : RedisConnectionFacrotyBase, IEventSourceRedisConnectionFacroty - { - #region Ctor - - #region Overloads - - /// - /// Constructor - /// - /// - /// - /// Environment keys of the credentials - public EventSourceRedisConnectionFacroty( - ILogger logger, - Action? configuration = null, - RedisCredentialsKeys credentialsKeys = default - ) : this((ILogger)logger, configuration, credentialsKeys) - { - } - - #endregion // Overloads - - /// - /// Constructor - /// - /// - /// - /// Environment keys of the credentials - public EventSourceRedisConnectionFacroty( - ILogger logger, - Action? configuration = null, - RedisCredentialsKeys credentialsKeys = default) : base(logger, configuration, credentialsKeys) - { - //CredentialsKeys = credentialsKeys; - } - - - #endregion // Ctor - - #region Kind - - /// - /// Gets the kind. - /// - protected override string Kind => "Event-Sourcing"; - - #endregion // Kind - - //#region CredentialsKeys - - ///// - ///// Gets the credentials keys. - ///// - //protected override RedisCredentialsKeys CredentialsKeys { get; } - - //#endregion // CredentialsKeys - } -} diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Factory/IEventSourceRedisConnectionFacroty.cs b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Factory/IEventSourceRedisConnectionFacroty.cs deleted file mode 100644 index 8db35333..00000000 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Factory/IEventSourceRedisConnectionFacroty.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Weknow.EventSource.Backbone -{ - /// - /// Connection factory - /// - public interface IEventSourceRedisConnectionFacroty : IRedisConnectionFacrotyBase - { - } -} \ No newline at end of file diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Factory/IRedisConnectionFacrotyBase.cs b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Factory/IRedisConnectionFacrotyBase.cs deleted file mode 100644 index 467da1e7..00000000 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Factory/IRedisConnectionFacrotyBase.cs +++ /dev/null @@ -1,19 +0,0 @@ -using StackExchange.Redis; - -namespace Weknow.EventSource.Backbone -{ - /// - /// Connection factory - /// - public interface IRedisConnectionFacrotyBase - { - /// - /// Get a valid connection - /// - Task GetAsync(); - /// - /// Get database - /// - Task GetDatabaseAsync(); - } -} \ No newline at end of file diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Factory/RedisClientFactory.cs b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Factory/RedisClientFactory.cs deleted file mode 100644 index 659f6216..00000000 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Factory/RedisClientFactory.cs +++ /dev/null @@ -1,191 +0,0 @@ -using System.Reflection; -using System.Text; - -using Microsoft.Extensions.Logging; - -using StackExchange.Redis; - -using static Weknow.EventSource.Backbone.Channels.RedisProvider.Common.RedisChannelConstants; - -namespace Weknow.EventSource.Backbone -{ - /// - /// REDIS client factory - /// - public static class RedisClientFactory - { - private static int _index = 0; - private const string CONNECTION_NAME_PATTERN = "ev-src:{0}:{1}:{2}"; - private static readonly string? ASSEMBLY_NAME = Assembly.GetEntryAssembly()?.GetName()?.Name?.ToDash(); - private static readonly Version? ASSEMBLY_VERSION = Assembly.GetEntryAssembly()?.GetName()?.Version; - - /// - /// Blocking Create REDIS client. - /// Exist only for code which don't support async (like ASP.NET setup (AddSingleton)) - /// - /// The configuration. - /// The credential. - /// - /// Fail to establish REDIS connection - /// Fail to establish REDIS connection - public static IConnectionMultiplexer CreateProviderBlocking( - Action? configuration = null, - RedisCredentialsKeys credential = default) - { - var task = CreateProviderAsync(null, configuration, credential); - return task.Result; - } - - /// - /// Blocking Create REDIS client. - /// Exist only for code which don't support async (like ASP.NET setup (AddSingleton)) - /// - /// The logger. - /// The configuration. - /// The credential. - /// - /// Fail to establish REDIS connection - /// Fail to establish REDIS connection - public static IConnectionMultiplexer CreateProviderBlocking( - ILogger logger, - Action? configuration = null, - RedisCredentialsKeys credential = default) - { - var task = CreateProviderAsync(logger, configuration, credential); - return task.Result; - } - - /// - /// Create REDIS client. - /// - /// The configuration. - /// The credential. - /// - /// Fail to establish REDIS connection - /// Fail to establish REDIS connection - public static Task CreateProviderAsync( - Action? configuration = null, - RedisCredentialsKeys credential = default) - { - return CreateProviderAsync(null, configuration, credential); - } - - - /// - /// Create REDIS client. - /// - /// The logger. - /// The configuration. - /// The credential's environment keys. - /// - /// - /// - /// Fail to establish REDIS connection - /// Fail to establish REDIS connection - public static async Task CreateProviderAsync( - ILogger? logger, - Action? configuration = null, - RedisCredentialsKeys credential = default) - { - string endpointKey = credential.EndpointKey ?? END_POINT_KEY; - string passwordKey = credential.PasswordKey ?? PASSWORD_KEY; - string? endpoint = Environment.GetEnvironmentVariable(endpointKey); - - try - { - if (endpoint == null) - { - #region Throw + Log - - if (logger != null) - logger.LogError("REDIS CONNECTION: ENDPOINT [ENV variable: {endpointKey}] is missing", endpointKey); - else - Console.WriteLine($"REDIS CONNECTION: ENDPOINT [ENV variable: {endpointKey}] is missing"); - throw new KeyNotFoundException($"REDIS KEY [ENV variable: {endpointKey}] is missing"); - - #endregion // Throw + Log - } - - string? password = Environment.GetEnvironmentVariable(passwordKey); - - var sb = new StringBuilder(); - var writer = new StringWriter(sb); - - // https://stackexchange.github.io/StackExchange.Redis/Configuration.html - var redisConfiguration = ConfigurationOptions.Parse(endpoint); - redisConfiguration.ClientName = string.Format( - CONNECTION_NAME_PATTERN, - ASSEMBLY_NAME, - ASSEMBLY_VERSION, - Interlocked.Increment(ref _index)); - - configuration?.Invoke(redisConfiguration); - redisConfiguration.Password = password; - // keep retry to get connection on failure - redisConfiguration.AbortOnConnectFail = false; - //redisConfiguration.ConnectTimeout = 15; - //redisConfiguration.SyncTimeout = 10; - //redisConfiguration.AsyncTimeout = 10; - //redisConfiguration.DefaultDatabase = Debugger.IsAttached ? 1 : null; - - - IConnectionMultiplexer redis = await ConnectionMultiplexer.ConnectAsync(redisConfiguration, writer); - if (logger != null) - logger.LogInformation("REDIS Connection [{envKey}]: {info} succeed", endpointKey, sb); - else - Console.WriteLine($"REDIS Connection [{endpointKey}] succeed: {sb}"); - redis.ConnectionFailed += OnConnectionFailed; - redis.ErrorMessage += OnConnErrorMessage; - redis.InternalError += OnInternalConnError; - - return redis; - } - catch (Exception ex) - { - if (logger != null) - logger.LogError(ex.FormatLazy(), "REDIS CONNECTION ERROR"); - else - Console.WriteLine($"REDIS CONNECTION ERROR: {ex.FormatLazy()}"); - throw; - } - - #region Event Handlers - - void OnInternalConnError(object? sender, InternalErrorEventArgs e) - { - if (logger != null) - { - logger.LogError(e.Exception, "REDIS Connection internal failure: Failure type = {typeOfConnection}, Origin = {typeOfFailure}", - e.ConnectionType, e.Origin); - } - else - Console.WriteLine($"REDIS Connection internal failure: Failure type = {e.ConnectionType}, Origin = {e.Origin}"); - } - - void OnConnErrorMessage(object? sender, RedisErrorEventArgs e) - { - if (logger != null) - { - logger.LogWarning("REDIS Connection error: {message}", - e.Message); - } - else - Console.WriteLine($"REDIS Connection error: {e.Message}"); - } - - - void OnConnectionFailed(object? sender, ConnectionFailedEventArgs e) - { - if (logger != null) - { - logger.LogError(e.Exception, "REDIS Connection failure: Failure type = {typeOfConnection}, Failure type = {typeOfFailure}", e.ConnectionType, e.FailureType); - } - else - Console.WriteLine($"REDIS Connection failure: Failure type = {e.ConnectionType}, Failure type = {e.FailureType}"); - } - - #endregion // Event Handlers - - } - } -} diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Factory/RedisConnectionFacrotyBase.cs b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Factory/RedisConnectionFacrotyBase.cs deleted file mode 100644 index f2361057..00000000 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Factory/RedisConnectionFacrotyBase.cs +++ /dev/null @@ -1,200 +0,0 @@ -using Microsoft.Extensions.Logging; - -using StackExchange.Redis; - - -namespace Weknow.EventSource.Backbone -{ - /// - /// Event Source connection (for IoC) - /// Because IConnectionMultiplexer may be used by other component, - /// It's more clear to wrap the IConnectionMultiplexer for easier resove by IoC. - /// This factory is also responsible of the connection health. - /// It will return same connection as long as it healthy. - /// - public abstract class RedisConnectionFacrotyBase : IRedisConnectionFacrotyBase, IDisposable, IAsyncDisposable - { - private const int CLOSE_DELEY_MILLISECONDS = 5000; - private Task _redisTask; - private readonly ILogger _logger; - private readonly Action? _configuration; - private readonly RedisCredentialsKeys _credentialsKeys; - private readonly AsyncLock _lock = new AsyncLock(TimeSpan.FromSeconds(CLOSE_DELEY_MILLISECONDS)); - private DateTime _lastResetConnection = DateTime.Now; - private int _reconnectTry = 0; - - #region Ctor - - #region Overloads - - /// - /// Constructor - /// - /// The logger. - /// The configuration. - /// The credentials keys. - public RedisConnectionFacrotyBase( - ILogger logger, - Action? configuration = null, - RedisCredentialsKeys credentialsKeys = default - ) : this((ILogger)logger, configuration, credentialsKeys) - { - } - - #endregion // Overloads - - /// - /// Constructor - /// - /// The logger. - /// The configuration. - /// The credentials keys. - public RedisConnectionFacrotyBase( - ILogger logger, - Action? configuration = null, - RedisCredentialsKeys credentialsKeys = default) - { - _logger = logger; - _configuration = configuration; - _credentialsKeys = credentialsKeys; - _redisTask = RedisClientFactory.CreateProviderAsync(logger, configuration, credentialsKeys); - } - - - #endregion // Ctor - - //#region CredentialsKeys - - ///// - ///// Gets the credentials keys. - ///// - //protected abstract RedisCredentialsKeys CredentialsKeys { get; } - - //#endregion // CredentialsKeys - - #region Kind - - /// - /// Gets the kind. - /// - protected abstract string Kind { get; } - - #endregion // Kind - - #region GetAsync - - /// - /// Get a valid connection - /// - async Task IRedisConnectionFacrotyBase.GetAsync() - { - var conn = await _redisTask; - if (conn.IsConnected) - return conn; - string status = conn.GetStatus(); - _logger.LogWarning("REDIS Connection [{kind}] [{ClientName}]: status = [{status}]", - Kind, - conn.ClientName, status); - var disp = await _lock.AcquireAsync(); - using (disp) - { - conn = await _redisTask; - if (conn.IsConnected) - return conn; - int tryNumber = Interlocked.Increment(ref _reconnectTry); - _logger.LogWarning("[{kind}] Reconnecting to REDIS: try=[{tryNumber}], client name=[{clientName}]", - Kind, tryNumber, conn.ClientName); - var duration = DateTime.Now - _lastResetConnection; - if (duration > TimeSpan.FromSeconds(5)) - { - _lastResetConnection = DateTime.Now; - var cn = conn; - Task _ = Task.Delay(CLOSE_DELEY_MILLISECONDS).ContinueWith(_ => cn.CloseAsync()); - _redisTask = RedisClientFactory.CreateProviderAsync(_logger, _configuration, _credentialsKeys); - var newConn = await _redisTask; - return newConn; - } - return conn; - } - } - - #endregion // GetAsync - - #region GetDatabaseAsync - - /// - /// Get database - /// - async Task IRedisConnectionFacrotyBase.GetDatabaseAsync() - { - IRedisConnectionFacrotyBase self = this; - IConnectionMultiplexer conn = await self.GetAsync(); - IDatabaseAsync db = conn.GetDatabase(); - return db; - } - - #endregion // GetDatabaseAsync - - #region Dispose (pattern) - - /// - /// Disposed indication - /// - public bool Disposed { get; private set; } - - /// - /// Dispose - /// - /// - private void Dispose(bool disposing) - { - _logger.LogWarning("REDIS [{kind}]: Disposing connection", Kind); - if (!Disposed) - { - var conn = _redisTask.Result; - conn.Dispose(); - Disposed = true; - OnDispose(disposing); - } - } - - /// - /// Dispose - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - /// - /// Called when [dispose]. - /// - /// if set to true [disposing]. - /// - public virtual void OnDispose(bool disposing) { } - - /// - /// Dispose - /// - /// - public async ValueTask DisposeAsync() - { - _logger.LogWarning("REDIS [{kind}]: Disposing connection (async)", Kind); - var redis = await _redisTask; - redis.Dispose(); - } - - /// - /// Finalizer - /// - ~RedisConnectionFacrotyBase() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: false); - } - - #endregion // Dispose (pattern) - } -} diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Factory/RedisCredentialsKeys.cs b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Factory/RedisCredentialsKeys.cs deleted file mode 100644 index 50c4ef97..00000000 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Factory/RedisCredentialsKeys.cs +++ /dev/null @@ -1,26 +0,0 @@ -using static Weknow.EventSource.Backbone.Channels.RedisProvider.Common.RedisChannelConstants; - - -namespace Weknow.EventSource.Backbone -{ - /// - /// Environment keys for REDIS's credentials - /// - public readonly record struct RedisCredentialsKeys - { - public RedisCredentialsKeys() - { - EndpointKey = END_POINT_KEY; - PasswordKey = PASSWORD_KEY; - } - - /// - /// Endpoint Key - /// - public string EndpointKey { get; init; } - /// - /// Password Key - /// - public string PasswordKey { get; init; } - } -} diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/RedisChannelConstants.cs b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/RedisChannelConstants.cs deleted file mode 100644 index 5a4c107d..00000000 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/RedisChannelConstants.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Weknow.EventSource.Backbone.Channels.RedisProvider.Common -{ - public static class RedisChannelConstants - { - public const string CHANNEL_TYPE = "REDIS Channel V1"; - public const string META_ARRAY_SEPARATOR = "~|~"; - - public const string END_POINT_KEY = "REDIS_EVENT_SOURCE_ENDPOINT"; - public const string PASSWORD_KEY = "REDIS_EVENT_SOURCE_PASS"; - - /// - /// a work around used to release messages back to the stream (consumer) - /// - public const string NONE_CONSUMER = "__NONE_CUNSUMER__"; - - public static class MetaKeys - { - public const string SegmentsKeys = "segments-keys"; - public const string InterceptorsKeys = "interceptors-keys"; - public const string TelemetryBaggage = "telemetry-baggage"; - public const string TelemetrySpanId = "telemetry-span-id"; - public const string TelemetryTraceId = "telemetry-trace-id"; - } - } -} diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/RedisCommonProviderExtensions.cs b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/RedisCommonProviderExtensions.cs deleted file mode 100644 index a0ae8590..00000000 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/RedisCommonProviderExtensions.cs +++ /dev/null @@ -1,154 +0,0 @@ -using Microsoft.Extensions.Logging; - -using StackExchange.Redis; - -namespace Weknow.EventSource.Backbone.Private -{ - /// - /// Redis common provider extensions - /// - public static class RedisCommonProviderExtensions - { - private const int MAX_DELAY = 15_000; - private const int KEY_NOT_EXISTS_DELAY = 3_000; - - private static readonly AsyncLock _lock = new AsyncLock(TimeSpan.FromSeconds(20)); - - #region CreateConsumerGroupIfNotExistsAsync - - /// - /// Creates the consumer group if not exists asynchronous. - /// - /// The connection factory. - /// The event source key. - /// The consumer group. - /// The logger. - /// - public static async Task CreateConsumerGroupIfNotExistsAsync( - this IEventSourceRedisConnectionFacroty connFactory, - string eventSourceKey, - string consumerGroup, - ILogger logger) - { - StreamGroupInfo[] groupsInfo = Array.Empty(); - - int delay = 0; - bool exists = false; - int tryNumber = 0; - while (groupsInfo.Length == 0) - { - tryNumber++; - - IConnectionMultiplexer conn = await connFactory.GetAsync(); - IDatabaseAsync db = conn.GetDatabase(); - try - { - #region Validation (if key exists) - - if (!await db.KeyExistsAsync(eventSourceKey, - flags: CommandFlags.DemandMaster)) - { - await Task.Delay(KEY_NOT_EXISTS_DELAY); - logger.LogDebug("Key not exists (yet): {info}", CurrentInfo()); - continue; - } - - #endregion // Validation (if key exists) - - #region delay on retry - - if (delay == 0) - delay = 4; - else - { - delay = Math.Min(delay * 2, MAX_DELAY); - await Task.Delay(delay); - if (tryNumber % 10 == 0) - { - logger.LogWarning("Create Consumer Group If Not Exists: still waiting {info}", CurrentInfo()); - } - } - - - #endregion // delay on retry - - using var lk = await _lock.AcquireAsync(); - groupsInfo = await db.StreamGroupInfoAsync( - eventSourceKey, - flags: CommandFlags.DemandMaster); - exists = groupsInfo.Any(m => m.Name == consumerGroup); - } - #region Exception Handling - - catch (RedisServerException ex) - { - if (await db.KeyExistsAsync(eventSourceKey, - flags: CommandFlags.DemandMaster)) - { - logger.LogWarning(ex, "Create Consumer Group If Not Exists: failed. {info}", CurrentInfo()); - } - else - { - await Task.Delay(KEY_NOT_EXISTS_DELAY); - logger.LogDebug(ex, "Create Consumer Group If Not Exists: failed. {info}", CurrentInfo()); - } - } - catch (RedisConnectionException ex) - { - logger.LogWarning(ex.FormatLazy(), "Create Consumer Group If Not Exists: connection failure. {info}", CurrentInfo()); - } - catch (RedisTimeoutException ex) - { - logger.LogWarning(ex.FormatLazy(), "Create Consumer Group If Not Exists: timeout failure. {info}", CurrentInfo()); - } - catch (Exception ex) - { - logger.LogWarning(ex.FormatLazy(), "Create Consumer Group If Not Exists: unexpected failure. {info}", CurrentInfo()); - } - - #endregion // Exception Handling - if (!exists) - { - try - { - using var lk = await _lock.AcquireAsync(); - await db.StreamCreateConsumerGroupAsync(eventSourceKey, - consumerGroup, - StreamPosition.Beginning, - flags: CommandFlags.DemandMaster); - } - #region Exception Handling - - catch (RedisServerException ex) - { - logger.LogWarning(ex.FormatLazy(), $"{nameof(CreateConsumerGroupIfNotExistsAsync)}.StreamCreateConsumerGroupAsync: failed & still waiting {CurrentInfo()}"); - } - catch (RedisConnectionException ex) - { - logger.LogWarning(ex.FormatLazy(), $"{nameof(CreateConsumerGroupIfNotExistsAsync)}.StreamCreateConsumerGroupAsync: connection failure {CurrentInfo()}"); - } - catch (Exception ex) - { - logger.LogWarning(ex.FormatLazy(), $"{nameof(CreateConsumerGroupIfNotExistsAsync)}.StreamCreateConsumerGroupAsync: unexpected failure {CurrentInfo()}"); - } - - #endregion // Exception Handling - } - - #region string CurrentInfo() - - string CurrentInfo() => @$" -Try number: {tryNumber} -Stream key: {eventSourceKey} -Consumer Group: {consumerGroup} -Is Connected: {db.Multiplexer.IsConnected} -Configuration: {db.Multiplexer.Configuration} -"; - - #endregion // string CurrentInfo() - } - } - - #endregion // CreateConsumerGroupIfNotExistsAsync - } -} diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/RedisDiExtensions.cs b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/RedisDiExtensions.cs deleted file mode 100644 index f47b8b94..00000000 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/RedisDiExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -using Weknow.EventSource.Backbone; - -namespace Microsoft.Extensions.Configuration -{ - /// - /// The redis DI extensions. - /// - public static class RedisDiExtensions - { - /// - /// Adds the event source redis connection to the DI. - /// - /// The services. - /// An IServiceCollection. - public static IServiceCollection AddEventSourceRedisConnection( - this IServiceCollection services) - { - services.AddSingleton(); - return services; - } - } -} diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/RedisTelemetryrExtensions.cs b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/RedisTelemetryrExtensions.cs deleted file mode 100644 index 221ef41a..00000000 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/RedisTelemetryrExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Diagnostics; - -namespace Weknow.EventSource.Backbone -{ - public static class RedisTelemetryrExtensions - { - #region InjectTelemetryTags - - /// - /// Adds standard open-telemetry tags (for redis). - /// - /// The meta. - /// The activity. - public static void InjectTelemetryTags(this Metadata meta, Activity? activity) - { - // These tags are added demonstrating the semantic conventions of the OpenTelemetry messaging specification - // See: - // * https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/messaging.md#messaging-attributes - activity?.SetTag("messaging.system", "redis-stream"); - activity?.SetTag("messaging.destination_kind", "topic"); - - activity?.SetTag("messaging.destination", meta.Operation); - activity?.SetTag("messaging.message_id", meta.MessageId); - activity?.SetTag("messaging.redis.key", $"{meta.Partition}:{meta.Shard}"); - - meta.InjectMetaTelemetryTags(activity); - } - - #endregion // InjectTelemetryTags - } -} diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Weknow.EventSource.Backbone.Channels.RedisProvider.Common.csproj b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Weknow.EventSource.Backbone.Channels.RedisProvider.Common.csproj deleted file mode 100644 index 571df549..00000000 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Weknow.EventSource.Backbone.Channels.RedisProvider.Common.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Weknow.EventSource.Backbone.Channels.RedisProvider.Common.xml b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Weknow.EventSource.Backbone.Channels.RedisProvider.Common.xml deleted file mode 100644 index f50410df..00000000 --- a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/Weknow.EventSource.Backbone.Channels.RedisProvider.Common.xml +++ /dev/null @@ -1,116 +0,0 @@ - - - - Weknow.EventSource.Backbone.Channels.RedisProvider.Common - - - - - REDIS client factory - - - - - Blocking Create REDIS client. - Exist only for code which don't support async (like ASP.NET setup (AddSingleton)) - - The configuration. - The endpoint key. - The password key. - - Fail to establish REDIS connection - Fail to establish REDIS connection - - - - Blocking Create REDIS client. - Exist only for code which don't support async (like ASP.NET setup (AddSingleton)) - - The logger. - The configuration. - The endpoint key. - The password key. - - Fail to establish REDIS connection - Fail to establish REDIS connection - - - - Create REDIS client. - - The configuration. - The endpoint key. - The password key. - - Fail to establish REDIS connection - Fail to establish REDIS connection - - - - Create REDIS client. - - The logger. - The configuration. - The endpoint key. - The password key. - - Fail to establish REDIS connection - Fail to establish REDIS connection - - - - Create REDIS client. - - The logger. - The number of retries. - The configuration. - The endpoint key. - The password key. - - - - Fail to establish REDIS connection - Fail to establish REDIS connection - - - - Create REDIS database client. - - The configuration. - The endpoint key. - The password key. - - Fail to establish REDIS connection - Fail to establish REDIS connection - - - - Create REDIS database client. - - The logger. - The configuration. - The endpoint key. - The password key. - - Fail to establish REDIS connection - Fail to establish REDIS connection - - - - Creates the consumer group if not exists asynchronous. - - The database. - The event source key. - The consumer group. - The logger. - - - - - Adds standard open-telemetry tags (for redis). - - The meta. - The activity. - - - diff --git a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/icon.png b/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/icon.png deleted file mode 100644 index d6811ad8..00000000 Binary files a/Channels/REDIS/Weknow.EventSource.Backbone.Channels.RedisProvider.Common/icon.png and /dev/null differ diff --git a/Channels/S3/EventSourcing.Backbone.Channels.S3StoreConsumerProvider/EventSourcing.Backbone.Channels.S3StoreConsumerProvider.csproj b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreConsumerProvider/EventSourcing.Backbone.Channels.S3StoreConsumerProvider.csproj new file mode 100644 index 00000000..867c07d3 --- /dev/null +++ b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreConsumerProvider/EventSourcing.Backbone.Channels.S3StoreConsumerProvider.csproj @@ -0,0 +1,25 @@ + + + + README.md + + + + + True + \ + + + + + + + + + + + + + + + diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider/S3ConsumerStorageStrategy.cs b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreConsumerProvider/S3ConsumerStorageStrategy.cs similarity index 91% rename from Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider/S3ConsumerStorageStrategy.cs rename to Channels/S3/EventSourcing.Backbone.Channels.S3StoreConsumerProvider/S3ConsumerStorageStrategy.cs index f5c16e98..fda173c7 100644 --- a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider/S3ConsumerStorageStrategy.cs +++ b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreConsumerProvider/S3ConsumerStorageStrategy.cs @@ -1,14 +1,13 @@ -using System.Collections.Immutable; -using System.Text.Json; +using System.Text.Json; -using Microsoft.Extensions.Logging; +using EventSourcing.Backbone.Channels; -using Weknow.EventSource.Backbone.Channels; +using Microsoft.Extensions.Logging; -using static Weknow.EventSource.Backbone.EventSourceConstants; +using static EventSourcing.Backbone.EventSourceConstants; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Responsible to load information from S3 storage. @@ -52,6 +51,11 @@ public S3ConsumerStorageStrategy( #endregion // ctor + /// + /// Gets the name of the storage provider. + /// + public string Name { get; } = "S3"; + /// /// Load the bucket information. /// @@ -70,8 +74,6 @@ async ValueTask IConsumerStorageStrategy.LoadBucketAsync( Func getProperty, CancellationToken cancellation) { - string id = meta.MessageId; - var lookup = ImmutableDictionary.CreateRange(prevBucket); string json = getProperty($"{Constants.PROVIDER_ID}~{type}"); var keyPathPairs = JsonSerializer.Deserialize[]>( json, SerializerOptionsWithIndent) ?? diff --git a/Channels/S3/EventSourcing.Backbone.Channels.S3StoreConsumerProvider/S3ConsumerStorageStrategyExtension.cs b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreConsumerProvider/S3ConsumerStorageStrategyExtension.cs new file mode 100644 index 00000000..0ba31042 --- /dev/null +++ b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreConsumerProvider/S3ConsumerStorageStrategyExtension.cs @@ -0,0 +1,106 @@ +using Amazon.S3; + +using EventSourcing.Backbone.Building; +using EventSourcing.Backbone.Channels; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + + +namespace EventSourcing.Backbone +{ + /// + /// Extension methods for S3 storage strategy. + /// + public static class S3ConsumerStorageStrategyExtension + { + /// + /// Adds the S3 storage strategy. + /// + /// The builder. + /// The options. + /// Type of the target. + /// Either the access key or environment variable hold it (depend on the fromEnvironment parameters). + /// Either the secret or environment variable hold it (depend on the fromEnvironment parameters) + /// Either the region or environment variable hold it (depend on the fromEnvironment parameters) + /// if set to truelooks for the access key, secret and region in the environment variables. + /// + public static IConsumerStoreStrategyBuilder AddS3Storage( + this IConsumerStoreStrategyBuilder builder, + S3Options options = default, + EventBucketCategories targetType = EventBucketCategories.All, + string envAccessKey = "S3_EVENT_SOURCE_ACCESS_KEY", + string envSecretKey = "S3_EVENT_SOURCE_SECRET", + string envRegion = "S3_EVENT_SOURCE_REGION", + bool fromEnvironment = true) + { + var result = builder.AddStorageStrategyFactory(Local, targetType); + + ValueTask Local(ILogger logger) + { + var factory = S3RepositoryFactory.Create(logger, envAccessKey, envSecretKey, envRegion, fromEnvironment); + var repo = factory.Get(options); + var strategy = new S3ConsumerStorageStrategy(repo); + return strategy.ToValueTask(); + } + return result; + } + + /// + /// Adds the S3 storage strategy. + /// + /// The builder. + /// + /// S3 client. + /// Learn how to setup an AWS client: https://codewithmukesh.com/blog/aws-credentials-for-dotnet-applications/ + /// + /// The options. + /// Type of the target. + /// + public static IConsumerStoreStrategyBuilder AddS3Storage( + this IConsumerStoreStrategyBuilder builder, + IAmazonS3 client, + S3Options options = default, + EventBucketCategories targetType = EventBucketCategories.All) + { + var result = builder.AddStorageStrategyFactory(Local, targetType); + + ValueTask Local(ILogger logger) + { + var factory = S3RepositoryFactory.Create(logger, client); + var repo = factory.Get(options); + var strategy = new S3ConsumerStorageStrategy(repo); + return strategy.ToValueTask(); + } + return result; + } + + /// + /// Adds the S3 storage strategy. + /// Will resolve IAmazonS3 + /// See the following article for more details on how can you register Amazon credentials: + /// https://codewithmukesh.com/blog/aws-credentials-for-dotnet-applications/ + /// + /// The builder. + /// The options. + /// Type of the target. + /// + public static IConsumerStoreStrategyBuilder ResolveS3Storage( + this IConsumerIocStoreStrategyBuilder builder, + S3Options options = default, + EventBucketCategories targetType = EventBucketCategories.All) + { + ILogger? logger = builder.ServiceProvider.GetService>(); + IAmazonS3? s3Client = builder.ServiceProvider.GetService(); + if (s3Client != null) + { + var injectionResult = builder.AddS3Storage(s3Client, options, targetType); + logger?.LogInformation("Consumer, Resolving AWS S3 via IAmazonS3 injection (might be via profile)"); + return injectionResult; + } + logger?.LogInformation("Consumer, Resolving AWS S3 via environment variable"); + var envVarResult = builder.AddS3Storage(options, targetType); + return envVarResult; + } + } +} diff --git a/Channels/S3/EventSourcing.Backbone.Channels.S3StoreConsumerProvider/icon.png b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreConsumerProvider/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreConsumerProvider/icon.png differ diff --git a/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProducerProvider/EventSourcing.Backbone.Channels.S3StoreProducerProvider.csproj b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProducerProvider/EventSourcing.Backbone.Channels.S3StoreProducerProvider.csproj new file mode 100644 index 00000000..a28a0266 --- /dev/null +++ b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProducerProvider/EventSourcing.Backbone.Channels.S3StoreProducerProvider.csproj @@ -0,0 +1,24 @@ + + + + README.md + + + + + True + \ + + + + + + + + + + + + + + diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider/S3ProducerStorageStrategy.cs b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProducerProvider/S3ProducerStorageStrategy.cs similarity index 90% rename from Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider/S3ProducerStorageStrategy.cs rename to Channels/S3/EventSourcing.Backbone.Channels.S3StoreProducerProvider/S3ProducerStorageStrategy.cs index 58dd3df1..c2fc1530 100644 --- a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider/S3ProducerStorageStrategy.cs +++ b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProducerProvider/S3ProducerStorageStrategy.cs @@ -1,14 +1,14 @@ using System.Collections.Immutable; using System.Text.Json; -using Microsoft.Extensions.Logging; +using EventSourcing.Backbone.Channels; -using Weknow.EventSource.Backbone.Channels; +using Microsoft.Extensions.Logging; -using static Weknow.EventSource.Backbone.EventSourceConstants; +using static EventSourcing.Backbone.EventSourceConstants; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Responsible to save information to S3 storage. @@ -53,6 +53,11 @@ public S3ProducerStorageStrategy( #endregion // ctor + /// + /// Gets the name of the storage provider. + /// + public string Name { get; } = "S3"; + /// /// Saves the bucket information. /// @@ -73,7 +78,7 @@ async ValueTask> IProducerStorageStrategy.S { var date = DateTime.UtcNow; int index = Interlocked.Increment(ref _index); - string basePath = $"{meta.Partition}/{meta.Shard}/{date:yyyy-MM-dd/HH:mm}/{meta.Operation}/{id}/{index}/{type}"; + string basePath = $"{meta.Uri}/{date:yyyy-MM-dd/HH:mm}/{meta.Operation}/{id}/{index}/{type}"; var tasks = bucket.Select(SaveAsync); var propKeyToS3Key = await Task.WhenAll(tasks); string json = JsonSerializer.Serialize(propKeyToS3Key, SerializerOptionsWithIndent); diff --git a/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProducerProvider/S3ProducerStorageStrategyExtension.cs b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProducerProvider/S3ProducerStorageStrategyExtension.cs new file mode 100644 index 00000000..c0e4be9b --- /dev/null +++ b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProducerProvider/S3ProducerStorageStrategyExtension.cs @@ -0,0 +1,110 @@ +using Amazon.S3; + +using EventSourcing.Backbone.Channels; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace EventSourcing.Backbone +{ + /// + /// Extension methods for S3 storage strategy. + /// + public static class S3ProducerStorageStrategyExtension + { + /// + /// Adds the S3 storage strategy. + /// + /// The builder. + /// The options. + /// Type of the target. + /// The filter of which keys in the bucket will be store into this storage. + /// Either the access key or environment variable hold it (depend on the fromEnvironment parameters). + /// Either the secret or environment variable hold it (depend on the fromEnvironment parameters) + /// Either the region or environment variable hold it (depend on the fromEnvironment parameters) + /// if set to truelooks for the access key, secret and region in the environment variables. + /// + public static IProducerStoreStrategyBuilder AddS3Storage( + this IProducerStoreStrategyBuilder builder, + S3Options options = default, + EventBucketCategories targetType = EventBucketCategories.All, + Predicate? filter = null, + string envAccessKey = "S3_EVENT_SOURCE_ACCESS_KEY", + string envSecretKey = "S3_EVENT_SOURCE_SECRET", + string envRegion = "S3_EVENT_SOURCE_REGION", + bool fromEnvironment = true) + { + var result = builder.AddStorageStrategy(Local, targetType, filter); + + ValueTask Local(ILogger logger) + { + var factory = S3RepositoryFactory.Create(logger, envAccessKey, envSecretKey, envRegion, fromEnvironment); + var repo = factory.Get(options); + var strategy = new S3ProducerStorageStrategy(repo); + return strategy.ToValueTask(); + } + + return result; + } + + /// + /// Adds the S3 storage strategy. + /// + /// The builder. + /// + /// S3 client. + /// Learn how to setup an AWS client: https://codewithmukesh.com/blog/aws-credentials-for-dotnet-applications/ + /// + /// The options. + /// Type of the target. + /// The filter of which keys in the bucket will be store into this storage. + /// + public static IProducerStoreStrategyBuilder AddS3Storage( + this IProducerStoreStrategyBuilder builder, + IAmazonS3 client, + S3Options options = default, + EventBucketCategories targetType = EventBucketCategories.All, + Predicate? filter = null) + { + var result = builder.AddStorageStrategy(Local, targetType, filter); + + ValueTask Local(ILogger logger) + { + var factory = S3RepositoryFactory.Create(logger, client); + var repo = factory.Get(options); + var strategy = new S3ProducerStorageStrategy(repo); + return strategy.ToValueTask(); + } + + return result; + } + + /// + /// Adds the S3 storage strategy. + /// + /// The builder. + /// The options. + /// Type of the target. + /// The filter of which keys in the bucket will be store into this storage. + /// + public static IProducerStoreStrategyBuilder ResolveS3Storage( + this IProducerIocStoreStrategyBuilder builder, + S3Options options = default, + EventBucketCategories targetType = EventBucketCategories.All, + Predicate? filter = null) + { + ILogger? logger = builder.ServiceProvider.GetService>(); + IAmazonS3? s3Client = builder.ServiceProvider.GetService(); + + if (s3Client != null) + { + IProducerStoreStrategyBuilder injectionResult = builder.AddS3Storage(s3Client, options, targetType, filter); + logger?.LogInformation("Producer, Resolving AWS S3 via IAmazonS3 injection (might be via profile)"); + return injectionResult; + } + logger?.LogInformation("Producer, Resolving AWS S3 via environment variable"); + IProducerStoreStrategyBuilder envVarResult = builder.AddS3Storage(options, targetType, filter); + return envVarResult; + } + } +} diff --git a/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProducerProvider/icon.png b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProducerProvider/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProducerProvider/icon.png differ diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/BlobResponse.cs b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/BlobResponse.cs similarity index 81% rename from Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/BlobResponse.cs rename to Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/BlobResponse.cs index 4e893828..31b1a321 100644 --- a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/BlobResponse.cs +++ b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/BlobResponse.cs @@ -1,35 +1,35 @@ -namespace Weknow.EventSource.Backbone.Channels +namespace EventSourcing.Backbone.Channels { /// /// Response structure /// - public class BlobResponse : IEquatable + public sealed class BlobResponse : IEquatable { #region Ctor /// /// Prevents a default instance of the class from being created. /// +#pragma warning disable S1133 // Deprecated code should be removed [Obsolete("Use other constructors (this one exists to enable de-serialization)", true)] +#pragma warning restore S1133 // Deprecated code should be removed private BlobResponse() { } /// /// Create request instance. /// /// The blob key. - /// The partition. /// The e tag. /// The content version. /// Name of the file. + /// public BlobResponse( string key, - string partition, string eTag, string contentVersion, string? fileName = null) { _key = key; - _partition = partition; _eTag = eTag; _contentVersion = contentVersion; _fileName = fileName; @@ -46,27 +46,14 @@ public BlobResponse( public string Key { get => _key; +#pragma warning disable S1133 // Deprecated code should be removed [Obsolete("Exposed for the serializer", true)] +#pragma warning restore S1133 // Deprecated code should be removed set => _key = value; } #endregion Key - #region Partition - - private string _partition = string.Empty; - /// - /// Gets or sets the partition. - /// - public string Partition - { - get => _partition; - [Obsolete("Exposed for the serializer", true)] - set => _partition = value; - } - - #endregion Partition - #region FileName private string? _fileName = string.Empty; @@ -76,7 +63,9 @@ public string Partition public string? FileName { get => _fileName; +#pragma warning disable S1133 // Deprecated code should be removed [Obsolete("Exposed for the serializer", true)] +#pragma warning restore S1133 // Deprecated code should be removed set => _fileName = value; } @@ -91,7 +80,9 @@ public string? FileName public string ETag { get => _eTag; +#pragma warning disable S1133 // Deprecated code should be removed [Obsolete("Exposed for the serializer", true)] +#pragma warning restore S1133 // Deprecated code should be removed set => _eTag = value; } @@ -106,7 +97,9 @@ public string ETag public string ContentVersion { get => _contentVersion; +#pragma warning disable S1133 // Deprecated code should be removed [Obsolete("Exposed for the serializer", true)] +#pragma warning restore S1133 // Deprecated code should be removed set => _contentVersion = value; } @@ -138,7 +131,6 @@ public bool Equals(BlobResponse? other) return other != null && _key == other._key && _fileName == other._fileName && - _partition == other._partition && _eTag == other._eTag && _contentVersion == other._contentVersion; } @@ -149,10 +141,17 @@ public bool Equals(BlobResponse? other) /// /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. /// +#pragma warning disable S2328 // "GetHashCode" should not reference mutable fields public override int GetHashCode() { - return HashCode.Combine(_key, _fileName, _partition, _eTag, _contentVersion); + return HashCode.Combine( + _key, + _fileName, + _eTag, + _contentVersion); } +#pragma warning restore S2328 // "GetHashCode" should not reference mutable fields + /// /// Implements the operator ==. diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/Constants.cs b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/Constants.cs similarity index 75% rename from Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/Constants.cs rename to Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/Constants.cs index 2e101a17..a6f4ac4c 100644 --- a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/Constants.cs +++ b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/Constants.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.Channels +namespace EventSourcing.Backbone.Channels { /// /// Constants diff --git a/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/EventSourcing.Backbone.Channels.S3StoreProvider.Common.csproj b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/EventSourcing.Backbone.Channels.S3StoreProvider.Common.csproj new file mode 100644 index 00000000..2ff1b791 --- /dev/null +++ b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/EventSourcing.Backbone.Channels.S3StoreProvider.Common.csproj @@ -0,0 +1,25 @@ + + + + README.md + + + + + True + \ + + + + + + + + + + + + + + + diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/IS3Repository.cs b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/IS3Repository.cs similarity index 98% rename from Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/IS3Repository.cs rename to Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/IS3Repository.cs index 1df734dc..2a527832 100644 --- a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/IS3Repository.cs +++ b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/IS3Repository.cs @@ -1,7 +1,7 @@ using System.Collections.Immutable; using System.Text.Json; -namespace Weknow.EventSource.Backbone.Channels +namespace EventSourcing.Backbone.Channels { /// /// The S3 repository contract. diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/IS3RepositoryFactory.cs b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/IS3RepositoryFactory.cs similarity index 66% rename from Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/IS3RepositoryFactory.cs rename to Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/IS3RepositoryFactory.cs index 8da82527..7260ccbb 100644 --- a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/IS3RepositoryFactory.cs +++ b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/IS3RepositoryFactory.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.Channels +namespace EventSourcing.Backbone.Channels { public interface IS3RepositoryFactory { @@ -7,6 +7,6 @@ public interface IS3RepositoryFactory /// /// /// - S3Repository Get(S3Options options = default); + IS3Repository Get(S3Options options = default); } } \ No newline at end of file diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/S3EnvironmentConvention.cs b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/S3EnvironmentConvention.cs similarity index 82% rename from Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/S3EnvironmentConvention.cs rename to Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/S3EnvironmentConvention.cs index 02fd4e83..a1fe9441 100644 --- a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/S3EnvironmentConvention.cs +++ b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/S3EnvironmentConvention.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Environment convention's options @@ -10,7 +10,7 @@ public enum S3EnvironmentConvention /// None, /// - /// Environment as bucket perfix + /// Environment as bucket prefix /// BucketPrefix, /// diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/S3Options.cs b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/S3Options.cs similarity index 89% rename from Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/S3Options.cs rename to Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/S3Options.cs index 47156c18..077f5e46 100644 --- a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/S3Options.cs +++ b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/S3Options.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// S3 provider options @@ -9,7 +9,7 @@ public S3Options() { Bucket = null; BasePath = null; - EnvironmentConvension = S3EnvironmentConvention.None; + EnvironmentConvension = S3EnvironmentConvention.BucketPrefix; } /// @@ -17,6 +17,7 @@ public S3Options() /// public string? Bucket { get; init; } + /// /// Base path /// diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/S3Repository.cs b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/S3Repository.cs similarity index 87% rename from Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/S3Repository.cs rename to Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/S3Repository.cs index 16012089..094fe00b 100644 --- a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/S3Repository.cs +++ b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/S3Repository.cs @@ -7,15 +7,15 @@ using Microsoft.Extensions.Logging; -using static Weknow.EventSource.Backbone.EventSourceConstants; +using static EventSourcing.Backbone.EventSourceConstants; -namespace Weknow.EventSource.Backbone.Channels +namespace EventSourcing.Backbone.Channels { /// /// Abstract S3 operations /// - public sealed class S3Repository : IS3Repository, IDisposable + internal sealed class S3Repository : IS3Repository, IDisposable { private static readonly string BUCKET = Environment.GetEnvironmentVariable("S3_EVENT_SOURCE_BUCKET") @@ -24,11 +24,10 @@ public sealed class S3Repository : IS3Repository, IDisposable private readonly string _bucket; private readonly ILogger _logger; private readonly string? _basePath; - private readonly AmazonS3Client _client; + private readonly IAmazonS3 _client; private static readonly List EMPTY_TAGS = new List(); private int _disposeCount = 0; private readonly S3EnvironmentConvention _environmentConvension; - private const StringComparison STRING_COMPARISON = StringComparison.OrdinalIgnoreCase; private readonly bool _dryRun; #region Ctor @@ -36,11 +35,14 @@ public sealed class S3Repository : IS3Repository, IDisposable /// /// Initializes a new instance. /// - /// S3 client. + /// + /// S3 client. + /// Learn how to setup an AWS client: https://codewithmukesh.com/blog/aws-credentials-for-dotnet-applications/ + /// /// The logger. /// The s3 options. public S3Repository( - AmazonS3Client client, + IAmazonS3 client, ILogger logger, S3Options options = default) { @@ -154,7 +156,7 @@ public async ValueTask GetAsync(Env env, string id, CancellationToken canc if (response == null) { - throw new NullReferenceException("Failed to deserialize industries"); + throw new EventSourcingException("Failed to deserialize industries"); } #endregion // Validation @@ -205,12 +207,12 @@ public async ValueTask GetStreamAsync(Env env, string id, CancellationTo if (res == null) { - throw new Exception($"S3 key [{key}] not found. bucket = {bucketName}"); + throw new EventSourcingException($"S3 key [{key}] not found. bucket = {bucketName}"); } if (res.HttpStatusCode >= HttpStatusCode.Ambiguous) { - throw new Exception($"Failed to get blob [{res.HttpStatusCode}]"); + throw new EventSourcingException($"Failed to get blob [{res.HttpStatusCode}]"); } #endregion // Validation @@ -314,8 +316,11 @@ public async ValueTask SaveAsync( string key = GetKey(env, id); try { - var date = DateTime.UtcNow; - //tags = tags.Add("month", date.ToString("yyyy-MM")); + +#pragma warning disable S125 // Sections of code should not be commented out + // var date = DateTime.UtcNow; + // tags = tags.Add("month", date.ToString("yyyy-MM")); +#pragma warning restore S125 // Sections of code should not be commented out var s3Request = new PutObjectRequest { @@ -325,6 +330,7 @@ public async ValueTask SaveAsync( ContentType = mediaType, TagSet = tags?.Select(m => new Tag { Key = m.Key, Value = m.Value })?.ToList() ?? EMPTY_TAGS, }; + // s3Request.Headers.ExpiresUtc = DateTime.Now.AddHours(2); // cache expiration if (metadata != null) @@ -339,7 +345,7 @@ public async ValueTask SaveAsync( if (_dryRun) { - return new BlobResponse(key, _bucket, string.Empty, string.Empty); + return new BlobResponse(key, string.Empty, string.Empty); } #endregion // if (_dryRun) return ... @@ -350,16 +356,18 @@ public async ValueTask SaveAsync( if (res.HttpStatusCode >= HttpStatusCode.Ambiguous) { - throw new Exception($"Failed to save blob [{res.HttpStatusCode}]"); + throw new EventSourcingException($"Failed to save blob [{res.HttpStatusCode}]"); } #endregion // Validation - BlobResponse response = new BlobResponse(key, _bucket, res.ETag, res.VersionId); + BlobResponse response = new BlobResponse(key, res.ETag, res.VersionId); return response; } #region Exception Handling +#pragma warning disable S2486 // Generic exceptions should not be ignored +#pragma warning disable S108 // Nested blocks of code should not be left empty catch (AmazonS3Exception e) { string json = ""; @@ -369,9 +377,15 @@ public async ValueTask SaveAsync( } catch { } _logger.LogError(e.FormatLazy(), - "AWS-S3 Failed to write: {payload}, {env}, {id}, {bucket}, {key}", json, env, id, bucket, key); - string msg = $"AWS-S3 Failed to write: {env}, {id}, {bucket}, {key}"; - throw new ApplicationException(msg, e); + """ + AWS-S3 Failed to write: {payload}, {env}, {id}, {bucket}, {key} + Make sure to that the bucket exists & credentials sets right. + """, json, env, id, bucket, key); + string msg = $""" + AWS-S3 Failed to write: {env}, {id}, {bucket}, {key} + Make sure to that the bucket exists & credentials sets right. + """; + throw new EventSourcingException(msg, e); } catch (Exception e) { @@ -384,8 +398,10 @@ public async ValueTask SaveAsync( _logger.LogError(e.FormatLazy(), "S3 writing Failed: {payload}, {env}, {id}, {bucket}, {key}", json, env, id, bucket, key); string msg = $"S3 writing Failed: {env}, {id}, {bucket}, {key}"; - throw new ApplicationException(msg, e); + throw new EventSourcingException(msg, e); } +#pragma warning restore S2486 +#pragma warning restore S108 #endregion // Exception Handling } @@ -401,6 +417,8 @@ public async ValueTask SaveAsync( /// private string GetBucket(Env env) { + if (string.IsNullOrEmpty(env)) + return _bucket; var bucket = _environmentConvension switch { S3EnvironmentConvention.BucketPrefix => $"{env.Format()}.{_bucket}", diff --git a/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/S3RepositoryFactory.cs b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/S3RepositoryFactory.cs new file mode 100644 index 00000000..6b6bd9ab --- /dev/null +++ b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/S3RepositoryFactory.cs @@ -0,0 +1,147 @@ +using System.Collections.Concurrent; + +using Amazon; +using Amazon.S3; + +using Microsoft.Extensions.Logging; + + +namespace EventSourcing.Backbone.Channels +{ + /// + /// Abstract S3 operations + /// + public sealed class S3RepositoryFactory : IS3RepositoryFactory + { + private readonly ILogger _logger; + private readonly IAmazonS3 _client; + private readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + + #region CreateClient + + /// + /// Creates the S3 client. + /// + /// The access key or environment variable which hold it. + /// The secret or environment variable which hold it . + /// The region environment variable which hold it. + /// if set to true [will try to find the value from environment variable. + /// + public static IAmazonS3 CreateClient( + string accessKey = "S3_EVENT_SOURCE_ACCESS_KEY", + string secret = "S3_EVENT_SOURCE_SECRET", + string region = "S3_EVENT_SOURCE_REGION", + bool fromEnvironment = true) + { + accessKey = + fromEnvironment + ? Environment.GetEnvironmentVariable(accessKey) ?? accessKey + : accessKey; + secret = + fromEnvironment + ? Environment.GetEnvironmentVariable(secret) ?? secret + : secret; + string? regionKey = + fromEnvironment + ? Environment.GetEnvironmentVariable(region) ?? region + : region; + + RegionEndpoint rgnKey = (!string.IsNullOrEmpty(regionKey)) + ? RegionEndpoint.GetBySystemName(regionKey) + : RegionEndpoint.USEast2; + var client = new AmazonS3Client(accessKey, secret, rgnKey); + return client; + } + + #endregion // CreateClient + + #region Create + + /// + /// Creates the specified logger. + /// + /// The logger. + /// The access key or environment variable which hold it. + /// The secret or environment variable which hold it . + /// The region environment variable which hold it. + /// if set to true [will try to find the value from environment variable. + /// + public static IS3RepositoryFactory Create(ILogger logger, + string accessKey = "S3_EVENT_SOURCE_ACCESS_KEY", + string secret = "S3_EVENT_SOURCE_SECRET", + string region = "S3_EVENT_SOURCE_REGION", + bool fromEnvironment = true) + { + var client = CreateClient(accessKey, secret, region, fromEnvironment); + return new S3RepositoryFactory(logger, client); + } + + /// + /// Creates the specified logger. + /// + /// The logger. + /// + /// S3 client. + /// Learn how to setup an AWS client: https://codewithmukesh.com/blog/aws-credentials-for-dotnet-applications/ + /// + /// + public static IS3RepositoryFactory Create(ILogger logger, + IAmazonS3 client) + { + return new S3RepositoryFactory(logger, client); + } + + #endregion // Create + + #region Ctor + + /// + /// Initializes a new instance. + /// + /// The logger. + /// The client. + public S3RepositoryFactory( + ILogger logger, + IAmazonS3 client) + { + _logger = logger; + + _client = client; + } + + #endregion // Ctor + + #region Get + + /// + /// Get repository instance. + /// + /// The options. + /// + IS3Repository IS3RepositoryFactory.Get(S3Options options) + { + + var repo = _cache.GetOrAdd(options, CreateInternal); + repo.AddReference(); + return repo; + } + + #endregion // Get + + #region CreateInternal + + /// + /// Creates repository. + /// + /// The options. + /// + private S3Repository CreateInternal( + S3Options options = default) + { + return new S3Repository(_client, _logger, options); + } + + #endregion // CreateInternal + } + +} diff --git a/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/icon.png b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/Channels/S3/EventSourcing.Backbone.Channels.S3StoreProvider.Common/icon.png differ diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider/S3ConsumerStorageStrategyExtension.cs b/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider/S3ConsumerStorageStrategyExtension.cs deleted file mode 100644 index 02e8fc18..00000000 --- a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider/S3ConsumerStorageStrategyExtension.cs +++ /dev/null @@ -1,41 +0,0 @@ - -using Microsoft.Extensions.Logging; - -using Weknow.EventSource.Backbone.Building; -using Weknow.EventSource.Backbone.Channels; - - -namespace Weknow.EventSource.Backbone -{ - /// - /// Extension methods for S3 storage strategy. - /// - public static class S3ConsumerStorageStrategyExtension - { - /// - /// Adds the S3 storage strategy. - /// - /// The builder. - /// The options. - /// Type of the target. - /// - public static IConsumerStoreStrategyBuilder AddS3Strategy( - this IConsumerStoreStrategyBuilder builder, - S3Options options = default, - EventBucketCategories targetType = EventBucketCategories.All) - { - var result = builder.AddStorageStrategyFactory(Local, targetType); - - ValueTask Local(ILogger logger) - { - var factory = S3RepositoryFactory.Create(logger); - var repo = factory.Get(options); - var strategy = new S3ConsumerStorageStrategy(repo); - return strategy.ToValueTask(); - } - return result; - } - - - } -} diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider.csproj b/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider.csproj deleted file mode 100644 index a807d788..00000000 --- a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProviderr.xml b/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProviderr.xml deleted file mode 100644 index f28cfed5..00000000 --- a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProviderr.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider - - - - - Responsible to load information from S3 storage. - The information can be either Segmentation or Interception. - When adding it via the builder it can be arrange in a chain in order of having - 'Chain of Responsibility' for saving different parts into different storage (For example GDPR's PII). - Alternative, chain can serve as a cache layer. - - - - - Initializes a new instance. - - S3 repository. - Use S3Factory in order to create it (will create one if missing). - S3Factory will read credentials from the following environment variables: "S3_ACCESS_KEY", "S3_SECRET", "S3_REGION". - - - - Initializes a new instance. - - The logger. - The bucket. - The base path. - The repository's factory. - - - - Load the bucket information. - - The meta fetch provider. - The current bucket (previous item in the chain). - The type of the storage. - The get property. - The cancellation. - - Either Segments or Interceptions. - - - - - Extension methods for S3 storage strategy. - - - - - Adds the S3 storage strategy. - - The builder. - The bucket. - The base path. - Type of the target. - - - - diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider/icon.png b/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider/icon.png deleted file mode 100644 index d6811ad8..00000000 Binary files a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider/icon.png and /dev/null differ diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider/S3ProducerStorageStrategyExtension.cs b/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider/S3ProducerStorageStrategyExtension.cs deleted file mode 100644 index 25c77337..00000000 --- a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider/S3ProducerStorageStrategyExtension.cs +++ /dev/null @@ -1,49 +0,0 @@ - -using Microsoft.Extensions.Logging; - -using Weknow.EventSource.Backbone.Channels; - - -namespace Weknow.EventSource.Backbone -{ - /// - /// Extension methods for S3 storage strategy. - /// - public static class S3ProducerStorageStrategyExtension - { - /// - /// Adds the S3 storage strategy. - /// - /// The builder. - /// The options. - /// Type of the target. - /// The filter of which keys in the bucket will be store into this storage. - /// The environment variable of access key. - /// The environment variable of secret key. - /// The environment variable of region. - /// - public static IProducerStoreStrategyBuilder AddS3Strategy( - this IProducerStoreStrategyBuilder builder, - S3Options options = default, - EventBucketCategories targetType = EventBucketCategories.All, - Predicate? filter = null, - string envAccessKey = "S3_EVENT_SOURCE_ACCESS_KEY", - string envSecretKey = "S3_EVENT_SOURCE_SECRET", - string envRegion = "S3_EVENT_SOURCE_REGION") - { - var result = builder.AddStorageStrategy(Local, targetType, filter); - - ValueTask Local(ILogger logger) - { - var factory = S3RepositoryFactory.Create(logger, envAccessKey, envSecretKey, envRegion); - var repo = factory.Get(options); - var strategy = new S3ProducerStorageStrategy(repo); - return strategy.ToValueTask(); - } - - return result; - } - - - } -} diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider.csproj b/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider.csproj deleted file mode 100644 index 6ea2c899..00000000 --- a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider.xml b/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider.xml deleted file mode 100644 index cb908e12..00000000 --- a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - - Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider - - - - - Responsible to save information to S3 storage. - The information can be either Segmentation or Interception. - When adding it via the builder it can be arrange in a chain in order of having - 'Chain of Responsibility' for saving different parts into different storage (For example GDPR's PII). - Alternative, chain can serve as a cache layer. - - - - - Initializes a new instance. - - S3 repository. - Use S3Factory in order to create it (will create one if missing). - S3Factory will read credentials from the following environment variables: "S3_ACCESS_KEY", "S3_SECRET", "S3_REGION". - - - - Initializes a new instance. - - The logger. - The bucket. - The base path. - The repository's factory. - - - - Saves the bucket information. - - The identifier. - Either Segments or Interceptions. - The type. - The meta. - The cancellation. - - Array of metadata entries which can be used by the consumer side storage strategy, in order to fetch the data. - - - - - Extension methods for S3 storage strategy. - - - - - Adds the S3 storage strategy. - - The builder. - The bucket. - The base path. - Type of the target. - The filter of which keys in the bucket will be store into this storage. - - - - diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider/icon.png b/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider/icon.png deleted file mode 100644 index d6811ad8..00000000 Binary files a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider/icon.png and /dev/null differ diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/S3RepositoryFactory.cs b/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/S3RepositoryFactory.cs deleted file mode 100644 index 6ce7c72d..00000000 --- a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/S3RepositoryFactory.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.Collections.Concurrent; - -using Amazon; -using Amazon.S3; - -using Microsoft.Extensions.Logging; - - -namespace Weknow.EventSource.Backbone.Channels -{ - /// - /// Abstract S3 operations - /// - public sealed class S3RepositoryFactory : IS3RepositoryFactory - { - private readonly ILogger _logger; - private readonly AmazonS3Client _client; - private readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); - - #region Create - - /// - /// Creates the specified logger. - /// - /// The logger. - /// The environment variable of access key. - /// The environment variable of secret key. - /// The environment variable of region. - /// - public static IS3RepositoryFactory Create(ILogger logger, - string envAccessKey = "S3_EVENT_SOURCE_ACCESS_KEY", - string envSecretKey = "S3_EVENT_SOURCE_SECRET", - string envRegion = "S3_EVENT_SOURCE_REGION") => new S3RepositoryFactory(logger, envAccessKey, envSecretKey, envRegion); - - #endregion // Create - - #region Ctor - - /// - /// Initializes a new instance. - /// - /// The logger. - public S3RepositoryFactory( - ILogger logger) : this((ILogger)logger) - { - } - - /// - /// Initializes a new instance. - /// - /// The logger. - /// The environment variable of access key. - /// The environment variable of secret key. - /// The environment variable of region. - public S3RepositoryFactory( - ILogger logger, - string envAccessKey = "S3_EVENT_SOURCE_ACCESS_KEY", - string envSecretKey = "S3_EVENT_SOURCE_SECRET", - string envRegion = "S3_EVENT_SOURCE_REGION") - { - _logger = logger; - - string accessKey = - Environment.GetEnvironmentVariable(envAccessKey) ?? ""; - string secretKey = - Environment.GetEnvironmentVariable(envSecretKey) ?? ""; - string? regionKey = - Environment.GetEnvironmentVariable(envRegion); - RegionEndpoint rgnKey = (!string.IsNullOrEmpty(regionKey)) - ? RegionEndpoint.GetBySystemName(regionKey) - : RegionEndpoint.USEast2; - - _client = new AmazonS3Client( - accessKey, - secretKey, - rgnKey); - } - - #endregion // Ctor - - #region Get - - /// - /// Get repository instance. - /// - /// The options. - /// - S3Repository IS3RepositoryFactory.Get(S3Options options) - { - - var repo = _cache.GetOrAdd(options, CreateInternal); - repo.AddReference(); - return repo; - } - - #endregion // Get - - #region CreateInternal - - /// - /// Creates repository. - /// - /// The options. - /// - private S3Repository CreateInternal( - S3Options options = default) - { - return new S3Repository(_client, _logger, options); - } - - #endregion // CreateInternal - } - -} diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common.csproj b/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common.csproj deleted file mode 100644 index b0c1e45d..00000000 --- a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common.xml b/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common.xml deleted file mode 100644 index 8584bf01..00000000 --- a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common.xml +++ /dev/null @@ -1,265 +0,0 @@ - - - - Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common - - - - - Response structure - - - - - Prevents a default instance of the class from being created. - - - - - Create request instance. - - The blob key. - The partition. - The e tag. - The content version. - Name of the file. - - - - Gets or sets the key. - - - - - Gets or sets the partition. - - - - - Gets or sets the file name. - - - - - Gets or sets the eTag. - - - - - Gets or sets the contentVersion. - - - - - Determines whether the specified object is equal to the current object. - - The object to compare with the current object. - - if the specified object is equal to the current object; otherwise, . - - - - - Indicates whether the current object is equal to another object of the same type. - - An object to compare with this object. - - if the current object is equal to the parameter; otherwise, . - - - - - Returns a hash code for this instance. - - - A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - - - - - Implements the operator ==. - - The left. - The right. - - The result of the operator. - - - - - Implements the operator !=. - - The left. - The right. - - The result of the operator. - - - - - Constants - - - - - Abstract S3 operations - - - - - Initializes a new instance. - - S3 client. - The logger. - The s3 bucket. - The base path within the bucket. - - - - Adds the reference to the repository. - This reference will prevent disposal until having no active references. - - - - - Get content. - - The identifier which is the S3 key. - The cancellation. - - - - - - Get content. - - The identifier which is the S3 key. - The cancellation. - - - - - - Get content. - - - The identifier which is the S3 key. - The cancellation. - - Failed to deserialize industries - - - - - Get content. - - The identifier which is the S3 key. - The cancellation. - - - - Failed to get blob [{res.HttpStatusCode}] - - - - Saves content. - - The data. - The identifier of the resource. - The metadata. - The tags. - The cancellation. - - Failed to save blob [{res.HttpStatusCode}] - - - - Saves content. - - The data. - The identifier of the resource. - The metadata. - The tags. - Type of the media. - The cancellation. - - Failed to save blob [{res.HttpStatusCode}] - - - - Saves content. - - The data. - The identifier of the resource. - The metadata. - The tags. - Type of the media. - The cancellation. - - Failed to save blob [{res.HttpStatusCode}] - Failed to save blob [{res.HttpStatusCode}] - - - - Gets s3 key. - - The identifier. - - - - - Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - - - - - Releases unmanaged and - optionally - managed resources. - - true to release both managed and unmanaged resources; false to release only unmanaged resources. - - - - Finalizes an instance of the class. - - - - - Abstract S3 operations - - - - - Creates the specified logger. - - The logger. - - - - - Initializes a new instance. - - The logger. - - - - Initializes a new instance. - - The logger. - - - - Get repository instance. - - The bucket. - The base path. - - - - - Creates repository. - - The props. - - - - diff --git a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/icon.png b/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/icon.png deleted file mode 100644 index d6811ad8..00000000 Binary files a/Channels/S3/Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common/icon.png and /dev/null differ diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers/Builder/ConsumerBase.EventSourceSubscriber.cs b/Consumers/EventSourcing.Backbone.Consumers/Builder/ConsumerBase.EventSourceSubscriber.cs similarity index 83% rename from Consumers/Weknow.EventSource.Backbone.Consumers/Builder/ConsumerBase.EventSourceSubscriber.cs rename to Consumers/EventSourcing.Backbone.Consumers/Builder/ConsumerBase.EventSourceSubscriber.cs index 46e17e97..6400de76 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers/Builder/ConsumerBase.EventSourceSubscriber.cs +++ b/Consumers/EventSourcing.Backbone.Consumers/Builder/ConsumerBase.EventSourceSubscriber.cs @@ -1,13 +1,14 @@ -using System.Collections.Concurrent; +using System.Collections; +using System.Collections.Concurrent; -using Microsoft.Extensions.Logging; +using EventSourcing.Backbone.Building; +using EventSourcing.Backbone.Enums; -using Weknow.EventSource.Backbone.Building; -using Weknow.EventSource.Backbone.Enums; +using Microsoft.Extensions.Logging; -using Handler = System.Func>; +using Handler = System.Func>; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// @@ -18,7 +19,7 @@ public partial class ConsumerBase /// /// Represent single consuming subscription /// - private class EventSourceSubscriber : IConsumerLifetime, IConsumerBridge + private sealed class EventSourceSubscriber : IConsumerLifetime, IConsumerBridge { private readonly IConsumer _consumer; private readonly CancellationTokenSource _disposeCancellation = new CancellationTokenSource(); @@ -55,7 +56,7 @@ public EventSourceSubscriber( _maxMessages = _options.MaxMessages; _handlers = new ConcurrentQueue(handlers); - _subscriptionLifetime = channel.SubsribeAsync( + _subscriptionLifetime = channel.SubscribeAsync( plan, ConsumingAsync, _disposeCancellation.Token); @@ -126,7 +127,7 @@ private async ValueTask ConsumingAsync( if (_maxMessages != 0 && _maxMessages < count) { await DisposeAsync(); - throw new OperationCanceledException(); // make sure it not auto ack; + throw new OperationCanceledException(); } #endregion // Increment & Validation Max Messages Limit @@ -134,7 +135,6 @@ private async ValueTask ConsumingAsync( var consumerMeta = new ConsumerMetadata(meta, cancellation); var logger = Logger; - logger.LogDebug("Consuming event: {0}", meta.Key()); #region _plan.Interceptors.InterceptAsync(...) @@ -152,7 +152,6 @@ private async ValueTask ConsumingAsync( #endregion // _plan.Interceptors.InterceptAsync(...) - string operation = meta.Operation; PartialConsumerBehavior partialBehavior = _options.PartialBehavior; bool hasProcessed = false; @@ -179,38 +178,37 @@ private async ValueTask ConsumingAsync( } return false; }, cancellation); - logger.LogDebug("Consumed event: {0}", meta.Key()); var options = Plan.Options; var behavior = options.AckBehavior; if (partialBehavior == PartialConsumerBehavior.ThrowIfNotHandled && !hasProcessed) { - Logger.LogCritical("No handler is matching event: {stream}, operation{operation}, MessageId:{id}", meta.Key(), meta.Operation, meta.MessageId); - throw new InvalidOperationException($"No handler is matching event: {meta.Key()}, operation{meta.Operation}, MessageId:{meta.MessageId}"); + Logger.LogCritical("No handler is matching event: {stream}, operation{operation}, MessageId:{id}", meta.FullUri(), meta.Operation, meta.MessageId); + throw new InvalidOperationException($"No handler is matching event: {meta.FullUri()}, operation{meta.Operation}, MessageId:{meta.MessageId}"); } if (hasProcessed) { if (behavior == AckBehavior.OnSucceed) - await ack.AckAsync(); - } - else - { - if (partialBehavior == PartialConsumerBehavior.Loose) - { - await ack.AckAsync(); - } - if (partialBehavior == PartialConsumerBehavior.Sequential) - { - await ack.AckAsync(); - } + await ack.AckAsync(AckBehavior.OnSucceed); } + //else + //{ + // if (partialBehavior == PartialConsumerBehavior.Loose) + // { + // await ack.AckAsync(); + // } + // if (partialBehavior == PartialConsumerBehavior.Sequential) + // { + // await ack.AckAsync(); + // } + //} } } #region Exception Handling catch (OperationCanceledException) { - logger.LogWarning("Canceled event: {0}", meta.Key()); + logger.LogWarning("Canceled event: {0}", meta.FullUri()); if (Plan.Options.AckBehavior != AckBehavior.OnFinally) { await ack.CancelAsync(); @@ -219,7 +217,7 @@ private async ValueTask ConsumingAsync( } catch (Exception ex) { - logger.LogError(ex, "event: {0}", meta.Key()); + logger.LogError(ex, "event: {0}", meta.FullUri()); if (Plan.Options.AckBehavior != AckBehavior.OnFinally) { await ack.CancelAsync(); @@ -230,13 +228,9 @@ private async ValueTask ConsumingAsync( #endregion // Exception Handling finally { - - if (Plan.Options.AckBehavior == AckBehavior.OnFinally) + if (Plan.Options.AckBehavior == AckBehavior.OnFinally && partialBehavior != PartialConsumerBehavior.Sequential) { - if (partialBehavior != PartialConsumerBehavior.Sequential) - { - await ack.AckAsync(); - } + await ack.AckAsync(Plan.Options.AckBehavior); } #region Validation Max Messages Limit @@ -256,12 +250,26 @@ private async ValueTask ConsumingAsync( #region Subscribe + /// + /// Subscribe consumer. + /// + /// Per operation invocation handler, handle methods calls. + /// + /// The subscription lifetime (dispose to remove the subscription) + /// + /// _plan + IConsumerLifetime IConsumerSubscribtionHubBuilder.Subscribe(ISubscriptionBridge handler) + + { + return ((IConsumerSubscribtionHubBuilder)this).Subscribe(handler.ToEnumerable()); + } + /// /// Subscribe consumer. /// /// Per operation invocation handler, handle methods calls. /// - /// The partition subscription (dispose to remove the subscription) + /// The subscription lifetime (dispose to remove the subscription) /// /// _plan IConsumerLifetime IConsumerSubscribtionHubBuilder.Subscribe(ISubscriptionBridge[] handlers) @@ -275,7 +283,7 @@ IConsumerLifetime IConsumerSubscribtionHubBuilder.Subscribe(ISubscriptionBridge[ /// /// Per operation invocation handler, handle methods calls. /// - /// The partition subscription (dispose to remove the subscription) + /// The subscription lifetime (dispose to remove the subscription) /// /// _plan IConsumerLifetime IConsumerSubscribtionHubBuilder.Subscribe( @@ -291,7 +299,7 @@ IConsumerLifetime IConsumerSubscribtionHubBuilder.Subscribe( /// /// Per operation invocation handler, handle methods calls. /// - /// The partition subscription (dispose to remove the subscription) + /// The subscription lifetime (dispose to remove the subscription) /// /// _plan IConsumerLifetime IConsumerSubscribtionHubBuilder.Subscribe( @@ -306,7 +314,7 @@ IConsumerLifetime IConsumerSubscribtionHubBuilder.Subscribe( /// /// Per operation invocation handler, handle methods calls. /// - /// The partition subscription (dispose to remove the subscription) + /// The subscription lifetime (dispose to remove the subscription) /// /// _plan IConsumerLifetime IConsumerSubscribtionHubBuilder.Subscribe( diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers/Builder/ConsumerBase.cs b/Consumers/EventSourcing.Backbone.Consumers/Builder/ConsumerBase.cs similarity index 97% rename from Consumers/Weknow.EventSource.Backbone.Consumers/Builder/ConsumerBase.cs rename to Consumers/EventSourcing.Backbone.Consumers/Builder/ConsumerBase.cs index 707332e0..118f87d4 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers/Builder/ConsumerBase.cs +++ b/Consumers/EventSourcing.Backbone.Consumers/Builder/ConsumerBase.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Base class for the consumer's code generator diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers/Builder/ConsumerBuilder.Iterator.cs b/Consumers/EventSourcing.Backbone.Consumers/Builder/ConsumerBuilder.Iterator.cs similarity index 83% rename from Consumers/Weknow.EventSource.Backbone.Consumers/Builder/ConsumerBuilder.Iterator.cs rename to Consumers/EventSourcing.Backbone.Consumers/Builder/ConsumerBuilder.Iterator.cs index 43dee576..8466e68a 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers/Builder/ConsumerBuilder.Iterator.cs +++ b/Consumers/EventSourcing.Backbone.Consumers/Builder/ConsumerBuilder.Iterator.cs @@ -2,19 +2,17 @@ using System.Runtime.CompilerServices; using System.Text.Json; -using Weknow.EventSource.Backbone.Building; +using EventSourcing.Backbone.Building; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public partial class ConsumerBuilder { - #region private class Iterator - /// /// Receive data (on demand data query). /// - [DebuggerDisplay("{_plan.Environment}:{_plan.Partition}:{_plan.Shard}")] - private class Iterator : IConsumerIterator + [DebuggerDisplay("{_plan.Environment}:{_plan.Uri}")] + private sealed class Iterator : IConsumerIterator { private readonly IConsumerPlan _plan; @@ -35,7 +33,7 @@ public Iterator(IConsumerPlan plan) /// /// Include the environment as prefix of the stream key. - /// for example: production:partition-name:shard-name + /// for example: env:URI /// /// The environment (null: keep current environment, empty: reset the environment to nothing). /// @@ -51,22 +49,40 @@ IConsumerIterator IConsumerEnvironmentOfBuilder.Environment(E #endregion // Environment - #region Partition + #region Environment + + /// + /// Fetch the origin environment of the message from an environment variable. + /// + /// The environment variable key. + /// + IConsumerIterator IConsumerEnvironmentOfBuilder.EnvironmentFromVariable(string environmentVariableKey) + { + string environment = Environment.GetEnvironmentVariable(environmentVariableKey) ?? throw new EventSourcingException($"EnvironmentFromVariable failed, [{environmentVariableKey}] not found!"); + + IConsumerPlan plan = _plan.ChangeEnvironment(environment); + var result = new Iterator(plan); + return result; + } + + #endregion // Environment + + #region Uri /// - /// replace the partition of the stream key. - /// for example: production:partition-name:shard-name + /// replace the stream's key (identity). + /// for example: env:URI /// - /// The partition. + /// The URI. /// - IConsumerIterator IConsumerPartitionBuilder.Partition(string partition) + IConsumerIterator IConsumerUriBuilder.Uri(string uri) { - IConsumerPlan plan = _plan.ChangePartition(partition); + IConsumerPlan plan = _plan.ChangeKey(uri); var result = new Iterator(plan); return result; } - #endregion // Partition + #endregion // Uri #region GetAsyncEnumerable @@ -132,8 +148,8 @@ IConsumerIterator IConsumerIterator.Specialize(ICo /// /// Receive data (on demand data query). /// - [DebuggerDisplay("{_plan.Environment}:{_plan.Partition}:{_plan.Shard}")] - private class Iterator : IConsumerIterator + [DebuggerDisplay("{_plan.Environment}:{_plan.Uri}")] + private sealed class Iterator : IConsumerIterator { private readonly IConsumerPlan _plan; private readonly IConsumerIterator _iterator; @@ -222,7 +238,5 @@ async IAsyncEnumerable IConsumerIteratorCommands.GetAsyncE #endregion // GetAsyncEnumerable } - - #endregion // private class Iterator } } diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers/Builder/ConsumerBuilder.Receiver.cs b/Consumers/EventSourcing.Backbone.Consumers/Builder/ConsumerBuilder.Receiver.cs similarity index 66% rename from Consumers/Weknow.EventSource.Backbone.Consumers/Builder/ConsumerBuilder.Receiver.cs rename to Consumers/EventSourcing.Backbone.Consumers/Builder/ConsumerBuilder.Receiver.cs index 0800e4f7..8ba79c58 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers/Builder/ConsumerBuilder.Receiver.cs +++ b/Consumers/EventSourcing.Backbone.Consumers/Builder/ConsumerBuilder.Receiver.cs @@ -1,19 +1,18 @@ using System.Diagnostics; using System.Text.Json; -using Weknow.EventSource.Backbone.Building; +using EventSourcing.Backbone.Building; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public partial class ConsumerBuilder { - #region private class Receiver /// /// Receive data (on demand data query). /// - [DebuggerDisplay("{_plan.Environment}:{_plan.Partition}:{_plan.Shard}")] - private class Receiver : IConsumerReceiver + [DebuggerDisplay("{_plan.Environment}:{_plan.Uri}")] + private sealed class Receiver : IConsumerReceiver { private readonly IConsumerPlan _plan; @@ -32,9 +31,29 @@ public Receiver(IConsumerPlan plan) #region Environment + /// + /// Environments from variable. + /// + /// The environment variable key. + /// + /// EnvironmentFromVariable failed, [{environmentVariableKey}] not found! + IConsumerReceiver IConsumerEnvironmentOfBuilder.EnvironmentFromVariable(string environmentVariableKey) + { + string environment = Environment.GetEnvironmentVariable(environmentVariableKey) ?? throw new EventSourcingException($"EnvironmentFromVariable failed, [{environmentVariableKey}] not found!"); + + + IConsumerPlan plan = _plan.ChangeEnvironment(environment); + var result = new Receiver(plan); + return result; + } + + #endregion // Environment + + #region Environment + /// /// Include the environment as prefix of the stream key. - /// for example: production:partition-name:shard-name + /// for example: env:URI /// /// The environment (null: keep current environment, empty: reset the environment to nothing). /// @@ -50,20 +69,23 @@ IConsumerReceiver IConsumerEnvironmentOfBuilder.Environment(E #endregion // Environment + #region Uri /// - /// replace the partition of the stream key. - /// for example: production:partition-name:shard-name + /// replace the URI of the stream key. + /// for example: env:URI /// - /// The partition. + /// The URI. /// - IConsumerReceiver IConsumerPartitionBuilder.Partition(string partition) + IConsumerReceiver IConsumerUriBuilder.Uri(string uri) { - IConsumerPlan plan = _plan.ChangePartition(partition); + IConsumerPlan plan = _plan.ChangeKey(uri); var result = new Receiver(plan); return result; } + #endregion // Uri + #region GetByIdAsync /// @@ -104,7 +126,5 @@ async ValueTask IConsumerReceiverCommands.GetJsonByIdAsync( #endregion // GetJsonByIdAsync } - - #endregion // private class Receiver } } diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers/Builder/ConsumerBuilder.cs b/Consumers/EventSourcing.Backbone.Consumers/Builder/ConsumerBuilder.cs similarity index 77% rename from Consumers/Weknow.EventSource.Backbone.Consumers/Builder/ConsumerBuilder.cs rename to Consumers/EventSourcing.Backbone.Consumers/Builder/ConsumerBuilder.cs index 79fb9791..9087f0d0 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers/Builder/ConsumerBuilder.cs +++ b/Consumers/EventSourcing.Backbone.Consumers/Builder/ConsumerBuilder.cs @@ -1,26 +1,27 @@ using System.Buffers; +using System.Collections; using System.Diagnostics; using System.Text; using System.Text.Json; +using EventSourcing.Backbone.Building; + using Microsoft.Extensions.Logging; using Polly; -using Weknow.EventSource.Backbone.Building; - -using static Weknow.EventSource.Backbone.EventSourceConstants; +using static EventSourcing.Backbone.EventSourceConstants; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Event Source consumer builder. /// - [DebuggerDisplay("{_plan.Environment}:{_plan.Partition}:{_plan.Shard}")] + [DebuggerDisplay("{_plan.Environment}:{_plan.Uri}")] public partial class ConsumerBuilder : IConsumerBuilder, - IConsumerShardBuilder, - IConsumerStoreStrategyBuilder + IConsumerReadyBuilder, + IConsumerIocStoreStrategyBuilder { private readonly ConsumerPlan _plan = ConsumerPlan.Empty; @@ -74,6 +75,21 @@ IConsumerStoreStrategyBuilder IConsumerBuilder.UseChannel( return result; } + /// + /// Choose the communication channel provider. + /// + /// Dependency injection provider. + /// The channel provider. + /// + IConsumerIocStoreStrategyBuilder IConsumerBuilder.UseChannel( + IServiceProvider serviceProvider, + Func channel) + { + var prms = _plan.WithChannelFactory(channel, serviceProvider); + var result = new ConsumerBuilder(prms); + return result; + } + #endregion // UseChannel #region AddStorageStrategy @@ -145,11 +161,11 @@ IConsumerSubscribeBuilder IConsumerSubscribeBuilder.WithOptions(Func /// Include the environment as prefix of the stream key. - /// for example: production:partition-name:shard-name + /// for example: env:URI /// /// The environment (null: keep current environment, empty: reset the environment to nothing). /// - IConsumerPartitionBuilder IConsumerEnvironmentOfBuilder>.Environment(Env? environment) + IConsumerUriBuilder IConsumerEnvironmentOfBuilder>.Environment(Env? environment) { if (environment == null) return this; @@ -161,7 +177,7 @@ IConsumerPartitionBuilder IConsumerEnvironmentOfBuilder /// Include the environment as prefix of the stream key. - /// for example: production:partition-name:shard-name + /// for example: env:URI /// /// The environment (null: keep current environment, empty: reset the environment to nothing). /// @@ -177,95 +193,82 @@ IConsumerSubscribeBuilder IConsumerEnvironmentOfBuilder - /// Partition key represent logical group of - /// event source shards. - /// For example assuming each ORDERING flow can have its - /// own messaging sequence, yet can live concurrency with - /// other ORDER's sequences. - /// The partition will let consumer the option to be notify and - /// consume multiple shards from single consumer. - /// This way the consumer can handle all orders in - /// central place without affecting sequence of specific order - /// flow or limiting the throughput. + /// Fetch the origin environment of the message from an environment variable. /// - /// The partition key. + /// The environment variable key. /// - IConsumerShardBuilder IConsumerPartitionBuilder.Partition( - string partition) + /// EnvironmentFromVariable failed, [{environmentVariableKey}] not found! + IConsumerUriBuilder IConsumerEnvironmentOfBuilder>.EnvironmentFromVariable(string environmentVariableKey) { - var prms = _plan.WithPartition(partition); + string environment = Environment.GetEnvironmentVariable(environmentVariableKey) ?? throw new EventSourcingException($"EnvironmentFromVariable failed, [{environmentVariableKey}] not found!"); + + var prms = _plan.WithEnvironment(environment); var result = new ConsumerBuilder(prms); return result; } /// - /// Partition key represent logical group of - /// event source shards. - /// For example assuming each ORDERING flow can have its - /// own messaging sequence, yet can live concurrency with - /// other ORDER's sequences. - /// The partition will let consumer the option to be notify and - /// consume multiple shards from single consumer. - /// This way the consumer can handle all orders in - /// central place without affecting sequence of specific order - /// flow or limiting the throughput. + /// Fetch the origin environment of the message from an environment variable. /// - /// The partition key. + /// The environment variable key. /// - IConsumerSubscribeBuilder IConsumerPartitionBuilder.Partition( - string partition) + /// EnvironmentFromVariable failed, [{environmentVariableKey}] not found! + IConsumerSubscribeBuilder IConsumerEnvironmentOfBuilder.EnvironmentFromVariable(string environmentVariableKey) { - var prms = _plan.WithPartition(partition); + string environment = Environment.GetEnvironmentVariable(environmentVariableKey) ?? throw new EventSourcingException($"EnvironmentFromVariable failed, [{environmentVariableKey}] not found!"); + + var prms = _plan.WithEnvironment(environment); var result = new ConsumerBuilder(prms); return result; } - #endregion // Partition + #endregion // EnvironmentFromVariable + + #region ServiceProvider + + /// + /// Gets the service provider. + /// + /// ServiceProvider is null + IServiceProvider IConsumerIocStoreStrategyBuilder.ServiceProvider => _plan.ServiceProvider ?? throw new EventSourcingException("ServiceProvider is null"); + + #endregion // ServiceProvider + + #region Uri - #region Shard + /// + /// Stream's name + /// + /// + /// The stream identifier (the URI combined with the environment separate one stream from another) + /// + /// + IConsumerReadyBuilder IConsumerUriBuilder.Uri(string uri) + { + var prms = _plan.WithKey(uri); + var result = new ConsumerBuilder(prms); + return result; + } /// - /// Shard key represent physical sequence. - /// On the consumer side shard is optional - /// for listening on a physical source rather on the entire partition. - /// Use same shard when order is matter. - /// For example: assuming each ORDERING flow can have its - /// own messaging sequence, in this case you can split each - /// ORDER into different shard and gain performance bust.. + /// Stream's name /// - /// The shard key. + /// + /// The stream identifier (the URI combined with the environment separate one stream from another) + /// /// - /// - IConsumerReadyBuilder IConsumerShardOfBuilder.Shard(string shard) + IConsumerSubscribeBuilder IConsumerUriBuilder.Uri( + string uri) { - var prms = _plan.WithShard(shard); + var prms = _plan.WithKey(uri); var result = new ConsumerBuilder(prms); return result; } - ///// - ///// Shard key represent physical sequence. - ///// On the consumer side shard is optional - ///// for listening on a physical source rather on the entire partition. - ///// Use same shard when order is matter. - ///// For example: assuming each ORDERING flow can have its - ///// own messaging sequence, in this case you can split each - ///// ORDER into different shard and gain performance bust.. - ///// - ///// The shard key. - ///// - ///// - //IConsumerSubscribeBuilder IConsumerShardOfBuilder.Shard(string shard) - //{ - // var prms = _plan.WithShard(shard); - // var result = new ConsumerBuilder(prms); - // return result; - //} - - #endregion // Shard + #endregion // Uri #region RegisterSegmentationStrategy @@ -319,12 +322,12 @@ IConsumerHooksBuilder IConsumerHooksBuilder.RegisterSegmentationStrategy(IConsum /// /// Registers the interceptor. /// - /// The interceptor. + /// The interceptor. /// IConsumerHooksBuilder IConsumerHooksBuilder.RegisterInterceptor( - IConsumerInterceptor interceptor) + IConsumerInterceptor interceptorData) { - var bridge = new ConsumerInterceptorBridge(interceptor); + var bridge = new ConsumerInterceptorBridge(interceptorData); var prms = _plan.AddInterceptor(bridge); var result = new ConsumerBuilder(prms); return result; @@ -333,12 +336,12 @@ IConsumerHooksBuilder IConsumerHooksBuilder.RegisterInterceptor( /// /// Registers the interceptor. /// - /// The interceptor. + /// The interceptor. /// IConsumerHooksBuilder IConsumerHooksBuilder.RegisterInterceptor( - IConsumerAsyncInterceptor interceptor) + IConsumerAsyncInterceptor interceptorData) { - var prms = _plan.AddInterceptor(interceptor); + var prms = _plan.AddInterceptor(interceptorData); var result = new ConsumerBuilder(prms); return result; } @@ -409,14 +412,26 @@ IConsumerSubscribeBuilder IConsumerSubscribeBuilder.Name(string consumerName) #region Subscribe + /// + /// Subscribe consumer. + /// + /// Per operation invocation handler, handle methods calls. + /// + /// The subscription lifetime (dispose to remove the subscription) + /// + IConsumerLifetime IConsumerSubscribtionHubBuilder.Subscribe(ISubscriptionBridge handler) + + { + return ((IConsumerSubscribtionHubBuilder)this).Subscribe(handler.ToEnumerable()); + } + /// /// Subscribe consumer. /// /// Per operation invocation handler, handle methods calls. /// - /// The partition subscription (dispose to remove the subscription) + /// The subscription lifetime (dispose to remove the subscription) /// - /// _plan IConsumerLifetime IConsumerSubscribtionHubBuilder.Subscribe(ISubscriptionBridge[] handlers) { @@ -428,9 +443,8 @@ IConsumerLifetime IConsumerSubscribtionHubBuilder.Subscribe(ISubscriptionBridge[ /// /// Per operation invocation handler, handle methods calls. /// - /// The partition subscription (dispose to remove the subscription) + /// The subscription lifetime (dispose to remove the subscription) /// - /// _plan IConsumerLifetime IConsumerSubscribtionHubBuilder.Subscribe( IEnumerable handlers) @@ -438,7 +452,7 @@ IConsumerLifetime IConsumerSubscribtionHubBuilder.Subscribe( #region Validation if (_plan == null) - throw new ArgumentNullException(nameof(_plan)); + throw new EventSourcingException(nameof(_plan)); #endregion // Validation @@ -457,9 +471,8 @@ IConsumerLifetime IConsumerSubscribtionHubBuilder.Subscribe( /// /// Per operation invocation handler, handle methods calls. /// - /// The partition subscription (dispose to remove the subscription) + /// The subscription lifetime (dispose to remove the subscription) /// - /// _plan IConsumerLifetime IConsumerSubscribtionHubBuilder.Subscribe( params Func>[] handlers) @@ -472,9 +485,8 @@ IConsumerLifetime IConsumerSubscribtionHubBuilder.Subscribe( /// /// Per operation invocation handler, handle methods calls. /// - /// The partition subscription (dispose to remove the subscription) + /// The subscription lifetime (dispose to remove the subscription) /// - /// _plan IConsumerLifetime IConsumerSubscribtionHubBuilder.Subscribe( IEnumerable>> handlers) @@ -482,7 +494,7 @@ IConsumerLifetime IConsumerSubscribtionHubBuilder.Subscribe( #region Validation if (_plan == null) - throw new ArgumentNullException(nameof(_plan)); + throw new EventSourcingException(nameof(_plan)); #endregion // Validation @@ -604,11 +616,8 @@ private static JsonElement ToJson( w.WritePropertyName("__env__"); w.WriteStringValue(announcement.Environment); - w.WritePropertyName("__partition__"); - w.WriteStringValue(announcement.Partition); - - w.WritePropertyName("__shard__"); - w.WriteStringValue(announcement.Shard); + w.WritePropertyName("__uri__"); + w.WriteStringValue(announcement.Uri); w.WritePropertyName("__operation__"); w.WriteStringValue(announcement.Operation); @@ -632,11 +641,14 @@ private static JsonElement ToJson( { encoded = Encoding.UTF8.GetString(val.Span); } - catch { } + catch + { + plan.Logger.LogDebug("Failed to encode value of ({key})", key); + } - var err = $"GetJsonByIdAsync [{entryId}, {announcement.Key()}]: failed to deserialize key='{key}', base64='{Convert.ToBase64String(val.ToArray())}', data={encoded}"; + var err = $"GetJsonByIdAsync [{entryId}, {announcement.FullUri()}]: failed to deserialize key='{key}', base64='{Convert.ToBase64String(val.ToArray())}', data={encoded}"; plan.Logger.LogError(ex.FormatLazy(), "GetJsonByIdAsync [{id}, {at}]: failed to deserialize key='{key}', base64='{value}', data={data}", - entryId, announcement.Key(), key, + entryId, announcement.FullUri(), key, Convert.ToBase64String(val.ToArray()), encoded); throw new DataMisalignedException(err, ex); @@ -699,11 +711,18 @@ private static JsonElement ToJson( { encoded = Encoding.UTF8.GetString(val.Span); } - catch { } + #region Exception Handling + + catch + { + plan.Logger.LogDebug("Encoding failure"); + } + + #endregion // Exception Handling - var err = $"GetJsonByIdAsync [{entryId}, {announcement.Metadata.Key()}]: failed to deserialize key='{key}', base64='{Convert.ToBase64String(val.ToArray())}', data={encoded}"; + var err = $"GetJsonByIdAsync [{entryId}, {announcement.Metadata.FullUri()}]: failed to deserialize key='{key}', base64='{Convert.ToBase64String(val.ToArray())}', data={encoded}"; plan.Logger.LogError(ex.FormatLazy(), "GetJsonByIdAsync [{id}, {at}]: failed to deserialize key='{key}', base64='{value}', data={data}", - entryId, announcement.Metadata.Key(), key, + entryId, announcement.Metadata.FullUri(), key, Convert.ToBase64String(val.ToArray()), encoded); throw new DataMisalignedException(err, ex); diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers/Builder/ConsumerPlan.cs b/Consumers/EventSourcing.Backbone.Consumers/Builder/ConsumerPlan.cs similarity index 85% rename from Consumers/Weknow.EventSource.Backbone.Consumers/Builder/ConsumerPlan.cs rename to Consumers/EventSourcing.Backbone.Consumers/Builder/ConsumerPlan.cs index a60ed01f..9eacd789 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers/Builder/ConsumerPlan.cs +++ b/Consumers/EventSourcing.Backbone.Consumers/Builder/ConsumerPlan.cs @@ -1,21 +1,21 @@ using System.Collections.Immutable; using System.Diagnostics; +using EventSourcing.Backbone.Building; +using EventSourcing.Backbone.Private; + using Microsoft.Extensions.Logging; using Polly; -using Weknow.EventSource.Backbone.Building; -using Weknow.EventSource.Backbone.Private; - -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Hold builder definitions. /// Define the consumer execution pipeline. /// - [DebuggerDisplay("{Environment}:{Partition}:{Shard}, Consumer: [{ConsumerGroup}, {ConsumerName}]")] + [DebuggerDisplay("{Environment}:{Uri}, Consumer: [{ConsumerGroup}, {ConsumerName}]")] public class ConsumerPlan : IConsumerPlan, IConsumerPlanBuilder { public static readonly ConsumerPlan Empty = new ConsumerPlan(); @@ -43,8 +43,7 @@ private ConsumerPlan() /// The channel. /// The channel. /// The environment. - /// The partition. - /// The shard. + /// The stream's key (identity). /// The logger. /// The options. /// The segmentation strategies. @@ -57,13 +56,13 @@ private ConsumerPlan() /// Can use for observability. /// The resilience policy. /// The storage strategy. + /// The service provider. private ConsumerPlan( ConsumerPlan copyFrom, Func? channelFactory = null, IConsumerChannelProvider? channel = null, string? environment = null, - string? partition = null, - string? shard = null, + string? key = null, ILogger? logger = null, ConsumerOptions? options = null, IImmutableList? segmentationStrategies = null, @@ -73,13 +72,13 @@ private ConsumerPlan( string? consumerGroup = null, string? consumerName = null, AsyncPolicy? resiliencePolicy = null, - Func>? storageStrategyFactories = null) + Func>? storageStrategyFactories = null, + IServiceProvider? serviceProvider = null) { ChannelFactory = channelFactory ?? copyFrom.ChannelFactory; _channel = channel ?? copyFrom._channel; Environment = environment ?? copyFrom.Environment; - Partition = partition ?? copyFrom.Partition; - Shard = shard ?? copyFrom.Shard; + Uri = key ?? copyFrom.Uri; Logger = logger ?? copyFrom.Logger; Options = options ?? copyFrom.Options; SegmentationStrategies = segmentationStrategies ?? copyFrom.SegmentationStrategies; @@ -88,6 +87,7 @@ private ConsumerPlan( ConsumerGroup = consumerGroup ?? copyFrom.ConsumerGroup; ConsumerName = consumerName ?? copyFrom.ConsumerName; ResiliencePolicy = resiliencePolicy ?? copyFrom.ResiliencePolicy; + ServiceProvider = serviceProvider ?? copyFrom.ServiceProvider; StorageStrategyFactories = storageStrategyFactories == null ? copyFrom.StorageStrategyFactories : copyFrom.StorageStrategyFactories.Add(storageStrategyFactories); @@ -123,8 +123,11 @@ IConsumerChannelProvider IConsumerPlan.Channel { get { +#pragma warning disable S2372 // Exceptions should not be thrown from property getters if (_channel == null) - throw new ArgumentNullException("Event Source Consumer channel not set"); + throw new EventSourcingException("Event Source Consumer channel not set"); +#pragma warning restore S2372 + return _channel; } } @@ -167,36 +170,32 @@ IConsumerChannelProvider IConsumerPlan.Channel #endregion // Environment - #region Partition + #region Uri + private string _uri = string.Empty; /// - /// Partition key represent logical group of - /// event source shards. - /// For example assuming each ORDERING flow can have its - /// own messaging sequence, yet can live concurrency with - /// other ORDER's sequences. - /// The partition will let consumer the option to be notify and - /// consume multiple shards from single consumer. - /// This way the consumer can handle all orders in - /// central place without affecting sequence of specific order - /// flow or limiting the throughput. + /// The stream identifier (the URI combined with the environment separate one stream from another) /// - public string Partition { get; } = string.Empty; + public string Uri + { + get => _uri; + init + { + _uri = value; + UriDash = value.ToDash(); + } + } - #endregion // Partition + #endregion Uri - #region Shard + #region UriDash /// - /// Shard key represent physical sequence. - /// Use same shard when order is matter. - /// For example: assuming each ORDERING flow can have its - /// own messaging sequence, in this case you can split each - /// ORDER into different shard and gain performance bust.. + /// Gets a lower-case URI with dash separator. /// - public string Shard { get; } = string.Empty; + public string UriDash { get; private set; } = string.Empty; - #endregion // Shard + #endregion // UriDash #region Options @@ -252,7 +251,7 @@ IConsumerChannelProvider IConsumerPlan.Channel /// /// Routes are sub-pipelines are results of merge operation - /// which can split same payload into multiple partitions or shards. + /// which can split same payload into multiple URIs. /// private readonly IImmutableList Routes = ImmutableList.Empty; @@ -266,7 +265,7 @@ IConsumerChannelProvider IConsumerPlan.Channel /// Consumer Group allow a group of clients to cooperate /// consuming a different portion of the same stream of messages /// - public string ConsumerGroup { get; } = string.Empty; + public string ConsumerGroup { get; } = "default"; #endregion // ConsumerGroup @@ -289,6 +288,15 @@ IConsumerChannelProvider IConsumerPlan.Channel #endregion // ResiliencePolicy + #region ServiceProvider + + /// + /// Gets the service provider. + /// + public IServiceProvider? ServiceProvider { get; } + + #endregion // ServiceProvider + //------------------------------------------ #region GetParameterAsync @@ -322,10 +330,11 @@ async ValueTask IConsumerPlan.GetParameterAsync( /// Attach the channel. /// /// The channel. + /// Dependency injection provider. /// - internal ConsumerPlan WithChannelFactory(Func channel) + internal ConsumerPlan WithChannelFactory(Func channel, IServiceProvider? serviceProvider = null) { - return new ConsumerPlan(this, channelFactory: channel); + return new ConsumerPlan(this, channelFactory: channel, serviceProvider: serviceProvider); } #endregion // WithChannel @@ -418,55 +427,35 @@ IConsumerPlan IConsumerPlan.ChangeEnvironment(Env? environment) #endregion // ChangeEnvironment - #region ChangePartition + #region ChangeKey /// - /// Attach the environment. + /// change the stream's key. /// - /// The partition. + /// The URI. /// - IConsumerPlan IConsumerPlan.ChangePartition(Env? partition) + IConsumerPlan IConsumerPlan.ChangeKey(string? uri) { - if (partition == null) return this; - - return new ConsumerPlan(this, partition: partition); - } + if (uri == null) return this; - #endregion // ChangePartition - - #region WithPartition - - /// - /// Attach the partition. - /// - /// The partition. - /// - internal ConsumerPlan WithPartition(string partition) - { - return new ConsumerPlan(this, partition: partition); + return WithKey(uri); } - #endregion // WithPartition + #endregion // ChangeKey - #region WithShard + #region WithKey /// - /// Attach the shard. - /// - /// The shard. - /// - IConsumerPlan IConsumerPlanBase.WithShard(string shard) => WithShard(shard); - /// - /// Attach the shard. + /// Attach a key. /// - /// The shard. + /// The URI. /// - internal ConsumerPlan WithShard(string shard) + internal ConsumerPlan WithKey(string uri) { - return new ConsumerPlan(this, shard: shard); + return new ConsumerPlan(this, key: uri); } - #endregion // WithShard + #endregion // WithKey #region WithResiliencePolicy diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers/Builder/FilteredStorageStrategy.cs b/Consumers/EventSourcing.Backbone.Consumers/Builder/FilteredStorageStrategy.cs similarity index 91% rename from Consumers/Weknow.EventSource.Backbone.Consumers/Builder/FilteredStorageStrategy.cs rename to Consumers/EventSourcing.Backbone.Consumers/Builder/FilteredStorageStrategy.cs index d6a45b45..a914b706 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers/Builder/FilteredStorageStrategy.cs +++ b/Consumers/EventSourcing.Backbone.Consumers/Builder/FilteredStorageStrategy.cs @@ -1,6 +1,6 @@ -using Weknow.EventSource.Backbone.Building; +using EventSourcing.Backbone.Building; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Wrap Channel Storage with key filtering of the bucket. @@ -25,6 +25,11 @@ public FilteredStorageStrategy( _targetType = targetType; } + /// + /// Gets the name of the storage provider. + /// + public string Name => _storage.Name; + /// /// Determines whether is of the right target type. /// diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers/ConsumerDefaultSegmentationStrategy.cs b/Consumers/EventSourcing.Backbone.Consumers/ConsumerDefaultSegmentationStrategy.cs similarity index 85% rename from Consumers/Weknow.EventSource.Backbone.Consumers/ConsumerDefaultSegmentationStrategy.cs rename to Consumers/EventSourcing.Backbone.Consumers/ConsumerDefaultSegmentationStrategy.cs index 20bbf8cc..3247232f 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers/ConsumerDefaultSegmentationStrategy.cs +++ b/Consumers/EventSourcing.Backbone.Consumers/ConsumerDefaultSegmentationStrategy.cs @@ -1,9 +1,9 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Responsible of converting raw segment (parameter) into object, i.e. deserialize a segment /// - /// + /// public class ConsumerDefaultSegmentationStrategy : IConsumerAsyncSegmentationStrategy { @@ -36,7 +36,7 @@ public class ConsumerDefaultSegmentationStrategy : catch (Exception ex) { - throw new Exception($"Fail to serialize event [{metadata}]: operation=[{operation}], argument-name=[{argumentName}], Target type=[{typeof(T).Name}], Base64 Data=[{Convert.ToBase64String(data.ToArray())}]", ex); + throw new EventSourcingException($"Fail to serialize event [{metadata}]: operation=[{operation}], argument-name=[{argumentName}], Target type=[{typeof(T).Name}], Base64 Data=[{Convert.ToBase64String(data.ToArray())}]", ex); } #endregion // Exception Handling diff --git a/Consumers/EventSourcing.Backbone.Consumers/ConsumerTelemetryExtensions.cs b/Consumers/EventSourcing.Backbone.Consumers/ConsumerTelemetryExtensions.cs new file mode 100644 index 00000000..0a44eb51 --- /dev/null +++ b/Consumers/EventSourcing.Backbone.Consumers/ConsumerTelemetryExtensions.cs @@ -0,0 +1,97 @@ +using System.Collections; +using System.Diagnostics; +using System.Diagnostics.Metrics; + +namespace EventSourcing.Backbone.Consumers; + +public static class ConsumerTelemetryExtensions +{ + #region StartConsumerTrace + + /// + /// Starts a consumer trace. + /// + /// The activity source. + /// The metadata. + /// The parent context. + /// The tags action. + /// + public static Activity? StartConsumerTrace(this ActivitySource activitySource, + Metadata meta, + ActivityContext parentContext, + Action? tagsAction = null) + { + // Start an activity with a name following the semantic convention of the OpenTelemetry messaging specification. + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/messaging.md#span-name + var activityName = $"evt-src.sys.Consumer.{meta.Environment.ToDash()}.{meta.UriDash}.{meta.Operation}.process"; + + var tags = new ActivityTagsCollection(); + var t = new TagAddition(tags); + tagsAction?.Invoke(t); + + Activity? activity = activitySource.StartActivity( + activityName, + ActivityKind.Consumer, + parentContext, tags, links: new ActivityLink(parentContext).ToEnumerable()); + meta.InjectTelemetryTags(activity); + + return activity; + } + + #endregion // StartConsumerTrace + + #region WithEnvUriOperation + + /// + /// Add URI, Environment and Operation labels. + /// + /// + /// The counter. + /// The metadata. + /// + public static ICounterBuilder WithEnvUriOperation(this Counter counter, Metadata metadata) + where T : struct + { + return counter + .WithTag("uri", metadata.UriDash) + .WithTag("env", metadata.Environment.DashFormat()) + .WithTag("operation", metadata.Operation.ToDash()); + } + + /// + /// Add URI, Environment and Operation labels. + /// + /// + /// The counter. + /// The metadata. + /// + public static ICounterBuilder WithEnvUriOperation(this ICounterBuilder counter, Metadata metadata) + where T : struct + { + return counter + .WithTag("uri", metadata.UriDash) + .WithTag("env", metadata.Environment.DashFormat()) + .WithTag("operation", metadata.Operation.ToDash()); + } + + #endregion // WithEnvUriOperation + + #region AddEnvUriOperation + + /// + /// Add URI, Environment and Operation labels. + /// + /// + /// The counter. + /// The metadata. + /// + public static ITagAddition AddEnvUriOperation(this ITagAddition counter, Metadata metadata) + { + return counter + .Add("uri", metadata.UriDash) + .Add("env", metadata.Environment.DashFormat()) + .Add("operation", metadata.Operation.ToDash()); + } + + #endregion // AddEnvUriOperation +} diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers/Weknow.EventSource.Backbone.Consumers.csproj b/Consumers/EventSourcing.Backbone.Consumers/EventSourcing.Backbone.Consumers.csproj similarity index 53% rename from Consumers/Weknow.EventSource.Backbone.Consumers/Weknow.EventSource.Backbone.Consumers.csproj rename to Consumers/EventSourcing.Backbone.Consumers/EventSourcing.Backbone.Consumers.csproj index 9e899915..d46bfd97 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers/Weknow.EventSource.Backbone.Consumers.csproj +++ b/Consumers/EventSourcing.Backbone.Consumers/EventSourcing.Backbone.Consumers.csproj @@ -1,9 +1,19 @@  - - - - + + README.md + + + + + True + \ + + + + + + @@ -14,7 +24,7 @@ - + diff --git a/Consumers/EventSourcing.Backbone.Consumers/icon.png b/Consumers/EventSourcing.Backbone.Consumers/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/Consumers/EventSourcing.Backbone.Consumers/icon.png differ diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Ack/IAck.cs b/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Ack/IAck.cs deleted file mode 100644 index 0566a92a..00000000 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Ack/IAck.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Weknow.EventSource.Backbone -{ - /// - /// Preform acknowledge (which should prevent the - /// message from process again by the consumer) - /// - /// - public interface IAck : IAsyncDisposable - { - /// - /// Preform acknowledge (which should prevent the - /// message from process again by the consumer) - /// - ValueTask AckAsync(); - - /// - /// Cancel acknowledge (will happen on error in order to avoid ack on succeed) - /// - /// - ValueTask CancelAsync(); - } -} diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Attributes.cs b/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Attributes.cs deleted file mode 100644 index b44bdfb9..00000000 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Attributes.cs +++ /dev/null @@ -1,5 +0,0 @@ -using System.Runtime.CompilerServices; - - -[assembly: InternalsVisibleToAttribute("Weknow.EventSource.Backbone.Consumers")] - diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Route/IConsumerEnvironmentBuilder.cs b/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Route/IConsumerEnvironmentBuilder.cs deleted file mode 100644 index d390fccc..00000000 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Route/IConsumerEnvironmentBuilder.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Weknow.EventSource.Backbone.Building -{ - /// - /// Event Source producer builder. - /// - public interface IConsumerEnvironmentBuilder : - IConsumerPartitionBuilder, - IConsumerEnvironmentOfBuilder> - { - } -} diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Route/IConsumerEnvironmentOfBuilder.cs b/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Route/IConsumerEnvironmentOfBuilder.cs deleted file mode 100644 index 446cc24f..00000000 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Route/IConsumerEnvironmentOfBuilder.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Weknow.EventSource.Backbone.Building -{ - /// - /// Event Source producer builder. - /// - public interface IConsumerEnvironmentOfBuilder - { - /// - /// Include the environment as prefix of the stream key. - /// for example: production:partition-name:shard-name - /// - /// The environment (null: keep current environment, empty: reset the environment to nothing). - /// - T Environment(Env? environment); - } -} diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Route/IConsumerPartitionBuilder.cs b/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Route/IConsumerPartitionBuilder.cs deleted file mode 100644 index 804563db..00000000 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Route/IConsumerPartitionBuilder.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Weknow.EventSource.Backbone.Building -{ - - /// - /// Event Source producer builder. - /// - public interface IConsumerPartitionBuilder - { - /// - /// Partition key represent logical group of - /// event source shards. - /// For example assuming each ORDERING flow can have its - /// own messaging sequence, yet can live concurrency with - /// other ORDER's sequences. - /// The partition will let consumer the option to be notify and - /// consume multiple shards from single consumer. - /// This way the consumer can handle all orders in - /// central place without affecting sequence of specific order - /// flow or limiting the throughput. - /// - /// The partition key. - /// - T Partition(string partition); - } -} diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Route/IConsumerShardBuilder.cs b/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Route/IConsumerShardBuilder.cs deleted file mode 100644 index 0c506630..00000000 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Route/IConsumerShardBuilder.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Weknow.EventSource.Backbone.Building -{ - /// - /// Event Source producer builder. - /// - public interface IConsumerShardBuilder : - IConsumerReadyBuilder, IConsumerShardOfBuilder - { - } -} diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Route/IConsumerShardOfBuilder.cs b/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Route/IConsumerShardOfBuilder.cs deleted file mode 100644 index 07d40ad0..00000000 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Route/IConsumerShardOfBuilder.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Weknow.EventSource.Backbone.Building -{ - /// - /// Event Source producer builder. - /// - public interface IConsumerShardOfBuilder - { - - /// - /// Shard key represent physical sequence. - /// On the consumer side shard is optional - /// for listening on a physical source rather on the entire partition. - /// Use same shard when order is matter. - /// For example: assuming each ORDERING flow can have its - /// own messaging sequence, in this case you can split each - /// ORDER into different shard and gain performance bust.. - /// - /// The shard key. - /// - T Shard(string shardKey); - } -} diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/IConsumerBuilder.cs b/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/IConsumerBuilder.cs deleted file mode 100644 index 02e09ec0..00000000 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/IConsumerBuilder.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.Extensions.Logging; - -using Weknow.EventSource.Backbone.Building; - -namespace Weknow.EventSource.Backbone -{ - /// - /// Event Source Consumer builder. - /// - public interface IConsumerBuilder - { - /// - /// Choose the communication channel provider. - /// - /// The channel provider. - /// - IConsumerStoreStrategyBuilder UseChannel(Func channel); - } -} diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/TelemetryrExtensions.cs b/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/TelemetryrExtensions.cs deleted file mode 100644 index 4b4296f2..00000000 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/TelemetryrExtensions.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Diagnostics; - -using OpenTelemetry; -using OpenTelemetry.Context.Propagation; - -namespace Weknow.EventSource.Backbone -{ - public static class TelemetryrExtensions - { - private static readonly TextMapPropagator Propagator = Propagators.DefaultTextMapPropagator; - - #region ExtractSpan - - /// - /// Extract telemetry span's parent info - /// - /// - /// The meta. - /// The entries for extraction. - /// The injection strategy. - /// - public static ActivityContext ExtractSpan( - this Metadata meta, - T entries, - Func> injectStrategy) - { - PropagationContext parentContext = Propagator.Extract(default, entries, injectStrategy); - Baggage.Current = parentContext.Baggage; - - // Start an activity with a name following the semantic convention of the OpenTelemetry messaging specification. - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/messaging.md#span-name - return parentContext.ActivityContext; - } - - #endregion // ExtractSpan - } -} diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Weknow.EventSource.Backbone.Consumers.Contracts.csproj b/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Weknow.EventSource.Backbone.Consumers.Contracts.csproj deleted file mode 100644 index 563e362b..00000000 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Weknow.EventSource.Backbone.Consumers.Contracts.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - - Weknow.EventSource.Backbone - - - - - - - - - - - diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Weknow.EventSource.Backbone.Consumers.Contracts.xml b/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Weknow.EventSource.Backbone.Consumers.Contracts.xml deleted file mode 100644 index 21ee48cd..00000000 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Weknow.EventSource.Backbone.Consumers.Contracts.xml +++ /dev/null @@ -1,982 +0,0 @@ - - - - Weknow.EventSource.Backbone.Consumers.Contracts - - - - - Acknowledge context - - - - - - Initializes the class. - - - - - Sets ack. - - - - - - - Gets the current. - - - - - Empty implementation - - - - - - Preform acknowledge (which should prevent the - message from process again by the consumer) - - - - - - Cancel acknowledge (will happen on error in order to avoid ack on succeed) - - - - - Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously. - - - A task that represents the asynchronous dispose operation. - - - - - Disposable scope - - - - - - Initializes a new instance. - - The ack. - - - - Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously. - - - A task that represents the asynchronous dispose operation. - - - - - Automatic acknowledge when execute without exception. - - - - - Automatic acknowledge when execute complete (whether it succeed or having exception, like finaly) . - - - - - Ignored expected to be handle elsewhere. - - - - - Preform acknowledge (which should prevent the - message from process again by the consumer) - - - - - - Initializes a new instance. - - The ack. - The cancel. - The behavior. - The logger. - - - - Preform acknowledge (which should prevent the - message from process again by the consumer) - - - - - - Cancel acknowledge (will happen on error in order to avoid ack on succeed) - - - - - - Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously. - - - A task that represents the asynchronous dispose operation. - - - - - - Preform acknowledge (which should prevent the - message from process again by the consumer) - - - - - - Preform acknowledge (which should prevent the - message from process again by the consumer) - - - - - Cancel acknowledge (will happen on error in order to avoid ack on succeed) - - - - - - Event Source producer builder. - - - - - Include the environment as prefix of the stream key. - for example: production:partition-name:shard-name - - The environment. - - - - - Event Source producer builder. - - - - - Register raw interceptor. - Intercept the consumer side execution before de-serialization. - - - Specify the event source shard. - Shard is a unique source name - which used for direct message channeling (routing). - - - - - Register tag's channels, enable the consumer - to get data from multiple sources (shards). - For example: assuming that each order flow is written to - unique source (shard). - Register to ORDER tag will route all shards which holding - messages with ORDER tag to the consume. - - - - - - - Register tag's channels, enable the consumer - to get data from multiple sources (shards). - For example: assuming that each order flow is written to - unique source (shard). - Register to ORDER tag will route all shards which holding - messages with ORDER tag to the consume. - - - - - - - Event Source producer builder. - - - - - Withes the cancellation token. - - The cancellation. - - - - - Register raw interceptor. - Intercept the consumer side execution before de-serialization. - - The interceptor data as the interceptor defined in the producer stage. - - - - - Register raw interceptor. - Intercept the consumer side execution before de-serialization. - - The interceptor data as the interceptor defined in the producer stage. - - - - - Responsible of building instance from segmented data. - Segmented data is how the producer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - - The segmentation strategy. - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Responsible of building instance from segmented data. - Segmented data is how the producer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - - The segmentation strategy. - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Enable configuration. - - - - - Tune configuration. - - The options strategy. - - - - - Event Source producer builder. - - - - - Partition key represent logical group of - event source shards. - For example assuming each ORDERING flow can have its - own messaging sequence, yet can live concurrency with - other ORDER's sequences. - The partition will let consumer the option to be notify and - consume multiple shards from single consumer. - This way the consumer can handle all orders in - central place without affecting sequence of specific order - flow or limiting the throughput. - - The partition key. - - - - - Event Source producer builder. - - - - - Shard key represent physical sequence. - On the consumer side shard is optional - for listening on a physical source rather on the entire partition. - Use same shard when order is matter. - For example: assuming each ORDERING flow can have its - own messaging sequence, in this case you can split each - ORDER into different shard and gain performance bust.. - - The shard key. - - - - - Event Source producer builder. - - - - - Build receiver (on demand data query). - - - - - - Subscribe consumer. - - Per operation invocation handler, handle methods calls. - - Remove subscription. - keeping the disposable will prevent the consumer to be collected - by th GC (when the behavior don't indicate to hook it until cancellation or dispose). - - - - - Subscribe consumer. - - Per operation invocation handler, handle methods calls. - Consumer Group allow a group of clients to cooperate - consuming a different portion of the same stream of messages - Optional Name of the consumer. - Can use for observability. - - Remove subscription. - keeping the disposable will prevent the consumer to be collected - by th GC (when the behavior don't indicate to hook it until cancellation or dispose). - - - - - Subscribe consumer. - - Per operation invocation handler, handle methods calls. - Consumer Group allow a group of clients to cooperate - consuming a different portion of the same stream of messages - Optional Name of the consumer. - Can use for observability. - - Remove subscription. - keeping the disposable will prevent the consumer to be collected - by th GC (when the behavior don't indicate to hook it until cancellation or dispose). - - - - - Subscribe consumer. - - Per operation invocation handler, handle methods calls. - - Remove subscription. - keeping the disposable will prevent the consumer to be collected - by th GC (when the behavior don't indicate to hook it until cancellation or dispose). - - - - - Subscribe consumer. - - Per operation invocation handler, handle methods calls. - Consumer Group allow a group of clients to cooperate - consuming a different portion of the same stream of messages - Optional Name of the consumer. - Can use for observability. - - Remove subscription. - keeping the disposable will prevent the consumer to be collected - by th GC (when the behavior don't indicate to hook it until cancellation or dispose). - - - - - Subscribe consumer. - - Per operation invocation handler, handle methods calls. - Consumer Group allow a group of clients to cooperate - consuming a different portion of the same stream of messages - Optional Name of the consumer. - Can use for observability. - - Remove subscription. - keeping the disposable will prevent the consumer to be collected - by th GC (when the behavior don't indicate to hook it until cancellation or dispose). - - - - - Bridge segmentation - - - - - Initializes a new instance. - - The synchronize. - - - - Unique name which represent the correlation - between the consumer and consumer interceptor. - It's recommended to use URL format. - - - - - Bridge segmentation - - - - - Initializes a new instance. - - The synchronize. - - - - Unclassify segmented data into an instance. - Segments is how the producer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - - - The segments which was collect so far. - It start as Empty and flow though all the registered segmentation strategies. - The operation's key which represent the method call at the - producer proxy. - This way you can segment same type into different slot. - Name of the argument. - The options. - - Materialization of the segments. - - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Event Source producer builder. - - - - - Attach logger. - - The logger. - - - - - Set resilience policy - - The policy. - - - - - Receive data (on demand data query). - - - - - Gets data by id. - - The entry identifier. - The cancellation token. - - - - - Gets data by id. - - The entry identifier. - The cancellation token. - - - - - Responsible to load information from storage. - The information can be either Segmentation or Interception. - When adding it via the builder it can be arrange in a chain in order of having - 'Chain of Responsibility' for saving different parts into different storage (For example GDPR's PII). - Alternative, chain can serve as a cache layer. - - - - - Determines whether is of the right target type. - - The type. - - - - Enable configuration. - - - - - Adds the storage strategy (Segment / Interceptions). - Will use default storage (REDIS Hash) when empty. - When adding more than one it will to all, act as a fall-back (first win, can use for caching). - It important the consumer's storage will be in sync with this setting. - - Storage strategy provider. - Type of the target. - - - - - Event Source Consumer builder. - - - - - Choose the communication channel provider. - - The channel provider. - - - - - Represent the consuming completion.. - - - - - The actual concrete plan - - - - - Gets a communication channel provider factory. - - - - - Gets the storage strategies. - - - - - Common plan properties - - - - - Environment (part of the stream key). - - - The partition. - - - - - Partition key represent logical group of - event source shards. - For example assuming each ORDERING flow can have its - own messaging sequence, yet can live concurrency with - other ORDER's sequences. - The partition will let consumer the option to be notify and - consume multiple shards from single consumer. - This way the consumer can handle all orders in - central place without affecting sequence of specific order - flow or limiting the throughput. - - - The partition. - - - - - Shard key represent physical sequence. - Use same shard when order is matter. - For example: assuming each ORDERING flow can have its - own messaging sequence, in this case you can split each - ORDER into different shard and gain performance bust.. - - - - - Gets the consumer group. - Consumer Group allow a group of clients to cooperate - consuming a different portion of the same stream of messages - - - - - Optional Name of the consumer. - Can use for observability. - - - - - Gets the cancellation token. - - - - - Consumer interceptors (Timing: after serialization). - - - The interceptors. - - - - - Gets the logger. - - - - - Gets the configuration. - - - - - Segmentation responsible of splitting an instance into segments. - Segments is how the Consumer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Attach the shard. - - The shard. - - - - - Gets or sets the invocation resilience policy. - - - - - Metadata extensions - - - - - Gets the partition:shard as key. - - - - - Building phase of the plan - - - - - Gets a communication channel provider factory. - - - - - Gets the storage strategy factories. - - - - - Builds the plan. - - - - - - Represent metadata of message (command / event) metadata of - a communication channel (Pub/Sub, Event Source, REST, GraphQL). - It represent the operation's intent or represent event. - - - - - Get the metadata context - - - - - Initializes a new instance. - - The metadata. - The consuming cancellation - (stop consuming call-back on cancellation). - - - - Gets the metadata. - - - - - Cancel the consuming process. - - - - - Performs an implicit conversion. - - The instance. - - The result of the conversion. - - - - - Gets the max batch size of reading messages per shard. - The framework won't proceed to the next batch until all messages - in the batch complete (or timeout when it set to acknowledge on timeout). - - - - - Gets a value indicating whether to prevent the consumer - from being collect by the GC. - True by default, when you hold the subscription disposable - you can set it to false. as long as you keeping the disposable in - object that isn't candidate for being collected the consumer will stay alive. - - - true if [keep alive]; otherwise, false. - - - - - Gets the acknowledge behavior. - - - - - Gets the maximum messages to consume before detaching the subscription. - any number > 0 will activate this mechanism. - - - - - Gets the threshold duration which limit considering producer call as parent, - Beyond this period the consumer span will refer the producer as Link (rather than parent). - - - - - Base class for the consumer's code generator - - - - - Get parameter value from the announcement. - - The type of the parameter. - The argument. - Name of the argument. - The plan. - - - - - - Gets the parameter value. - - The type of the parameter. - The argument. - Name of the argument. - - - - - Consumer stage of an interception operation provider. - It can be use for variety of responsibilities like - flowing auth context or traces, producing metrics, etc. - - - - - - Interception operation. - - The metadata. - - The interceptor data which sets on the - producer stage of the interception. - - - - Consumer stage of an interception operation provider. - It can be use for variety of responsibilities like - flowing auth context or traces, producing metrics, etc. - - - - - - Interception operation. - - The metadata. - - The interceptor data which sets on the - producer stage of the interception. - - - - Subscription Bridge convention - - - - - Bridges to the subscriber implementation. - - The announcement. - The consumer bridge. - - - - - Channel provider responsible for passing the actual message - from producer to consumer. - - - - - Subscribe to the channel for specific metadata. - - The consumer plan. - The function. - The options. - The cancellation token. - - When completed - - - - - Gets the by identifier asynchronous. - - The entry identifier. - The plan. - The cancellation token. - - Size of the read buffer. - How many items to read on a single iteration. - - - - - - Responsible to load information from storage. - The information can be either Segmentation or Interception. - When adding it via the builder it can be arrange in a chain in order of having - 'Chain of Responsibility' for saving different parts into different storage (For example GDPR's PII). - Alternative, chain can serve as a cache layer. - - - - - Load the bucket information. - - The meta fetch provider. - The current bucket (previous item in the chain). - The type of the storage. - The get property. - The cancellation. - - Either Segments or Interceptions. - - - - - Responsible of building instance from segmented data. - Segmented data is how the producer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Unclassify segmented data into an instance. - Segments is how the producer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - - The segments which was collect so far. - It start as Empty and flow though all the registered segmentation strategies. - The operation's key which represent the method call at the - producer proxy. - This way you can segment same type into different slot. - Name of the argument. - The options. - - Materialization of the segments. - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Responsible of building instance from segmented data. - Segmented data is how the producer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Unclassify segmented data into an instance. - Segments is how the producer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - - The segments which was collect so far. - It start as Empty and flow though all the registered segmentation strategies. - The operation's key which represent the method call at the - producer proxy. - This way you can segment same type into different slot. - Name of the argument. - The options. - - Materialization of the segments. - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Extract telemetry span's parent info - - - The meta. - The entries for extraction. - The injection strategy. - - - - diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/icon.png b/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/icon.png deleted file mode 100644 index d6811ad8..00000000 Binary files a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/icon.png and /dev/null differ diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers/Weknow.EventSource.Backbone.xml b/Consumers/Weknow.EventSource.Backbone.Consumers/Weknow.EventSource.Backbone.xml deleted file mode 100644 index 958c4594..00000000 --- a/Consumers/Weknow.EventSource.Backbone.Consumers/Weknow.EventSource.Backbone.xml +++ /dev/null @@ -1,622 +0,0 @@ - - - - Weknow.EventSource.Backbone.Consumers - - - - - Base class for the consumer's code generator - - - - - Initializes a new instance. - - The plan. - Per method handler, handle methods calls. - - - - Initializes a new instance. - - The plan. - Per method handler, handle methods calls. - - - - Subscribes this instance. - - - - - - Represent single consuming subscription - - - - - Initializes a new subscription. - - The plan. - Per operation invocation handler, handle methods calls. - - - - Get parameter value from the announcement. - - The type of the parameter. - The argument. - Name of the argument. - - - - - - Represent the consuming completion.. - - - - - Handles consuming of single event. - - The argument. - - The acknowledge callback which will prevent message from - being re-fetch from same consumer group. - - - - - Release consumer. - - - A task that represents the asynchronous dispose operation. - - - - - Event Source consumer builder. - - - - - Event Source consumer builder. - - - - - Prevents a default instance of the class from being created. - - - - - Initializes a new instance. - - The plan. - - - - Choose the communication channel provider. - - The channel provider. - - - - - Adds the storage strategy (Segment / Interceptions). - Will use default storage (REDIS Hash) when empty. - When adding more than one it will to all, act as a fall-back (first win, can use for caching). - It important the consumer's storage will be in sync with this setting. - - Storage strategy provider. - Type of the target. - - - - - Attach configuration. - - - - - - - Include the environment as prefix of the stream key. - for example: production:partition-name:shard-name - - The environment. - - - - - Partition key represent logical group of - event source shards. - For example assuming each ORDERING flow can have its - own messaging sequence, yet can live concurrency with - other ORDER's sequences. - The partition will let consumer the option to be notify and - consume multiple shards from single consumer. - This way the consumer can handle all orders in - central place without affecting sequence of specific order - flow or limiting the throughput. - - The partition key. - - - - - Shard key represent physical sequence. - On the consumer side shard is optional - for listening on a physical source rather on the entire partition. - Use same shard when order is matter. - For example: assuming each ORDERING flow can have its - own messaging sequence, in this case you can split each - ORDER into different shard and gain performance bust.. - - The shard key. - - - - - - Responsible of building instance from segmented data. - Segmented data is how the producer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - - The segmentation strategy. - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Responsible of building instance from segmented data. - Segmented data is how the producer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - - The segmentation strategy. - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Registers the interceptor. - - The interceptor. - - - - - Registers the interceptor. - - The interceptor. - - - - - Attach logger. - - The logger. - - - - - Set resilience policy - - The policy. - - - - - Subscribe consumer. - - Per operation invocation handler, handle methods calls. - - The partition subscription (dispose to remove the subscription) - - _plan - - - - Subscribe consumer. - - Per operation invocation handler, handle methods calls. - Consumer Group allow a group of clients to cooperate - consuming a different portion of the same stream of messages - Optional Name of the consumer. - Can use for observability. - - The partition subscription (dispose to remove the subscription) - - _plan - - - - Subscribe consumer. - - Per operation invocation handler, handle methods calls. - Consumer Group allow a group of clients to cooperate - consuming a different portion of the same stream of messages - Optional Name of the consumer. - Can use for observability. - - The partition subscription (dispose to remove the subscription) - - _plan - - - - Subscribe consumer. - - Per operation invocation handler, handle methods calls. - - The partition subscription (dispose to remove the subscription) - - _plan - - - - Subscribe consumer. - - Per operation invocation handler, handle methods calls. - Consumer Group allow a group of clients to cooperate - consuming a different portion of the same stream of messages - Optional Name of the consumer. - Can use for observability. - - The partition subscription (dispose to remove the subscription) - - _plan - - - - Subscribe consumer. - - Per operation invocation handler, handle methods calls. - Consumer Group allow a group of clients to cooperate - consuming a different portion of the same stream of messages - Optional Name of the consumer. - Can use for observability. - - The partition subscription (dispose to remove the subscription) - - _plan - - - - Withes the cancellation token. - - The cancellation. - - - - - Build receiver (on demand data query). - - - - - - Initializes a new instance. - - The plan. - - - - Gets the asynchronous. - - The entry identifier. - The cancellation token. - - - - - Gets the asynchronous. - - The entry identifier. - The cancellation token. - - - - - Hold builder definitions. - Define the consumer execution pipeline. - - - - - Initializes a new instance. - - - - - Initializes a new instance. - - The copy from. - The channel. - The channel. - The environment. - The partition. - The shard. - The logger. - The options. - The segmentation strategies. - The interceptors. - The routes. - The cancellation token. - Consumer Group allow a group of clients to cooperate - consuming a different portion of the same stream of messages - Optional Name of the consumer. - Can use for observability. - The resilience policy. - The storage strategy. - - - - Gets the communication channel provider. - - - - - Gets the communication channel provider. - - - - - Gets the storage strategy. - - - - - Gets the storage strategies. - - - - - Gets the logger. - - - - - Environment (part of the stream key) - - - - - Partition key represent logical group of - event source shards. - For example assuming each ORDERING flow can have its - own messaging sequence, yet can live concurrency with - other ORDER's sequences. - The partition will let consumer the option to be notify and - consume multiple shards from single consumer. - This way the consumer can handle all orders in - central place without affecting sequence of specific order - flow or limiting the throughput. - - - - - Shard key represent physical sequence. - Use same shard when order is matter. - For example: assuming each ORDERING flow can have its - own messaging sequence, in this case you can split each - ORDER into different shard and gain performance bust.. - - - - - Gets the configuration. - - - - - Segmentation responsible of splitting an instance into segments. - Segments is how the Consumer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Consumer interceptors (Timing: after serialization). - - - The interceptors. - - - - - Gets the cancellation token. - - - - - Routes are sub-pipelines are results of merge operation - which can split same payload into multiple partitions or shards. - - - - - Gets the consumer group. - Consumer Group allow a group of clients to cooperate - consuming a different portion of the same stream of messages - - - - - Optional Name of the consumer. - Can use for observability. - - - - - Gets or sets the invocation resilience policy. - - - - - Attach the channel. - - The channel. - - - - - Attach the Storage Strategy. - - The storage strategy. - - - - - Attach the LOGGER. - - The logger. - - - - - Attach the cancellation. - - The cancellation. - - - - - Attach the options. - - The options. - - - - - Attach the environment. - - The environment. - - - - - Attach the partition. - - The partition. - - - - - Attach the shard. - - The shard. - - - - - Attach the shard. - - The shard. - - - - - Set resilience policy - - The policy. - - - - - Adds the route. - - The route. - - - - - Adds the segmentation. - - The segmentation. - - - - - Adds the interceptor. - - The interceptor. - - - - - Set Consumer Group which allow a group of clients to cooperate - consuming a different portion of the same stream of messages - - - Consumer Group allow a group of clients to cooperate - consuming a different portion of the same stream of messages - - - Optional Name of the consumer. - Can use for observability. - - - - - - Builds this instance. - - - - - - Wrap Channel Storage with key filtering of the bucket. - Useful for 'Chain of Responsibility' by saving different parts - into different storage (For example GDPR's PII). - - - - - Initializes a new instance. - - The actual storage provider. - Type of the target. - - - - Determines whether is of the right target type. - - The type. - - - - - Load the bucket information. - - The meta fetch provider. - The current bucket (previous item in the chain). - The type of the storage. - The get property. - The cancellation. - - Either Segments or Interceptions. - - - - diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers/icon.png b/Consumers/Weknow.EventSource.Backbone.Consumers/icon.png deleted file mode 100644 index d6811ad8..00000000 Binary files a/Consumers/Weknow.EventSource.Backbone.Consumers/icon.png and /dev/null differ diff --git a/Directory.Build.props b/Directory.Build.props index 528eac17..364fb105 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,13 @@ - 1.2.18 + 1.2.118 + # 1.2.85: + Breaking changes: S3Strategy was renamed to S3Storage + # 1.2.96 + Breaking changes: Method on the consumer interface generated with first parameter of type ConsumerMetadata + # 1.2.115 + Breaking changes: registration extensions was re-module @@ -16,9 +22,9 @@ true - event-source, pub, sub, pubsub, messaging, reliable, redis, weknow, bnaya - https://medium.com/weknow-network - https://github.com/weknow-network/Event-Source-Backbone + event-source, event-driven, event-sourcing, producer, consumer, pub, sub, pub-sub, messaging, reliable, redis, bnaya + https://medium.com/@bnayae + https://github.com/bnayae/Event-Source-Backbone GitHub true MIT @@ -33,23 +39,13 @@ True - - - 1.1.276: [BREAKING CHANGES] Consumer options / consumer channel setting had changed - 1.2.19: - Support Inheritance - [BREAKING CHANGES] Renaming of Enum - - - - true $(NoWarn);1591 - + diff --git a/Weknow.EventSource.Backbone.Contracts/Attributes/EventSourceVersion.cs b/EventSourcing.Backbone.Abstractions/Attributes/EventSourceVersion.cs similarity index 85% rename from Weknow.EventSource.Backbone.Contracts/Attributes/EventSourceVersion.cs rename to EventSourcing.Backbone.Abstractions/Attributes/EventSourceVersion.cs index 57c733ab..45b847a8 100644 --- a/Weknow.EventSource.Backbone.Contracts/Attributes/EventSourceVersion.cs +++ b/EventSourcing.Backbone.Abstractions/Attributes/EventSourceVersion.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Event source's version control. @@ -8,12 +8,11 @@ /// We don't expect a gap between ConsumeFrom to a lower version. /// Versions expect to start at 0, if no version specified It will consider version 0, /// - /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class EventSourceVersionAttribute : Attribute { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The version which will be produce. public EventSourceVersionAttribute(ushort version) diff --git a/Weknow.EventSource.Backbone.Contracts/Attributes/GenerateEventSourceBaseAttribute.cs b/EventSourcing.Backbone.Abstractions/Attributes/EventsContractAttribute.cs similarity index 60% rename from Weknow.EventSource.Backbone.Contracts/Attributes/GenerateEventSourceBaseAttribute.cs rename to EventSourcing.Backbone.Abstractions/Attributes/EventsContractAttribute.cs index 8a06217c..3c4de4c0 100644 --- a/Weknow.EventSource.Backbone.Contracts/Attributes/GenerateEventSourceBaseAttribute.cs +++ b/EventSourcing.Backbone.Abstractions/Attributes/EventsContractAttribute.cs @@ -1,20 +1,17 @@ -using System.ComponentModel; - -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Mark for code generation /// /// - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - [EditorBrowsable(EditorBrowsableState.Never)] - public class GenerateEventSourceBaseAttribute : Attribute + [AttributeUsage(AttributeTargets.Interface, AllowMultiple = true)] + public class EventsContractAttribute : Attribute { /// - /// Initializes a new instance of the class. + /// Initializes a new instance. /// /// Type of the generate. - public GenerateEventSourceBaseAttribute(EventSourceGenType generateType) + public EventsContractAttribute(EventsContractType generateType) { Type = generateType; } @@ -33,6 +30,6 @@ public GenerateEventSourceBaseAttribute(EventSourceGenType generateType) /// /// Type of the generation /// - public EventSourceGenType Type { get; } + public EventsContractType Type { get; } } } diff --git a/Weknow.EventSource.Backbone.Contracts/Attributes/EventSourceGenType.cs b/EventSourcing.Backbone.Abstractions/Attributes/EventsContractType.cs similarity index 62% rename from Weknow.EventSource.Backbone.Contracts/Attributes/EventSourceGenType.cs rename to EventSourcing.Backbone.Abstractions/Attributes/EventsContractType.cs index 37839af1..8aba398f 100644 --- a/Weknow.EventSource.Backbone.Contracts/Attributes/EventSourceGenType.cs +++ b/EventSourcing.Backbone.Abstractions/Attributes/EventsContractType.cs @@ -1,9 +1,9 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Event Source Generation Type /// - public enum EventSourceGenType + public enum EventsContractType { Producer, Consumer diff --git a/Weknow.EventSource.Backbone.Contracts/Bucket.cs b/EventSourcing.Backbone.Abstractions/Bucket.cs similarity index 94% rename from Weknow.EventSource.Backbone.Contracts/Bucket.cs rename to EventSourcing.Backbone.Abstractions/Bucket.cs index 4d7b4e32..263266e5 100644 --- a/Weknow.EventSource.Backbone.Contracts/Bucket.cs +++ b/EventSourcing.Backbone.Abstractions/Bucket.cs @@ -3,9 +3,9 @@ using System.Text; using System.Text.Json; -using static Weknow.EventSource.Backbone.EventSourceConstants; +using static EventSourcing.Backbone.EventSourceConstants; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public class Bucket : IEnumerable>> { @@ -186,7 +186,15 @@ public bool ContainsKey(string key) /// public bool TryGetValue(string key, out ReadOnlyMemory value) { - return _data.TryGetValue(key, out value); + if (_data.TryGetValue(key, out value)) + return true; + if (_data.TryGetValue(key.ToPascal(), out value)) + return true; + if (_data.TryGetValue(key.ToCamel(), out value)) + return true; + if (_data.TryGetValue(key.ToDash(), out value)) + return true; + return _data.TryGetValue(key.ToLower(), out value); } #endregion // TryGetValue diff --git a/Weknow.EventSource.Backbone.Contracts/BucketJsonConverter.cs b/EventSourcing.Backbone.Abstractions/BucketJsonConverter.cs similarity index 95% rename from Weknow.EventSource.Backbone.Contracts/BucketJsonConverter.cs rename to EventSourcing.Backbone.Abstractions/BucketJsonConverter.cs index 271997a0..e028f9da 100644 --- a/Weknow.EventSource.Backbone.Contracts/BucketJsonConverter.cs +++ b/EventSourcing.Backbone.Abstractions/BucketJsonConverter.cs @@ -1,11 +1,11 @@ using System.Text.Json; using System.Text.Json.Serialization; -using static Weknow.EventSource.Backbone.EventSourceOptions; +using static EventSourcing.Backbone.EventSourceOptions; using BucketData = System.Collections.Immutable.ImmutableDictionary>; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Ack/Ack.cs b/EventSourcing.Backbone.Abstractions/Consumer/Ack/Ack.cs similarity index 85% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Ack/Ack.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Ack/Ack.cs index 480b586c..4e3c2392 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Ack/Ack.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Ack/Ack.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Acknowledge context @@ -49,7 +49,7 @@ public static IAsyncDisposable Set(IAck ack) /// /// Empty implementation /// - /// + /// private readonly struct NOP : IAck { public static readonly IAck Default = new NOP(); @@ -57,13 +57,17 @@ public static IAsyncDisposable Set(IAck ack) /// Preform acknowledge (which should prevent the /// message from process again by the consumer) /// + /// The cause of the acknowledge. /// - public ValueTask AckAsync() => ValueTask.CompletedTask; + public ValueTask AckAsync(AckBehavior cause) => ValueTask.CompletedTask; /// /// Cancel acknowledge (will happen on error in order to avoid ack on succeed) /// - public ValueTask CancelAsync() => ValueTask.CompletedTask; + /// The cause of the cancellation. + /// + /// Must be execute from a consuming scope (i.e. method call invoked by the consumer's event processing) + public ValueTask CancelAsync(AckBehavior cause) => ValueTask.CompletedTask; /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously. diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Ack/AckBehavior.cs b/EventSourcing.Backbone.Abstractions/Consumer/Ack/AckBehavior.cs similarity index 91% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Ack/AckBehavior.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Ack/AckBehavior.cs index 90700101..ee84333e 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Ack/AckBehavior.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Ack/AckBehavior.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public enum AckBehavior { diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Ack/AckOnce.cs b/EventSourcing.Backbone.Abstractions/Consumer/Ack/AckOnce.cs similarity index 58% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Ack/AckOnce.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Ack/AckOnce.cs index e57c99ff..5c695b0a 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Ack/AckOnce.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Ack/AckOnce.cs @@ -1,6 +1,11 @@ -using Microsoft.Extensions.Logging; +using System; +using System.Diagnostics.Metrics; -namespace Weknow.EventSource.Backbone +using Microsoft.Extensions.Logging; + +using static EventSourcing.Backbone.Private.EventSourceTelemetry; + +namespace EventSourcing.Backbone { /// /// Preform acknowledge (which should prevent the @@ -9,28 +14,36 @@ namespace Weknow.EventSource.Backbone /// public class AckOnce : IAck { - private static readonly Func NON_FN = () => ValueTask.CompletedTask; - private readonly Func _ackAsync; - private readonly Func _cancelAsync; + private static readonly Func NON_FN = (_) => ValueTask.CompletedTask; + private readonly string _uri; + private readonly Func _ackAsync; + private readonly Func _cancelAsync; private readonly AckBehavior _behavior; private readonly ILogger _logger; private int _ackCount = 0; + private static readonly Counter AckCounter = EMeter.CreateCounter("evt-src.sys.consumer.event.ack", "count", + "Event's message handling acknowledge count"); + private static readonly Counter AbortCounter = EMeter.CreateCounter("evt-src.sys.consumer.event.abort", "count", + "Event's message handling aborted (cancel) count"); #region Ctor /// /// Initializes a new instance. /// + /// The URI. /// The ack. - /// The cancel. /// The behavior. /// The logger. + /// The cancel. public AckOnce( - Func ackAsync, + string uri, + Func ackAsync, AckBehavior behavior, ILogger logger, - Func? cancelAsync = null) + Func? cancelAsync = null) { + _uri = uri; _ackAsync = ackAsync; _cancelAsync = cancelAsync ?? NON_FN; _behavior = behavior; @@ -42,18 +55,20 @@ public AckOnce( #region AckAsync /// - /// Preform acknowledge (which should prevent the + /// Preform acknowledge (which should prevent the /// message from process again by the consumer) /// + /// The cause of the acknowledge. /// - public async ValueTask AckAsync() + public async ValueTask AckAsync(AckBehavior cause) { int count = Interlocked.Increment(ref _ackCount); + if (count != 1) + return; try { - if (count == 1) - await _ackAsync(); - + AckCounter.WithTag("URI", _uri).WithTag("cause", cause).Add(1); + await _ackAsync(cause); } catch (Exception ex) { @@ -71,15 +86,18 @@ public async ValueTask AckAsync() /// /// Cancel acknowledge (will happen on error in order to avoid ack on succeed) /// + /// The cause of the cancellation. /// - public async ValueTask CancelAsync() + /// Must be execute from a consuming scope (i.e. method call invoked by the consumer's event processing) + public async ValueTask CancelAsync(AckBehavior cause) { int count = Interlocked.Increment(ref _ackCount); + if (count != 1) + return; try { - if (count == 1) - await _cancelAsync(); - + AbortCounter.WithTag("URI", _uri).WithTag("cause", cause).Add(1); + await _cancelAsync(cause); } catch (Exception ex) { @@ -105,7 +123,7 @@ public async ValueTask DisposeAsync() { if (_behavior == AckBehavior.OnSucceed) { - await AckAsync(); + await AckAsync(AckBehavior.OnSucceed); } } diff --git a/EventSourcing.Backbone.Abstractions/Consumer/Ack/IAck.cs b/EventSourcing.Backbone.Abstractions/Consumer/Ack/IAck.cs new file mode 100644 index 00000000..f2d3c4ee --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Consumer/Ack/IAck.cs @@ -0,0 +1,11 @@ +namespace EventSourcing.Backbone +{ + /// + /// Preform acknowledge (which should prevent the + /// message from process again by the consumer) + /// + /// + public interface IAck : IAckOperations, IAsyncDisposable + { + } +} diff --git a/EventSourcing.Backbone.Abstractions/Consumer/Ack/IAckOperations.cs b/EventSourcing.Backbone.Abstractions/Consumer/Ack/IAckOperations.cs new file mode 100644 index 00000000..d2144895 --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Consumer/Ack/IAckOperations.cs @@ -0,0 +1,27 @@ +namespace EventSourcing.Backbone +{ + /// + /// Preform acknowledge (which should prevent the + /// message from process again by the consumer) + /// + /// + public interface IAckOperations + { + /// + /// Preform acknowledge (which should prevent the + /// message from process again by the consumer). + /// Must be execute from a consuming scope (i.e. method call invoked by the consumer's event processing) + /// + /// The cause of the acknowledge. + /// + ValueTask AckAsync(AckBehavior cause = AckBehavior.Manual); + + /// + /// Cancel acknowledge (will happen on error in order to avoid ack on succeed) + /// + /// Must be execute from a consuming scope (i.e. method call invoked by the consumer's event processing) + /// The cause of the cancellation. + /// + ValueTask CancelAsync(AckBehavior cause = AckBehavior.Manual); + } +} diff --git a/EventSourcing.Backbone.Abstractions/Consumer/Attributes.cs b/EventSourcing.Backbone.Abstractions/Consumer/Attributes.cs new file mode 100644 index 00000000..4ffe53a9 --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Consumer/Attributes.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; + + +[assembly: InternalsVisibleToAttribute("EventSourcing.Backbone.Consumers")] + diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerFilterBuilder.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerFilterBuilder.cs similarity index 97% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerFilterBuilder.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerFilterBuilder.cs index 83a4dea5..25bb3cb6 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerFilterBuilder.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerFilterBuilder.cs @@ -1,6 +1,6 @@ using System.Collections; -namespace Weknow.EventSource.Backbone.Building +namespace EventSourcing.Backbone.Building { /// /// Event Source producer builder. diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerHooksBuilder.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerHooksBuilder.cs similarity index 62% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerHooksBuilder.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerHooksBuilder.cs index 6f4ac883..d763f4fe 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerHooksBuilder.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerHooksBuilder.cs @@ -1,6 +1,6 @@ -using Weknow.EventSource.Backbone.Building; +using EventSourcing.Backbone.Building; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Event Source producer builder. @@ -59,38 +59,5 @@ IConsumerHooksBuilder RegisterSegmentationStrategy( /// IConsumerHooksBuilder RegisterSegmentationStrategy( IConsumerAsyncSegmentationStrategy segmentationStrategy); - - //[Consumer(Partition="X", Shard="y")] - //public class ConsumerX : Consumer, ISequenceOperations - //{ - //} - //// void BuildAutoDiscover(); - //void Build(Func factory, string partition, string? shard = null); - //IEventSourceConsumer3Builder ForType( - // IConsumerSegmentationProvider segmentationProvider, - // params string[] intents); - - //IEventSourceConsumer3Builder ForType( - // Func>, - // IDataSerializer, - // T> segmentationProvider, - // params string[] intents); - - //IEventSourceConsumer3Builder ForType( - // IConsumerSegmentationProvider segmentationProvider, - // Func filter); - - //IEventSourceConsumer3Builder ForType( - // Func>, - // IDataSerializer, - // T> segmentationProvider, - // Func filter); - - ///// - ///// Builds consumer for non-specialized announcements. - ///// This is perfect for scenarios like storing backups in blobs like S3. - ///// - ///// - //ISourceBlock> BuildRaw(); } } diff --git a/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerIocStoreStrategyBuilder.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerIocStoreStrategyBuilder.cs new file mode 100644 index 00000000..a51462f9 --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerIocStoreStrategyBuilder.cs @@ -0,0 +1,14 @@ +namespace EventSourcing.Backbone.Building +{ + /// + /// storage configuration with IoC. + /// + /// + public interface IConsumerIocStoreStrategyBuilder : IConsumerStoreStrategyBuilder + { + /// + /// Gets the service provider. + /// + IServiceProvider ServiceProvider { get; } + } +} diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerOptionsBuilder.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerOptionsBuilder.cs similarity index 89% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerOptionsBuilder.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerOptionsBuilder.cs index d11395a6..51f5fd42 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerOptionsBuilder.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerOptionsBuilder.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.Building +namespace EventSourcing.Backbone.Building { /// /// Enable configuration. diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerReadyBuilder.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerReadyBuilder.cs similarity index 83% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerReadyBuilder.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerReadyBuilder.cs index b59ebcd9..aeed017b 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerReadyBuilder.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerReadyBuilder.cs @@ -1,10 +1,10 @@ -using Microsoft.Extensions.Logging; +using EventSourcing.Backbone.Building; -using Polly; +using Microsoft.Extensions.Logging; -using Weknow.EventSource.Backbone.Building; +using Polly; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Event Source producer builder. diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerStorageStrategyWithFilter.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerStorageStrategyWithFilter.cs similarity index 93% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerStorageStrategyWithFilter.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerStorageStrategyWithFilter.cs index 2f429a6d..bf894a19 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerStorageStrategyWithFilter.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerStorageStrategyWithFilter.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.Building +namespace EventSourcing.Backbone.Building { /// /// Responsible to load information from storage. diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerStoreStrategyBuilder.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerStoreStrategyBuilder.cs similarity index 92% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerStoreStrategyBuilder.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerStoreStrategyBuilder.cs index 550bf486..b67579b9 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerStoreStrategyBuilder.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerStoreStrategyBuilder.cs @@ -2,10 +2,10 @@ using Microsoft.Extensions.Logging; -namespace Weknow.EventSource.Backbone.Building +namespace EventSourcing.Backbone.Building { /// - /// Enable configuration. + /// storage configuration. /// public interface IConsumerStoreStrategyBuilder : IConsumerOptionsBuilder { diff --git a/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerSubscribeBuilder.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerSubscribeBuilder.cs new file mode 100644 index 00000000..b9ca2033 --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerSubscribeBuilder.cs @@ -0,0 +1,50 @@ +namespace EventSourcing.Backbone.Building +{ + + /// + /// Event Source producer builder. + /// + public interface IConsumerSubscribeBuilder : + IConsumerSubscribtionHubBuilder, + IConsumerEnvironmentOfBuilder, + IConsumerUriBuilder, + IWithCancellation + //IConsumerShardOfBuilder + { + /// + /// Tune configuration. + /// + /// The options strategy. + /// + IConsumerSubscribeBuilder WithOptions(Func optionsStrategy); + + /// + /// Consumer's group name. + /// + /// Alter the default consumer's group name. + /// + IConsumerSubscribeBuilder Group(string consumerGroup); + + /// + /// Set the consumer's name + /// + IConsumerSubscribeBuilder Name(string consumerName); + + /// + /// The routing information attached to this builder + /// + IPlanRoute Route { get; } + + /// + /// Build receiver (on demand data query). + /// + /// + IConsumerReceiver BuildReceiver(); + + /// + /// Build iterator (pull fusion). + /// + /// + IConsumerIterator BuildIterator(); + } +} diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerSubscribeBuilder.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerSubscribtionHubBuilder.cs similarity index 58% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerSubscribeBuilder.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerSubscribtionHubBuilder.cs index 23e5ec30..3299add5 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/IConsumerSubscribeBuilder.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/IConsumerSubscribtionHubBuilder.cs @@ -1,55 +1,18 @@ -namespace Weknow.EventSource.Backbone.Building +namespace EventSourcing.Backbone.Building { - - /// - /// Event Source producer builder. - /// - public interface IConsumerSubscribeBuilder : - IConsumerSubscribtionHubBuilder, - IConsumerEnvironmentOfBuilder, - IConsumerPartitionBuilder, - IWithCancellation - //IConsumerShardOfBuilder + public interface IConsumerSubscribtionHubBuilder { /// - /// Tune configuration. - /// - /// The options strategy. - /// - IConsumerSubscribeBuilder WithOptions(Func optionsStrategy); - - /// - /// Consumer's group name. - /// - /// Name of the group. - /// - IConsumerSubscribeBuilder Group(string consumerGroup); - - /// - /// Set the consumer's name - /// - IConsumerSubscribeBuilder Name(string consumerName); - - /// - /// The routing information attached to this builder - /// - IPlanRoute Route { get; } - - /// - /// Build receiver (on demand data query). - /// - /// - IConsumerReceiver BuildReceiver(); - - /// - /// Build iterator (pull fusion). + /// Subscribe consumer. /// - /// - IConsumerIterator BuildIterator(); - } + /// Per operation invocation handler, handle methods calls. + /// + /// Remove subscription. + /// keeping the disposable will prevent the consumer to be collected + /// by th GC (when the behavior don't indicate to hook it until cancellation or dispose). + /// + IConsumerLifetime Subscribe(ISubscriptionBridge handler); - public interface IConsumerSubscribtionHubBuilder - { /// /// Subscribe consumer. /// diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Receiver/ConsumerAsyncEnumerableJsonOptions.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Receiver/ConsumerAsyncEnumerableJsonOptions.cs similarity index 88% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Receiver/ConsumerAsyncEnumerableJsonOptions.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Receiver/ConsumerAsyncEnumerableJsonOptions.cs index 5e39ab94..af72ae3e 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Receiver/ConsumerAsyncEnumerableJsonOptions.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Receiver/ConsumerAsyncEnumerableJsonOptions.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Receive data (on demand data query). diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Receiver/ConsumerAsyncEnumerableOptions.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Receiver/ConsumerAsyncEnumerableOptions.cs similarity index 95% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Receiver/ConsumerAsyncEnumerableOptions.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Receiver/ConsumerAsyncEnumerableOptions.cs index e035855e..6eea06b0 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Receiver/ConsumerAsyncEnumerableOptions.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Receiver/ConsumerAsyncEnumerableOptions.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Receive data (on demand data query). diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Receiver/IConsumerIterator.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Receiver/IConsumerIterator.cs similarity index 86% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Receiver/IConsumerIterator.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Receiver/IConsumerIterator.cs index 5984d02c..22a7701a 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Receiver/IConsumerIterator.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Receiver/IConsumerIterator.cs @@ -1,13 +1,13 @@ -using Weknow.EventSource.Backbone.Building; +using EventSourcing.Backbone.Building; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Receive data (on demand data query). /// public interface IConsumerIterator : IConsumerEnvironmentOfBuilder, - IConsumerPartitionBuilder, + IConsumerUriBuilder, IConsumerIteratorCommands { /// diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Receiver/IConsumerIteratorCommands.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Receiver/IConsumerIteratorCommands.cs similarity index 97% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Receiver/IConsumerIteratorCommands.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Receiver/IConsumerIteratorCommands.cs index 0c4796f2..ea21ef24 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Receiver/IConsumerIteratorCommands.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Receiver/IConsumerIteratorCommands.cs @@ -1,6 +1,6 @@ using System.Text.Json; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Receive data (on demand data query). diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Receiver/IConsumerReceiver.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Receiver/IConsumerReceiver.cs similarity index 62% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Receiver/IConsumerReceiver.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Receiver/IConsumerReceiver.cs index 82f11784..cd4fdc5a 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Receiver/IConsumerReceiver.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Receiver/IConsumerReceiver.cs @@ -1,13 +1,13 @@ -using Weknow.EventSource.Backbone.Building; +using EventSourcing.Backbone.Building; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Receive data (on demand data query). /// public interface IConsumerReceiver : IConsumerEnvironmentOfBuilder, - IConsumerPartitionBuilder, + IConsumerUriBuilder, IConsumerReceiverCommands { } diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Receiver/IConsumerReceiverCommands.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Receiver/IConsumerReceiverCommands.cs similarity index 96% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Receiver/IConsumerReceiverCommands.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Receiver/IConsumerReceiverCommands.cs index 7f3d1a73..365c9e3e 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/Building/Receiver/IConsumerReceiverCommands.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Receiver/IConsumerReceiverCommands.cs @@ -1,6 +1,6 @@ using System.Text.Json; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Receive data (on demand data query). diff --git a/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Route/IConsumerEnvironmentBuilder.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Route/IConsumerEnvironmentBuilder.cs new file mode 100644 index 00000000..e8a34114 --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Route/IConsumerEnvironmentBuilder.cs @@ -0,0 +1,11 @@ +namespace EventSourcing.Backbone.Building +{ + /// + /// Event Source producer builder. + /// + public interface IConsumerEnvironmentBuilder : + IConsumerUriBuilder, + IConsumerEnvironmentOfBuilder> + { + } +} diff --git a/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Route/IConsumerEnvironmentOfBuilder.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Route/IConsumerEnvironmentOfBuilder.cs new file mode 100644 index 00000000..c0e707ae --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Route/IConsumerEnvironmentOfBuilder.cs @@ -0,0 +1,23 @@ +namespace EventSourcing.Backbone.Building +{ + /// + /// Event Source producer builder. + /// + public interface IConsumerEnvironmentOfBuilder + { + /// + /// Include the environment as prefix of the stream key. + /// for example: env:URI + /// + /// The environment (null: keep current environment, empty: reset the environment to nothing). + /// + T Environment(Env? environment); + + /// + /// Fetch the origin environment of the message from an environment variable. + /// + /// The environment variable key. + /// + T EnvironmentFromVariable(string environmentVariableKey = "ASPNETCORE_ENVIRONMENT"); + } +} diff --git a/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Route/IConsumerUriBuilder.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Route/IConsumerUriBuilder.cs new file mode 100644 index 00000000..223b4fcb --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/Building/Route/IConsumerUriBuilder.cs @@ -0,0 +1,17 @@ +namespace EventSourcing.Backbone.Building +{ + + /// + /// Event Source producer builder. + /// + /// + public interface IConsumerUriBuilder + { + /// + /// The stream identifier (the URI combined with the environment separate one stream from another) + /// + /// The URI. + /// + T Uri(string uri); + } +} diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/IConsumer.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/IConsumer.cs similarity index 84% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/IConsumer.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Builder/IConsumer.cs index cb1df579..e3bdd001 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/IConsumer.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/IConsumer.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// diff --git a/EventSourcing.Backbone.Abstractions/Consumer/Builder/IConsumerBuilder.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/IConsumerBuilder.cs new file mode 100644 index 00000000..44cb58b9 --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/IConsumerBuilder.cs @@ -0,0 +1,27 @@ +using EventSourcing.Backbone.Building; + +using Microsoft.Extensions.Logging; + +namespace EventSourcing.Backbone +{ + /// + /// Event Source Consumer builder. + /// + public interface IConsumerBuilder + { + /// + /// Choose the communication channel provider. + /// + /// The channel provider. + /// + IConsumerStoreStrategyBuilder UseChannel(Func channel); + + /// + /// Choose the communication channel provider. + /// + /// Dependency injection provider. + /// The channel provider. + /// + IConsumerIocStoreStrategyBuilder UseChannel(IServiceProvider serviceProvider, Func channel); + } +} diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/IConsumerLifetime.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/IConsumerLifetime.cs similarity index 73% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/IConsumerLifetime.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Builder/IConsumerLifetime.cs index 2d894a60..8d9a2ee7 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/IConsumerLifetime.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/IConsumerLifetime.cs @@ -1,6 +1,6 @@ -using Weknow.EventSource.Backbone.Building; +using EventSourcing.Backbone.Building; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public interface IConsumerLifetime : IConsumerSubscribtionHubBuilder, IAsyncDisposable { diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/IConsumerPlan.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/IConsumerPlan.cs similarity index 85% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/IConsumerPlan.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Builder/IConsumerPlan.cs index a0315913..686b16d2 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/IConsumerPlan.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/IConsumerPlan.cs @@ -1,8 +1,8 @@ using System.Collections.Immutable; -using Weknow.EventSource.Backbone.Building; +using EventSourcing.Backbone.Building; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// @@ -22,11 +22,11 @@ public interface IConsumerPlan : IConsumerPlanBase IConsumerPlan ChangeEnvironment(Env? environment); /// - /// change the partition. + /// change the stream's name (identity). /// - /// The partition. + /// The URI. /// An IConsumerPlan. - IConsumerPlan ChangePartition(Env? partition); + IConsumerPlan ChangeKey(string? uri); /// /// Gets the storage strategies. diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/IConsumerPlanBase.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/IConsumerPlanBase.cs similarity index 80% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/IConsumerPlanBase.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Builder/IConsumerPlanBase.cs index 79d7dd36..b69dc1eb 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/IConsumerPlanBase.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/IConsumerPlanBase.cs @@ -4,7 +4,7 @@ using Polly; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// @@ -56,13 +56,6 @@ public interface IConsumerPlanBase : IPlanRoute /// IImmutableList SegmentationStrategies { get; } - /// - /// Attach the shard. - /// - /// The shard. - /// - IConsumerPlan WithShard(string shard); - /// /// Gets or sets the invocation resilience policy. /// @@ -74,21 +67,21 @@ public interface IConsumerPlanBase : IPlanRoute /// public static class IConsumerPlanBaseExtensions { - #region Key + #region FullUri /// - /// Gets the partition:shard as key. + /// The stream's full identifier which is a combination of the URI and the environment /// - public static string Key(this IConsumerPlanBase meta, char separator = ':') + public static string FullUri(this IConsumerPlanBase meta, char separator = ':') { if (string.IsNullOrEmpty(meta.Environment)) - return $"{meta.Partition}{separator}{meta.Shard}"; + return meta.Uri; Env env = meta.Environment; string envFormatted = env.Format(); - return $"{envFormatted}{separator}{meta.Partition}{separator}{meta.Shard}"; + return $"{envFormatted}{separator}{meta.Uri}"; } - #endregion // Key + #endregion // FullUri } } \ No newline at end of file diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/IConsumerPlanBuilder.cs b/EventSourcing.Backbone.Abstractions/Consumer/Builder/IConsumerPlanBuilder.cs similarity index 90% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/IConsumerPlanBuilder.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Builder/IConsumerPlanBuilder.cs index 9bb697ca..b77c898d 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Builder/IConsumerPlanBuilder.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Builder/IConsumerPlanBuilder.cs @@ -1,10 +1,10 @@ using System.Collections.Immutable; -using Microsoft.Extensions.Logging; +using EventSourcing.Backbone.Building; -using Weknow.EventSource.Backbone.Building; +using Microsoft.Extensions.Logging; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Building phase of the plan diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/ClaimingTrigger.cs b/EventSourcing.Backbone.Abstractions/Consumer/ClaimingTrigger.cs similarity index 96% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/ClaimingTrigger.cs rename to EventSourcing.Backbone.Abstractions/Consumer/ClaimingTrigger.cs index 3d0cc2d7..2f78886c 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/ClaimingTrigger.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/ClaimingTrigger.cs @@ -1,6 +1,6 @@ // TODO: [bnaya 2021-02] use Record -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Define when to claim stale (long waiting) messages from other consumers diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/ConsumerMetadata.cs b/EventSourcing.Backbone.Abstractions/Consumer/ConsumerMetadata.cs similarity index 54% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/ConsumerMetadata.cs rename to EventSourcing.Backbone.Abstractions/Consumer/ConsumerMetadata.cs index 4a1f4b7b..266c14d7 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/ConsumerMetadata.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/ConsumerMetadata.cs @@ -1,21 +1,25 @@ using System.Diagnostics; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Represent metadata of message (command / event) metadata of /// a communication channel (Pub/Sub, Event Source, REST, GraphQL). /// It represent the operation's intent or represent event. /// - [DebuggerDisplay("{Metadata.Partition}/{Metadata.Shard} [{Metadata.MessageId} at {Metadata.ProducedAt}]")] - public sealed class ConsumerMetadata + [DebuggerDisplay("{Metadata.Uri} [{Metadata.MessageId} at {Metadata.ProducedAt}]")] + public sealed class ConsumerMetadata : IAckOperations { internal static readonly AsyncLocal _metaContext = new AsyncLocal(); /// /// Get the metadata context /// - public static ConsumerMetadata? Context => _metaContext.Value; + public static ConsumerMetadata Context => _metaContext.Value ?? throw new EventSourcingException( + """ + Consumer metadata doesn't available on the current context + (make sure you try to consume it within a scope of a consuming method call)"); + """); #region Ctor @@ -68,5 +72,31 @@ public static implicit operator Metadata(ConsumerMetadata? instance) } #endregion // Cast overloads + + #region AckAsync + + /// + /// Preform acknowledge (which should prevent the + /// message from process again by the consumer) + /// Must be execute from a consuming scope (i.e. method call invoked by the consumer's event processing). + /// + /// The cause of the acknowledge. + /// + public ValueTask AckAsync(AckBehavior cause = AckBehavior.Manual) => Ack.Current.AckAsync(cause); + + #endregion // AckAsync + + #region AckAsync + + /// + /// Cancel acknowledge (will happen on error in order to avoid ack on succeed) + /// + /// The cause of the cancellation. + /// + /// Must be execute from a consuming scope (i.e. method call invoked by the consumer's event processing). + public ValueTask CancelAsync(AckBehavior cause = AckBehavior.Manual) => Ack.Current.CancelAsync(cause); + + #endregion // AckAsync + } } diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/ConsumerOptions.cs b/EventSourcing.Backbone.Abstractions/Consumer/ConsumerOptions.cs similarity index 89% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/ConsumerOptions.cs rename to EventSourcing.Backbone.Abstractions/Consumer/ConsumerOptions.cs index 003e50b6..9d067b1b 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/ConsumerOptions.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/ConsumerOptions.cs @@ -1,12 +1,10 @@ -using Weknow.EventSource.Backbone.Enums; +using EventSourcing.Backbone.Enums; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public record ConsumerOptions : EventSourceOptions { - //public new static readonly ConsumerOptions Empty = new ConsumerOptions(); - #region BatchSize /// @@ -102,8 +100,7 @@ public DateTimeOffset? FetchUntilDateOrEmpty /// - /// Gets the threshold duration which limit considering producer call as parent, - /// Beyond this period the consumer span will refer the producer as Link (rather than parent). - /// - public TimeSpan TraceAsParent { get; init; } - - #endregion // TraceAsParent - #region OriginFilter /// diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Enums/MultiConsumerBehavior.cs b/EventSourcing.Backbone.Abstractions/Consumer/Enums/MultiConsumerBehavior.cs similarity index 89% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Enums/MultiConsumerBehavior.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Enums/MultiConsumerBehavior.cs index 6d217e8f..a7718c2f 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Enums/MultiConsumerBehavior.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Enums/MultiConsumerBehavior.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.Enums +namespace EventSourcing.Backbone.Enums { /// /// Collaborate behavior of multi consumers registered via common subscription object. @@ -7,8 +7,7 @@ /// /// Gets or sets the partial behavior diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/EventSourceConsumer.deprecated.cs b/EventSourcing.Backbone.Abstractions/Consumer/EventSourceConsumer.deprecated.cs similarity index 96% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/EventSourceConsumer.deprecated.cs rename to EventSourcing.Backbone.Abstractions/Consumer/EventSourceConsumer.deprecated.cs index 4c1e92b9..7dc5e1f2 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/EventSourceConsumer.deprecated.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/EventSourceConsumer.deprecated.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Base class for the consumer's code generator diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/IConsumerBridge.cs b/EventSourcing.Backbone.Abstractions/Consumer/IConsumerBridge.cs similarity index 93% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/IConsumerBridge.cs rename to EventSourcing.Backbone.Abstractions/Consumer/IConsumerBridge.cs index a0e3a79a..54b5c7fb 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/IConsumerBridge.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/IConsumerBridge.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Responsible of translating announcement's parameter into object diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/IConsumerEntityMapper.cs b/EventSourcing.Backbone.Abstractions/Consumer/IConsumerEntityMapper.cs similarity index 94% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/IConsumerEntityMapper.cs rename to EventSourcing.Backbone.Abstractions/Consumer/IConsumerEntityMapper.cs index 3647fbc4..953c3ee7 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/IConsumerEntityMapper.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/IConsumerEntityMapper.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Responsible of translating announcement's parameter into object diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/ISubscriptionBridge.cs b/EventSourcing.Backbone.Abstractions/Consumer/ISubscriptionBridge.cs similarity index 86% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/ISubscriptionBridge.cs rename to EventSourcing.Backbone.Abstractions/Consumer/ISubscriptionBridge.cs index 92638669..74bd8718 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/ISubscriptionBridge.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/ISubscriptionBridge.cs @@ -1,12 +1,10 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Subscription Bridge convention /// public interface ISubscriptionBridge { - // TODO: [bnaya 2021-10] version filtering - /// /// Bridges to the subscriber implementation. /// diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Interceptors/ConsumerInterceptorBridge.cs b/EventSourcing.Backbone.Abstractions/Consumer/Interceptors/ConsumerInterceptorBridge.cs similarity index 96% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Interceptors/ConsumerInterceptorBridge.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Interceptors/ConsumerInterceptorBridge.cs index 82c264c6..bb15aa01 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Interceptors/ConsumerInterceptorBridge.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Interceptors/ConsumerInterceptorBridge.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.Building +namespace EventSourcing.Backbone.Building { /// /// Bridge segmentation diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Interceptors/IConsumerAsyncInterceptor.cs b/EventSourcing.Backbone.Abstractions/Consumer/Interceptors/IConsumerAsyncInterceptor.cs similarity index 87% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Interceptors/IConsumerAsyncInterceptor.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Interceptors/IConsumerAsyncInterceptor.cs index fd941934..3d7fba91 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Interceptors/IConsumerAsyncInterceptor.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Interceptors/IConsumerAsyncInterceptor.cs @@ -1,11 +1,11 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Consumer stage of an interception operation provider. /// It can be use for variety of responsibilities like /// flowing auth context or traces, producing metrics, etc. /// - /// + /// public interface IConsumerAsyncInterceptor : IInterceptorName { diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Interceptors/IConsumerInterceptor.cs b/EventSourcing.Backbone.Abstractions/Consumer/Interceptors/IConsumerInterceptor.cs similarity index 86% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Interceptors/IConsumerInterceptor.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Interceptors/IConsumerInterceptor.cs index ba1f9cd9..2fbe60a2 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Interceptors/IConsumerInterceptor.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Interceptors/IConsumerInterceptor.cs @@ -1,11 +1,11 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Consumer stage of an interception operation provider. /// It can be use for variety of responsibilities like /// flowing auth context or traces, producing metrics, etc. /// - /// + /// public interface IConsumerInterceptor : IInterceptorName { diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Provider/IConsumerChannelProvider.cs b/EventSourcing.Backbone.Abstractions/Consumer/Provider/IConsumerChannelProvider.cs similarity index 96% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Provider/IConsumerChannelProvider.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Provider/IConsumerChannelProvider.cs index 05509104..d36017f4 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Provider/IConsumerChannelProvider.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Provider/IConsumerChannelProvider.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Channel provider responsible for passing the actual message @@ -15,7 +15,7 @@ public interface IConsumerChannelProvider /// /// When completed /// - ValueTask SubsribeAsync( + ValueTask SubscribeAsync( IConsumerPlan plan, Func> func, CancellationToken cancellationToken); diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Provider/IConsumerStorageStrategy.cs b/EventSourcing.Backbone.Abstractions/Consumer/Provider/IConsumerStorageStrategy.cs similarity index 88% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Provider/IConsumerStorageStrategy.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Provider/IConsumerStorageStrategy.cs index 5aaec665..5fa178fb 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Provider/IConsumerStorageStrategy.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Provider/IConsumerStorageStrategy.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Responsible to load information from storage. @@ -9,6 +9,11 @@ /// public interface IConsumerStorageStrategy { + /// + /// Gets the name of the storage provider. + /// + string Name { get; } + /// /// Load the bucket information. /// diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Segmentation/ConsumerSegmentationStrategyBridge.cs b/EventSourcing.Backbone.Abstractions/Consumer/Segmentation/ConsumerSegmentationStrategyBridge.cs similarity index 97% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Segmentation/ConsumerSegmentationStrategyBridge.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Segmentation/ConsumerSegmentationStrategyBridge.cs index b488ad10..f1a210be 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Segmentation/ConsumerSegmentationStrategyBridge.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Segmentation/ConsumerSegmentationStrategyBridge.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.Building +namespace EventSourcing.Backbone.Building { /// /// Bridge segmentation diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Segmentation/IConsumerAsyncSegmenationStrategy.cs b/EventSourcing.Backbone.Abstractions/Consumer/Segmentation/IConsumerAsyncSegmenationStrategy.cs similarity index 97% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Segmentation/IConsumerAsyncSegmenationStrategy.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Segmentation/IConsumerAsyncSegmenationStrategy.cs index 6e5362b0..b68e99ab 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Segmentation/IConsumerAsyncSegmenationStrategy.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Segmentation/IConsumerAsyncSegmenationStrategy.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Responsible of building instance from segmented data. diff --git a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Segmentation/IConsumerSegmenationStrategy.cs b/EventSourcing.Backbone.Abstractions/Consumer/Segmentation/IConsumerSegmenationStrategy.cs similarity index 97% rename from Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Segmentation/IConsumerSegmenationStrategy.cs rename to EventSourcing.Backbone.Abstractions/Consumer/Segmentation/IConsumerSegmenationStrategy.cs index f005cb13..73ed226e 100644 --- a/Consumers/Weknow.EventSource.Backbone.Consumers.Contracts/Segmentation/IConsumerSegmenationStrategy.cs +++ b/EventSourcing.Backbone.Abstractions/Consumer/Segmentation/IConsumerSegmenationStrategy.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Responsible of building instance from segmented data. diff --git a/Weknow.EventSource.Backbone.Contracts/Entities/Announcement/Announcement.cs b/EventSourcing.Backbone.Abstractions/Entities/Announcement/Announcement.cs similarity index 97% rename from Weknow.EventSource.Backbone.Contracts/Entities/Announcement/Announcement.cs rename to EventSourcing.Backbone.Abstractions/Entities/Announcement/Announcement.cs index 8217cca9..8fd100ac 100644 --- a/Weknow.EventSource.Backbone.Contracts/Entities/Announcement/Announcement.cs +++ b/EventSourcing.Backbone.Abstractions/Entities/Announcement/Announcement.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Non-generics form of announcement representation, diff --git a/Weknow.EventSource.Backbone.Contracts/Entities/Announcement/AnnouncementData.cs b/EventSourcing.Backbone.Abstractions/Entities/Announcement/AnnouncementData.cs similarity index 90% rename from Weknow.EventSource.Backbone.Contracts/Entities/Announcement/AnnouncementData.cs rename to EventSourcing.Backbone.Abstractions/Entities/Announcement/AnnouncementData.cs index 7ba2d4e9..24b2d2a0 100644 --- a/Weknow.EventSource.Backbone.Contracts/Entities/Announcement/AnnouncementData.cs +++ b/EventSourcing.Backbone.Abstractions/Entities/Announcement/AnnouncementData.cs @@ -1,12 +1,12 @@ using System.Diagnostics; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Non-generics form of announcement representation, /// used to transfer data via channels. /// - [DebuggerDisplay("{Operation} [{MessageId}]: Origin:{Origin}, {Environment}->{Partition}->{Shard}, EventKey:{EventKey}")] + [DebuggerDisplay("{Operation} [{MessageId}]: Origin:{Origin}, Target:{Environment}:{Uri}, EventKey:{EventKey}")] public record AnnouncementData : Metadata { #region Data @@ -40,9 +40,8 @@ public record AnnouncementData : Metadata EventKey = this.EventKey, MessageId = this.MessageId, Operation = this.Operation, - Partition = this.Partition, + Uri = this.Uri, ProducedAt = this.ProducedAt, - Shard = this.Shard, }; #endregion // ExtractMeta @@ -69,8 +68,7 @@ public static implicit operator AnnouncementData(Announcement announcement) MessageId = meta.MessageId, ProducedAt = meta.ProducedAt, Operation = meta.Operation, - Partition = meta.Partition, - Shard = meta.Shard + Uri = meta.Uri, }; } diff --git a/Weknow.EventSource.Backbone.Contracts/Entities/Announcement/EventSourceJsonContext.cs b/EventSourcing.Backbone.Abstractions/Entities/Announcement/EventSourceJsonContext.cs similarity index 87% rename from Weknow.EventSource.Backbone.Contracts/Entities/Announcement/EventSourceJsonContext.cs rename to EventSourcing.Backbone.Abstractions/Entities/Announcement/EventSourceJsonContext.cs index 1603a33e..0224b4e8 100644 --- a/Weknow.EventSource.Backbone.Contracts/Entities/Announcement/EventSourceJsonContext.cs +++ b/EventSourcing.Backbone.Abstractions/Entities/Announcement/EventSourceJsonContext.cs @@ -1,6 +1,6 @@ //using System.Text.Json.Serialization; -//using Weknow.EventSource.Backbone; +//using EventSourcing.Backbone; ////[JsonSerializable(typeof(Announcement))] ////[JsonSerializable(typeof(AnnouncementData))] diff --git a/Weknow.EventSource.Backbone.Contracts/Entities/Announcement/MessageOrigin.cs b/EventSourcing.Backbone.Abstractions/Entities/Announcement/MessageOrigin.cs similarity index 90% rename from Weknow.EventSource.Backbone.Contracts/Entities/Announcement/MessageOrigin.cs rename to EventSourcing.Backbone.Abstractions/Entities/Announcement/MessageOrigin.cs index 34855a98..44ce1990 100644 --- a/Weknow.EventSource.Backbone.Contracts/Entities/Announcement/MessageOrigin.cs +++ b/EventSourcing.Backbone.Abstractions/Entities/Announcement/MessageOrigin.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// The message origin diff --git a/Weknow.EventSource.Backbone.Contracts/Entities/Announcement/Metadata.cs b/EventSourcing.Backbone.Abstractions/Entities/Announcement/Metadata.cs similarity index 76% rename from Weknow.EventSource.Backbone.Contracts/Entities/Announcement/Metadata.cs rename to EventSourcing.Backbone.Abstractions/Entities/Announcement/Metadata.cs index d27f6f10..6ecae2f7 100644 --- a/Weknow.EventSource.Backbone.Contracts/Entities/Announcement/Metadata.cs +++ b/EventSourcing.Backbone.Abstractions/Entities/Announcement/Metadata.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// /// - [DebuggerDisplay("{Operation} [{MessageId}]: Origin:{Origin}, {Environment}->{Partition}->{Shard}, EventKey:{EventKey}")] + [DebuggerDisplay("{Operation} [{MessageId}]: Origin:{Origin}, Target:{Environment}:{Uri}, EventKey:{EventKey}")] public record Metadata { #region MessageId @@ -50,7 +50,7 @@ public record Metadata /// /// Gets or sets the operation. /// - public string Operation { get; init; } = string.Empty; + public required string Operation { get; init; } #endregion Operation @@ -59,18 +59,36 @@ public record Metadata /// /// Gets the origin environment of the message. /// - public string Environment { get; init; } = string.Empty; + public Env Environment { get; init; } = string.Empty; #endregion Environment - #region Partition + #region Uri + private string _uri = string.Empty; /// - /// Gets or sets the partition. + /// The stream identifier (the URI combined with the environment separate one stream from another) /// - public string Partition { get; init; } = string.Empty; + public required string Uri + { + get => _uri; + init + { + _uri = value; + UriDash = value.ToDash(); + } + } + + #endregion Uri + + #region UriDash + + /// + /// Gets a lower-case URI with dash separator. + /// + public string UriDash { get; private set; } = string.Empty; - #endregion Partition + #endregion // UriDash #region Origin @@ -90,15 +108,6 @@ public record Metadata #endregion // Linked - #region Shard - - /// - /// Gets or sets the shard. - /// - public string Shard { get; init; } = string.Empty; - - #endregion Shard - #region ChannelType /// @@ -116,7 +125,7 @@ public record Metadata /// /// A that represents this instance. /// - public override string ToString() => $"{EventKey}, {this.Key()}"; + public override string ToString() => $"{EventKey}, {this.FullUri()}"; #endregion // ToString } @@ -128,7 +137,7 @@ public static class MetadataExtensions { private const string EMPTY_KEY = "~EMPTY~"; - public static readonly Metadata Empty = new Metadata { MessageId = EMPTY_KEY }; + public static readonly Metadata Empty = new Metadata { MessageId = EMPTY_KEY, ChannelType = "NONE", EventKey = string.Empty, Operation = "NONE", Uri = string.Empty }; #region Duration @@ -139,21 +148,21 @@ public static class MetadataExtensions #endregion // Duration - #region Key + #region FullUri /// - /// Gets the partition:shard as key. + /// Gets the env:URI as key. /// - public static string Key(this Metadata meta, char separator = ':') + public static string FullUri(this Metadata meta, char separator = ':') { if (string.IsNullOrEmpty(meta.Environment)) - return $"{meta.Partition}{separator}{meta.Shard}"; + return meta.Uri; Env env = meta.Environment; string envFormatted = env.Format(); - return $"{envFormatted}{separator}{meta.Partition}{separator}{meta.Shard}"; + return $"{envFormatted}{separator}{meta.Uri}"; } - #endregion // Key + #endregion // FullUri #region IsEmpty diff --git a/Weknow.EventSource.Backbone.Contracts/Entities/EventKey/EventKey.cs b/EventSourcing.Backbone.Abstractions/Entities/EventKey/EventKey.cs similarity index 98% rename from Weknow.EventSource.Backbone.Contracts/Entities/EventKey/EventKey.cs rename to EventSourcing.Backbone.Abstractions/Entities/EventKey/EventKey.cs index 253608f9..5fbfd4c8 100644 --- a/Weknow.EventSource.Backbone.Contracts/Entities/EventKey/EventKey.cs +++ b/EventSourcing.Backbone.Abstractions/Entities/EventKey/EventKey.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Event Id diff --git a/Weknow.EventSource.Backbone.Contracts/Entities/EventKey/EventKeys.cs b/EventSourcing.Backbone.Abstractions/Entities/EventKey/EventKeys.cs similarity index 99% rename from Weknow.EventSource.Backbone.Contracts/Entities/EventKey/EventKeys.cs rename to EventSourcing.Backbone.Abstractions/Entities/EventKey/EventKeys.cs index 1ad23b2b..cf9ac276 100644 --- a/Weknow.EventSource.Backbone.Contracts/Entities/EventKey/EventKeys.cs +++ b/EventSourcing.Backbone.Abstractions/Entities/EventKey/EventKeys.cs @@ -1,7 +1,7 @@ using System.Collections; using System.Collections.Immutable; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Event Id diff --git a/Weknow.EventSource.Backbone.Contracts/Enums/EventBucketCategories.cs b/EventSourcing.Backbone.Abstractions/Enums/EventBucketCategories.cs similarity index 85% rename from Weknow.EventSource.Backbone.Contracts/Enums/EventBucketCategories.cs rename to EventSourcing.Backbone.Abstractions/Enums/EventBucketCategories.cs index 4e8f1283..955fef8b 100644 --- a/Weknow.EventSource.Backbone.Contracts/Enums/EventBucketCategories.cs +++ b/EventSourcing.Backbone.Abstractions/Enums/EventBucketCategories.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Bucket storage type diff --git a/EventSourcing.Backbone.Abstractions/Env.cs b/EventSourcing.Backbone.Abstractions/Env.cs new file mode 100644 index 00000000..5918912d --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Env.cs @@ -0,0 +1,195 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +using static System.StringComparison; + +namespace EventSourcing.Backbone +{ + /// + /// Common Constants + /// + [JsonConverter(typeof(EnvJsonConverter))] + public sealed class Env : IEquatable + { + private readonly string _value; + private string? _dash; + private string? _format; + private string? _dashFormat; + + #region Ctor + + /// + /// Initializes a new instance. + /// + /// The value. + public Env(string value) + { + _value = Env.Format(value); + } + + #endregion // Ctor + + #region Cast overloads + + /// + /// Performs an implicit conversion from to . + /// + /// The env. + /// + /// The result of the conversion. + /// + public static implicit operator Env(string env) => new Env(env); + + /// + /// Performs an implicit conversion from to . + /// + /// The env. + /// + /// The result of the conversion. + /// + public static implicit operator string(Env env) => env._value; + + public static bool operator ==(Env? left, Env? right) + { + return EqualityComparer.Default.Equals(left, right); + } + + public static bool operator !=(Env? left, Env? right) + { + return !(left == right); + } + + #endregion // Cast overloads + + #region DashFormat + + /// + /// Formats the specified environment into convention + dash. + /// + /// + public string DashFormat() + { + if (string.IsNullOrEmpty(_dashFormat)) + _dashFormat = Format().ToDash(); + return _dashFormat; + } + + #endregion // DashFormat + + #region Format + + /// + /// Formats the specified environment into convention. + /// + /// + public string Format() + { + if (string.IsNullOrEmpty(_format)) + _format = Env.Format(_value); + return _format; + } + + /// + /// Formats the specified environment into convention. + /// + /// + private static string Format(string e) + { + bool comp(string candidate) => string.Compare(e, candidate, OrdinalIgnoreCase) == 0; + + if (comp("Production")) return "prod"; + if (comp("Prod")) return "prod"; + if (comp("Development")) return "dev"; + if (comp("Dev")) return "dev"; + + return e; + } + + #endregion // Format + + #region ToDash + + /// + /// Converts to dash-string. + /// + public string ToDash() + { + if (string.IsNullOrEmpty(_dash)) + _dash = _value.ToDash(); + return _dash; + } + + #endregion // ToDash + + #region ToString + + /// + /// Converts to string. + /// + public override string ToString() => _value; + + #endregion // ToString + + #region EnvJsonConverter + + /// + /// Env Json Converter + /// + private sealed class EnvJsonConverter : JsonConverter + { + public override Env Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) => + new Env(reader.GetString()!); + + public override void Write( + Utf8JsonWriter writer, + Env env, + JsonSerializerOptions options) => + writer.WriteStringValue(env.ToString()); + } + + #endregion // EnvJsonConverter + + #region IEquatable members + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// + /// if the specified object is equal to the current object; otherwise, . + /// + public override bool Equals(object? obj) + { + return Equals(obj as Env); + } + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// An object to compare with this object. + /// + /// if the current object is equal to the parameter; otherwise, . + /// + public bool Equals(Env? other) + { + return other is not null && + _value == other._value; + } + + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + public override int GetHashCode() + { + return _value.GetHashCode(); + } + + #endregion // IEquatable members + } +} diff --git a/EventSourcing.Backbone.Abstractions/EventSourceConstants.cs b/EventSourcing.Backbone.Abstractions/EventSourceConstants.cs new file mode 100644 index 00000000..2d4c68a6 --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/EventSourceConstants.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace EventSourcing.Backbone; + +/// +/// Constants +/// +public static class EventSourceConstants +{ + + public static readonly JsonStringEnumConverter EnumConvertor = new JsonStringEnumConverter(JsonNamingPolicy.CamelCase); + public static readonly JsonSerializerOptions SerializerOptionsWithIndent = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + // PropertyNameCaseInsensitive = true, + // IgnoreNullValues = true, + WriteIndented = true, + Converters = + { + EnumConvertor, + JsonMemoryBytesConverterFactory.Default + } + }; + + public const string TELEMETRY_SOURCE = "evt-src"; + +} diff --git a/Weknow.EventSource.Backbone.Contracts/EventSourceFallbakLogger.cs b/EventSourcing.Backbone.Abstractions/EventSourceFallbakLogger.cs similarity index 98% rename from Weknow.EventSource.Backbone.Contracts/EventSourceFallbakLogger.cs rename to EventSourcing.Backbone.Abstractions/EventSourceFallbakLogger.cs index a8eddd83..b52a1c9e 100644 --- a/Weknow.EventSource.Backbone.Contracts/EventSourceFallbakLogger.cs +++ b/EventSourcing.Backbone.Abstractions/EventSourceFallbakLogger.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging; -namespace Weknow.EventSource.Backbone.Private +namespace EventSourcing.Backbone.Private { /// /// Best practice is to supply proper logger and diff --git a/Weknow.EventSource.Backbone.Contracts/EventSourceOptions.cs b/EventSourcing.Backbone.Abstractions/EventSourceOptions.cs similarity index 53% rename from Weknow.EventSource.Backbone.Contracts/EventSourceOptions.cs rename to EventSourcing.Backbone.Abstractions/EventSourceOptions.cs index 19f1610e..be65fc7e 100644 --- a/Weknow.EventSource.Backbone.Contracts/EventSourceOptions.cs +++ b/EventSourcing.Backbone.Abstractions/EventSourceOptions.cs @@ -1,37 +1,19 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public record EventSourceOptions { private static readonly JsonStringEnumConverter EnumConvertor = new JsonStringEnumConverter(JsonNamingPolicy.CamelCase); - //private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions - //{ - // PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - // DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - // // PropertyNameCaseInsensitive = true, - // // IgnoreNullValues = true, - // //WriteIndented = true, - // Converters = - // { - // EnumConvertor, - // JsonDictionaryConverter.Default, - // JsonImmutableDictionaryConverter.Default - // } internal static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - // PropertyNameCaseInsensitive = true, - // IgnoreNullValues = true, - //WriteIndented = true, Converters = { EnumConvertor, - JsonDictionaryConverter.Default, - JsonImmutableDictionaryConverter.Default, JsonMemoryBytesConverterFactory.Default } }; @@ -40,14 +22,9 @@ public record EventSourceOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - // PropertyNameCaseInsensitive = true, - // IgnoreNullValues = true, - //WriteIndented = true, Converters = { EnumConvertor, - JsonDictionaryConverter.Default, - JsonImmutableDictionaryConverter.Default, JsonMemoryBytesConverterFactory.Default, JsonBucketConverter.Default } @@ -55,8 +32,6 @@ public record EventSourceOptions private static readonly IDataSerializer DEFAULT_SERIALIZER = new JsonDataSerializer(FullSerializerOptions); - //public static readonly EventSourceOptions Empty = new EventSourceOptions(); - /// /// Gets the serializer. /// diff --git a/EventSourcing.Backbone.Abstractions/EventSourceTelemetry.cs b/EventSourcing.Backbone.Abstractions/EventSourceTelemetry.cs new file mode 100644 index 00000000..cc88c2b8 --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/EventSourceTelemetry.cs @@ -0,0 +1,10 @@ +using System.Diagnostics; +using System.Diagnostics.Metrics; + +namespace EventSourcing.Backbone.Private; + +public class EventSourceTelemetry +{ + public readonly static Meter EMeter = new(EventSourceConstants.TELEMETRY_SOURCE); + public static readonly ActivitySource ETracer = new ActivitySource(EventSourceConstants.TELEMETRY_SOURCE); +} diff --git a/EventSourcing.Backbone.Abstractions/EventSourcing.Backbone.Abstractions.csproj b/EventSourcing.Backbone.Abstractions/EventSourcing.Backbone.Abstractions.csproj new file mode 100644 index 00000000..6b9f0fe3 --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/EventSourcing.Backbone.Abstractions.csproj @@ -0,0 +1,35 @@ + + + + EventSourcing.Backbone + https://github.com/bnayae/Event-Source-Backbone + + + + README.md + + + + + True + \ + + + + + + + + + + + + + + + + + + + + diff --git a/EventSourcing.Backbone.Abstractions/EventSourcingException.cs b/EventSourcing.Backbone.Abstractions/EventSourcingException.cs new file mode 100644 index 00000000..c063d36d --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/EventSourcingException.cs @@ -0,0 +1,28 @@ +using System.Runtime.Serialization; + +namespace EventSourcing.Backbone +{ + /// + /// Event sourcing exception + /// + /// + [Serializable] + public class EventSourcingException : Exception + { + public EventSourcingException() : base() + { + } + + public EventSourcingException(string? message) : base(message) + { + } + + public EventSourcingException(string? message, Exception? innerException) : base(message, innerException) + { + } + + protected EventSourcingException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/Weknow.EventSource.Backbone.Contracts/Interfaces/IDataSerializer.cs b/EventSourcing.Backbone.Abstractions/Interfaces/IDataSerializer.cs similarity index 94% rename from Weknow.EventSource.Backbone.Contracts/Interfaces/IDataSerializer.cs rename to EventSourcing.Backbone.Abstractions/Interfaces/IDataSerializer.cs index 83453caf..20632949 100644 --- a/Weknow.EventSource.Backbone.Contracts/Interfaces/IDataSerializer.cs +++ b/EventSourcing.Backbone.Abstractions/Interfaces/IDataSerializer.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Enable to replace the default serialization diff --git a/Weknow.EventSource.Backbone.Contracts/Interfaces/IInterceptorName.cs b/EventSourcing.Backbone.Abstractions/Interfaces/IInterceptorName.cs similarity index 88% rename from Weknow.EventSource.Backbone.Contracts/Interfaces/IInterceptorName.cs rename to EventSourcing.Backbone.Abstractions/Interfaces/IInterceptorName.cs index d5d53215..803b68ba 100644 --- a/Weknow.EventSource.Backbone.Contracts/Interfaces/IInterceptorName.cs +++ b/EventSourcing.Backbone.Abstractions/Interfaces/IInterceptorName.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public interface IInterceptorName { diff --git a/EventSourcing.Backbone.Abstractions/Interfaces/IPlanRoute.cs b/EventSourcing.Backbone.Abstractions/Interfaces/IPlanRoute.cs new file mode 100644 index 00000000..12b53b0f --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Interfaces/IPlanRoute.cs @@ -0,0 +1,20 @@ +namespace EventSourcing.Backbone; + +/// +/// Plan routing identification +/// +public interface IPlanRoute +{ + /// + /// Environment (part of the stream key). + /// + Env Environment { get; } + /// + /// The stream identifier (the URI combined with the environment separate one stream from another) + /// + string Uri { get; } + /// + /// The stream identifier (the URI combined with the environment separate one stream from another) formatted with lower-case and dash separator + /// + string UriDash { get; } +} \ No newline at end of file diff --git a/Weknow.EventSource.Backbone.Contracts/Interfaces/IWithCancellation.cs b/EventSourcing.Backbone.Abstractions/Interfaces/IWithCancellation.cs similarity index 90% rename from Weknow.EventSource.Backbone.Contracts/Interfaces/IWithCancellation.cs rename to EventSourcing.Backbone.Abstractions/Interfaces/IWithCancellation.cs index 5f1a122a..50091cf9 100644 --- a/Weknow.EventSource.Backbone.Contracts/Interfaces/IWithCancellation.cs +++ b/EventSourcing.Backbone.Abstractions/Interfaces/IWithCancellation.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Event Source cancellation builder. diff --git a/EventSourcing.Backbone.Abstractions/JsonDataSerializer.cs b/EventSourcing.Backbone.Abstractions/JsonDataSerializer.cs new file mode 100644 index 00000000..6cb6139d --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/JsonDataSerializer.cs @@ -0,0 +1,36 @@ +using System.Text; +using System.Text.Json; + +using static System.Text.Json.Extension.Constants; + +namespace EventSourcing.Backbone; + +/// +/// Json serializer (this is the default serializer) +/// +/// +internal class JsonDataSerializer : IDataSerializer +{ + private readonly JsonSerializerOptions _options; + + public JsonDataSerializer(JsonSerializerOptions? options = null) + { + _options = options ?? SerializerOptions; + } + +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. +#pragma warning disable CS8603 // Possible null reference return. + T IDataSerializer.Deserialize(ReadOnlyMemory serializedData) + { + T result = JsonSerializer.Deserialize(serializedData.Span, _options); + return result; + } +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. +#pragma warning restore CS8603 // Possible null reference return. + + ReadOnlyMemory IDataSerializer.Serialize(T item) + { + string result = JsonSerializer.Serialize(item, _options); + return Encoding.UTF8.GetBytes(result).AsMemory(); + } +} \ No newline at end of file diff --git a/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerBuilderEnvironment.cs b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerBuilderEnvironment.cs new file mode 100644 index 00000000..c5372299 --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerBuilderEnvironment.cs @@ -0,0 +1,25 @@ +namespace EventSourcing.Backbone.Building +{ + /// + /// Origin environment of the message + /// + /// + public interface IProducerBuilderEnvironment + { + /// + /// Origin environment of the message + /// + /// + /// The environment (null: keep current environment, + /// empty: reset the environment to nothing). + /// + T Environment(Env? environment); + + /// + /// Fetch the origin environment of the message from an environment variable. + /// + /// The environment variable key. + /// + T EnvironmentFromVariable(string environmentVariableKey = "ASPNETCORE_ENVIRONMENT"); + } +} diff --git a/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerEnvironmentBuilder.cs b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerEnvironmentBuilder.cs new file mode 100644 index 00000000..509adc8f --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerEnvironmentBuilder.cs @@ -0,0 +1,9 @@ +namespace EventSourcing.Backbone.Building +{ + /// + /// Origin environment of the message + /// + public interface IProducerEnvironmentBuilder : IProducerUriBuilder, IProducerBuilderEnvironment + { + } +} diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerHooksBuilder.cs b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerHooksBuilder.cs similarity index 99% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerHooksBuilder.cs rename to EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerHooksBuilder.cs index fc2b5fdd..53558425 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerHooksBuilder.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerHooksBuilder.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.Building +namespace EventSourcing.Backbone.Building { /// /// Event Source producer builder. diff --git a/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerIocStoreStrategyBuilder.cs b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerIocStoreStrategyBuilder.cs new file mode 100644 index 00000000..2c1d9528 --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerIocStoreStrategyBuilder.cs @@ -0,0 +1,14 @@ +namespace EventSourcing.Backbone +{ + /// + /// storage configuration with IoC. + /// + /// + public interface IProducerIocStoreStrategyBuilder : IProducerStoreStrategyBuilder + { + /// + /// Gets the service provider. + /// + IServiceProvider ServiceProvider { get; } + } +} diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerLoggerBuilder.cs b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerLoggerBuilder.cs similarity index 92% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerLoggerBuilder.cs rename to EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerLoggerBuilder.cs index 3c4ac2c7..cf7bfeaa 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerLoggerBuilder.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerLoggerBuilder.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Logging; -namespace Weknow.EventSource.Backbone.Building +namespace EventSourcing.Backbone.Building { /// /// Event Source producer builder. diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerOptionsBuilder.cs b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerOptionsBuilder.cs similarity index 67% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerOptionsBuilder.cs rename to EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerOptionsBuilder.cs index dc798d88..a0e2190f 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerOptionsBuilder.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerOptionsBuilder.cs @@ -1,10 +1,10 @@ -namespace Weknow.EventSource.Backbone.Building +namespace EventSourcing.Backbone.Building { /// /// Enable configuration. /// public interface IProducerOptionsBuilder - : IProducerEnvironmentBuilder, IProducerLoggerBuilder + : IProducerEnvironmentBuilder { /// /// Apply configuration. diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerRawBuilder.cs b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerRawBuilder.cs similarity index 89% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerRawBuilder.cs rename to EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerRawBuilder.cs index 77af9241..51bc3992 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerRawBuilder.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerRawBuilder.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.Building +namespace EventSourcing.Backbone.Building { /// diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerSpecializeBuilder.cs b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerSpecializeBuilder.cs similarity index 97% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerSpecializeBuilder.cs rename to EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerSpecializeBuilder.cs index 532b03a3..dc96affa 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerSpecializeBuilder.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerSpecializeBuilder.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.Building +namespace EventSourcing.Backbone.Building { /// /// Event Source producer builder. diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerStorageStrategyWithFilter.cs b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerStorageStrategyWithFilter.cs similarity index 94% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerStorageStrategyWithFilter.cs rename to EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerStorageStrategyWithFilter.cs index 8ad0b201..8b5189bc 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerStorageStrategyWithFilter.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerStorageStrategyWithFilter.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Responsible to save information to storage. diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerStoreStrategyBuilder.cs b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerStoreStrategyBuilder.cs similarity index 84% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerStoreStrategyBuilder.cs rename to EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerStoreStrategyBuilder.cs index c05cd7f4..ee72dcfb 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerStoreStrategyBuilder.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerStoreStrategyBuilder.cs @@ -1,14 +1,14 @@  -using Microsoft.Extensions.Logging; +using EventSourcing.Backbone.Building; -using Weknow.EventSource.Backbone.Building; +using Microsoft.Extensions.Logging; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// - /// Enable configuration. + /// storage configuration. /// - /// + /// public interface IProducerStoreStrategyBuilder : IProducerOptionsBuilder { /// diff --git a/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerUriBuilder.cs b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerUriBuilder.cs new file mode 100644 index 00000000..dabd1956 --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IProducerUriBuilder.cs @@ -0,0 +1,18 @@ +namespace EventSourcing.Backbone.Building +{ + + /// + /// The stream's key (identity) + /// + public interface IProducerUriBuilder : IProducerRawBuilder, IProducerLoggerBuilder + { + /// + /// The stream key + /// + /// + /// The stream identifier (the URI combined with the environment separate one stream from another) + /// + /// + IProducerHooksBuilder Uri(string uri); + } +} diff --git a/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IRawProducer.cs b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IRawProducer.cs new file mode 100644 index 00000000..81efaedb --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/IRawProducer.cs @@ -0,0 +1,49 @@ +namespace EventSourcing.Backbone +{ + /// + /// Event Source raw producer. + /// + public interface IRawProducer + { + /// + /// + /// + /// + ValueTask Produce(Announcement data); + + /// + /// Converts to a subscription bridge which will forward the data into the producer when attached to a subscriber. + /// + /// + public ISubscriptionBridge ToSubscriptionBridge() + { + return new SubscriptionBridge(this); + } + + #region class SubscriptionBridge : ISubscriptionBridge ... + + /// + /// Subscription Bridge which produce forward data + /// + /// + private sealed class SubscriptionBridge : ISubscriptionBridge + { + private readonly IRawProducer _fw; + + public SubscriptionBridge(IRawProducer fw) + { + _fw = fw; + } + + public async Task BridgeAsync(Announcement announcement, IConsumerBridge consumerBridge) + { + await _fw.Produce(announcement); + return true; + + } + } + + #endregion // class SubscriptionBridge : ISubscriptionBridge ... + } +} diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/Override/IProducerOverrideBuildBuilder.cs b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/Override/IProducerOverrideBuildBuilder.cs similarity index 95% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/Override/IProducerOverrideBuildBuilder.cs rename to EventSourcing.Backbone.Abstractions/Producer/Builder/Building/Override/IProducerOverrideBuildBuilder.cs index d78e3734..3700f58c 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/Override/IProducerOverrideBuildBuilder.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/Override/IProducerOverrideBuildBuilder.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.Building +namespace EventSourcing.Backbone.Building { /// /// Enable dynamic transformation of the stream id before sending. diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/Override/IProducerOverrideBuilder.cs b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/Override/IProducerOverrideBuilder.cs similarity index 77% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/Override/IProducerOverrideBuilder.cs rename to EventSourcing.Backbone.Abstractions/Producer/Builder/Building/Override/IProducerOverrideBuilder.cs index 7e79622c..5a339c94 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/Override/IProducerOverrideBuilder.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/Override/IProducerOverrideBuilder.cs @@ -1,6 +1,6 @@ -using Weknow.EventSource.Backbone.Building; +using EventSourcing.Backbone.Building; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// @@ -12,10 +12,10 @@ public interface IProducerOverrideBuilder : IProducerOverrideEnvironmentBuild /// /// Dynamic override of the stream id before sending. - /// Can use for scenario like routing between environment like dev vs. prod or aws vs azure. + /// Can use for scenario like routing between environment like dev vs. prod or AWS vs Azure. /// /// The routing strategy. /// - IProducerOverrideBuildBuilder Strategy(Func routeStrategy); + IProducerOverrideBuildBuilder Strategy(Func routeStrategy); } } diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/Override/IProducerOverrideEnvironmentBuilder.cs b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/Override/IProducerOverrideEnvironmentBuilder.cs similarity index 76% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/Override/IProducerOverrideEnvironmentBuilder.cs rename to EventSourcing.Backbone.Abstractions/Producer/Builder/Building/Override/IProducerOverrideEnvironmentBuilder.cs index e08b90f0..11761db7 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/Override/IProducerOverrideEnvironmentBuilder.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/Override/IProducerOverrideEnvironmentBuilder.cs @@ -1,10 +1,10 @@ -namespace Weknow.EventSource.Backbone.Building +namespace EventSourcing.Backbone.Building { /// /// Enable dynamic transformation of the stream id before sending. /// Can use for scenario like routing between environment like dev vs. prod or aws vs azure. /// - public interface IProducerOverrideEnvironmentBuilder : IProducerOverridePartitionBuilder where T : class + public interface IProducerOverrideEnvironmentBuilder : IProducerOverrideUriBuilder where T : class { /// @@ -13,6 +13,6 @@ public interface IProducerOverrideEnvironmentBuilder : IProducerOverrideParti /// /// The environment. /// - IProducerOverridePartitionBuilder Environment(Env environment); + IProducerOverrideUriBuilder Environment(Env environment); } } diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/Override/IProducerOverrideShardBuilder.cs b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/Override/IProducerOverrideUriBuilder.cs similarity index 55% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/Override/IProducerOverrideShardBuilder.cs rename to EventSourcing.Backbone.Abstractions/Producer/Builder/Building/Override/IProducerOverrideUriBuilder.cs index 8caf5a80..1753d25f 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/Override/IProducerOverrideShardBuilder.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/Override/IProducerOverrideUriBuilder.cs @@ -1,18 +1,19 @@ -namespace Weknow.EventSource.Backbone.Building +namespace EventSourcing.Backbone.Building { /// /// Enable dynamic transformation of the stream id before sending. /// Can use for scenario like routing between environment like dev vs. prod or aws vs azure. /// - public interface IProducerOverrideShardBuilder : IProducerOverrideBuildBuilder where T : class + public interface IProducerOverrideUriBuilder : IProducerOverrideBuildBuilder where T : class { + /// - /// Override the shard. + /// Override the URI. /// Can use for scenario like routing between environment like dev vs. prod or aws vs azure. /// - /// The shard. + /// The URI. /// The type. /// - IProducerOverrideBuildBuilder Shard(string shard, RouteAssignmentType type = RouteAssignmentType.Prefix); + IProducerOverrideBuildBuilder Uri(string uri, RouteAssignmentType type = RouteAssignmentType.Prefix); } } diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/RawProducerOptions.cs b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/RawProducerOptions.cs similarity index 88% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/RawProducerOptions.cs rename to EventSourcing.Backbone.Abstractions/Producer/Builder/Building/RawProducerOptions.cs index 7b1c054f..5b579b41 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/RawProducerOptions.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/RawProducerOptions.cs @@ -1,5 +1,5 @@  -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Raw producer options diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/RouteAssignmentType.cs b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/RouteAssignmentType.cs similarity index 81% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/RouteAssignmentType.cs rename to EventSourcing.Backbone.Abstractions/Producer/Builder/Building/RouteAssignmentType.cs index c8df5e37..7013c014 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/RouteAssignmentType.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Builder/Building/RouteAssignmentType.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// the type of the routing assignment diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/IProducerBuilder.cs b/EventSourcing.Backbone.Abstractions/Producer/Builder/IProducerBuilder.cs similarity index 64% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/IProducerBuilder.cs rename to EventSourcing.Backbone.Abstractions/Producer/Builder/IProducerBuilder.cs index a8cf1abf..47cbaafe 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/IProducerBuilder.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Builder/IProducerBuilder.cs @@ -1,8 +1,8 @@ -using Microsoft.Extensions.Logging; +using EventSourcing.Backbone.Building; -using Weknow.EventSource.Backbone.Building; +using Microsoft.Extensions.Logging; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Event Source producer builder. @@ -17,6 +17,16 @@ public interface IProducerBuilder IProducerStoreStrategyBuilder UseChannel( Func channel); + /// + /// Choose the communication channel provider. + /// + /// The channel provider. + /// Dependency injection provider. + /// + IProducerIocStoreStrategyBuilder UseChannel( + IServiceProvider serviceProvider, + Func channel); + /// /// Merges multiple channels of same contract into single /// producer for broadcasting messages via all channels. diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Interceptors/IProducerAsyncInterceptor.cs b/EventSourcing.Backbone.Abstractions/Producer/Interceptors/IProducerAsyncInterceptor.cs similarity index 88% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Interceptors/IProducerAsyncInterceptor.cs rename to EventSourcing.Backbone.Abstractions/Producer/Interceptors/IProducerAsyncInterceptor.cs index 27c8123a..8d30b340 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Interceptors/IProducerAsyncInterceptor.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Interceptors/IProducerAsyncInterceptor.cs @@ -1,11 +1,11 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Producer stage of an interception operation provider. /// It can be use for variety of responsibilities like /// flowing auth context or traces, producing metrics, etc. /// - /// + /// public interface IProducerAsyncInterceptor : IInterceptorName { diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Interceptors/IProducerInterceptor.cs b/EventSourcing.Backbone.Abstractions/Producer/Interceptors/IProducerInterceptor.cs similarity index 85% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Interceptors/IProducerInterceptor.cs rename to EventSourcing.Backbone.Abstractions/Producer/Interceptors/IProducerInterceptor.cs index 2d0bd1a0..6601a219 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Interceptors/IProducerInterceptor.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Interceptors/IProducerInterceptor.cs @@ -1,11 +1,11 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Producer stage of an interception operation provider. /// It can be use for variety of responsibilities like /// flowing auth context or traces, producing metrics, etc. /// - /// + /// public interface IProducerInterceptor : IInterceptorName { diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Interceptors/ProducerInterceptorBridge.cs b/EventSourcing.Backbone.Abstractions/Producer/Interceptors/ProducerInterceptorBridge.cs similarity index 96% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Interceptors/ProducerInterceptorBridge.cs rename to EventSourcing.Backbone.Abstractions/Producer/Interceptors/ProducerInterceptorBridge.cs index bdf5cb21..397f5dc0 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Interceptors/ProducerInterceptorBridge.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Interceptors/ProducerInterceptorBridge.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.Building +namespace EventSourcing.Backbone.Building { /// /// Bridge segmentation diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Plan/IProducerPlan.cs b/EventSourcing.Backbone.Abstractions/Producer/Plan/IProducerPlan.cs similarity index 89% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Plan/IProducerPlan.cs rename to EventSourcing.Backbone.Abstractions/Producer/Plan/IProducerPlan.cs index d2a80aff..eed9805e 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Plan/IProducerPlan.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Plan/IProducerPlan.cs @@ -1,12 +1,12 @@ using System.Collections.Immutable; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Plan contract /// - /// + /// public interface IProducerPlan : IProducerPlanBase { /// diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Plan/IProducerPlanBase.cs b/EventSourcing.Backbone.Abstractions/Producer/Plan/IProducerPlanBase.cs similarity index 97% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Plan/IProducerPlanBase.cs rename to EventSourcing.Backbone.Abstractions/Producer/Plan/IProducerPlanBase.cs index 90f1d9f6..f275b55c 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Plan/IProducerPlanBase.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Plan/IProducerPlanBase.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Plan/IProducerPlanBuilder.cs b/EventSourcing.Backbone.Abstractions/Producer/Plan/IProducerPlanBuilder.cs similarity index 88% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Plan/IProducerPlanBuilder.cs rename to EventSourcing.Backbone.Abstractions/Producer/Plan/IProducerPlanBuilder.cs index ae2dad61..2d4f5478 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Plan/IProducerPlanBuilder.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Plan/IProducerPlanBuilder.cs @@ -1,15 +1,15 @@ using System.Collections.Immutable; -using Microsoft.Extensions.Logging; +using EventSourcing.Backbone.Building; -using Weknow.EventSource.Backbone.Building; +using Microsoft.Extensions.Logging; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Plan builder contract /// - /// + /// public interface IProducerPlanBuilder : IProducerPlanBase { /// diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Plan/ProducerPlan.cs b/EventSourcing.Backbone.Abstractions/Producer/Plan/ProducerPlan.cs similarity index 76% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Plan/ProducerPlan.cs rename to EventSourcing.Backbone.Abstractions/Producer/Plan/ProducerPlan.cs index 8a9a98c3..a525eaec 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Plan/ProducerPlan.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Plan/ProducerPlan.cs @@ -1,11 +1,11 @@ using System.Collections.Immutable; -using Microsoft.Extensions.Logging; +using EventSourcing.Backbone.Building; +using EventSourcing.Backbone.Private; -using Weknow.EventSource.Backbone.Building; -using Weknow.EventSource.Backbone.Private; +using Microsoft.Extensions.Logging; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// @@ -33,8 +33,7 @@ private ProducerPlan() /// The channel. /// The channel. /// The environment. - /// The partition. - /// The shard. + /// The URI. /// The logger. /// The options. /// The segmentation strategies. @@ -43,13 +42,13 @@ private ProducerPlan() /// Result of merging multiple channels. /// The forward channels. /// The storage strategy. + /// The service provider. private ProducerPlan( ProducerPlan copyFrom, Func? channelFactory = null, IProducerChannelProvider? channel = null, string? environment = null, - string? partition = null, - string? shard = null, + string? uri = null, ILogger? logger = null, EventSourceOptions? options = null, IImmutableList? segmentationStrategies = null, @@ -57,19 +56,20 @@ private ProducerPlan( IImmutableList? routes = null, IImmutableList? forwards = null, IImmutableList? forwardPlans = null, - Func>? storageStrategyFactories = null) + Func>? storageStrategyFactories = null, + IServiceProvider? serviceProvider = null) { ChannelFactory = channelFactory ?? copyFrom.ChannelFactory; _channel = channel ?? copyFrom._channel; Environment = environment ?? copyFrom.Environment; - Partition = partition ?? copyFrom.Partition; - Shard = shard ?? copyFrom.Shard; + Uri = uri ?? copyFrom.Uri; Options = options ?? copyFrom.Options; SegmentationStrategies = segmentationStrategies ?? copyFrom.SegmentationStrategies; Interceptors = interceptors ?? copyFrom.Interceptors; Routes = routes ?? copyFrom.Routes; Forwards = forwards ?? copyFrom.Forwards; _forwardPlans = forwardPlans ?? copyFrom._forwardPlans; + ServiceProvider = serviceProvider ?? copyFrom.ServiceProvider; Logger = logger ?? copyFrom.Logger; StorageStrategyFactories = storageStrategyFactories == null ? copyFrom.StorageStrategyFactories @@ -98,8 +98,13 @@ IProducerChannelProvider IProducerPlan.Channel { get { +#pragma warning disable S2372 // Exceptions should not be thrown from property getters +#pragma warning disable S3928 // Parameter names used into ArgumentException constructors should match an existing one if (_channel == null) throw new ArgumentNullException("Event Source Producer channel not set"); +#pragma warning restore S3928 // +#pragma warning restore S2372 + return _channel; } } @@ -145,36 +150,32 @@ IProducerChannelProvider IProducerPlan.Channel #endregion // Environment - #region Partition + #region Uri + private string _uri = string.Empty; /// - /// Partition key represent logical group of - /// event source shards. - /// For example assuming each ORDERING flow can have its - /// own messaging sequence, yet can live concurrency with - /// other ORDER's sequences. - /// The partition will let consumer the option to be notify and - /// consume multiple shards from single consumer. - /// This way the consumer can handle all orders in - /// central place without affecting sequence of specific order - /// flow or limiting the throughput. + /// The stream identifier (the URI combined with the environment separate one stream from another) /// - public string Partition { get; } = string.Empty; + public string Uri + { + get => _uri; + init + { + _uri = value; + UriDash = value.ToDash(); + } + } - #endregion // Partition + #endregion Uri - #region Shard + #region UriDash /// - /// Shard key represent physical sequence. - /// Use same shard when order is matter. - /// For example: assuming each ORDERING flow can have its - /// own messaging sequence, in this case you can split each - /// ORDER into different shard and gain performance bust.. + /// Gets a lower-case URI with dash separator. /// - public string Shard { get; } = string.Empty; + public string UriDash { get; private set; } = string.Empty; - #endregion // Shard + #endregion // UriDash #region Options @@ -227,6 +228,15 @@ IProducerChannelProvider IProducerPlan.Channel #endregion // Forwards + #region ServiceProvider + + /// + /// Gets the service provider. + /// + public IServiceProvider? ServiceProvider { get; } + + #endregion // ServiceProvider + #region ForwardChannels private readonly IImmutableList _forwardPlans = ImmutableList.Empty; @@ -242,7 +252,7 @@ IProducerChannelProvider IProducerPlan.Channel /// /// Routes are sub-pipelines are results of merge operation - /// which can split same payload into multiple partitions or shards. + /// which can split same payload into multiple URIs. /// private readonly IImmutableList Routes = ImmutableList.Empty; @@ -257,10 +267,11 @@ IProducerChannelProvider IProducerPlan.Channel /// Assign channel. /// /// The channel factory. + /// Dependency injection provider. /// - public ProducerPlan UseChannel(Func channelFactory) + public ProducerPlan UseChannel(Func channelFactory, IServiceProvider? serviceProvider = null) { - return new ProducerPlan(this, channelFactory: channelFactory); + return new ProducerPlan(this, channelFactory: channelFactory, serviceProvider: serviceProvider); } #endregion // WithOptions @@ -313,71 +324,46 @@ public ProducerPlan WithOptions(EventSourceOptions options) /// Withes the environment. /// /// The environment. - /// The partition. - /// The shard. - /// The type (only for partition and shard). + /// The URI. + /// The type (only for uri). /// public ProducerPlan WithEnvironment( Env environment, - string? partition = null, - string? shard = null, + string? uri = null, RouteAssignmentType type = RouteAssignmentType.Replace) { return type switch { - RouteAssignmentType.Prefix => new ProducerPlan(this, environment: environment, partition: $"{partition}{this.Partition}", shard: $"{shard}{this.Shard}"), - RouteAssignmentType.Replace => new ProducerPlan(this, environment: environment, partition: partition ?? this.Partition, shard: shard ?? this.Shard), - RouteAssignmentType.Suffix => new ProducerPlan(this, environment: environment, partition: $"{this.Partition}{partition}", shard: $"{this.Shard}{shard}"), + RouteAssignmentType.Prefix => new ProducerPlan(this, environment: environment, uri: $"{uri}{this.Uri}"), + RouteAssignmentType.Replace => new ProducerPlan(this, environment: environment, uri: uri ?? this.Uri), + RouteAssignmentType.Suffix => new ProducerPlan(this, environment: environment, uri: $"{this.Uri}{uri}"), _ => this, }; } #endregion // WithEnvironment - #region WithPartition - - /// - /// Withes the partition. - /// - /// The partition. - /// The shard. - /// The type. - /// - public ProducerPlan WithPartition(string partition, string? shard = null, - RouteAssignmentType type = RouteAssignmentType.Replace) - { - return type switch - { - RouteAssignmentType.Prefix => new ProducerPlan(this, partition: $"{partition}{this.Partition}", shard: $"{shard}{this.Shard}"), - RouteAssignmentType.Replace => new ProducerPlan(this, partition: partition, shard: shard ?? this.Shard), - RouteAssignmentType.Suffix => new ProducerPlan(this, partition: $"{this.Partition}{partition}", shard: $"{this.Shard}{shard}"), - _ => this, - }; - } - - #endregion // WithPartition - - #region WithShard + #region WithKey /// - /// Withes the shard. + /// Withes the stream's key (identifier). /// - /// The shard. + /// The URI. /// The type. + /// /// - public ProducerPlan WithShard(string shard, - RouteAssignmentType type = RouteAssignmentType.Replace) + public ProducerPlan WithKey(string uri, RouteAssignmentType type = RouteAssignmentType.Replace) { return type switch { - RouteAssignmentType.Prefix => new ProducerPlan(this, shard: $"{shard}{this.Shard}"), - RouteAssignmentType.Replace => new ProducerPlan(this, shard: shard ?? this.Shard), - RouteAssignmentType.Suffix => new ProducerPlan(this, shard: $"{this.Shard}{shard}"), + RouteAssignmentType.Prefix => new ProducerPlan(this, uri: $"{uri}{this.Uri}"), + RouteAssignmentType.Replace => new ProducerPlan(this, uri: uri), + RouteAssignmentType.Suffix => new ProducerPlan(this, uri: $"{this.Uri}{uri}"), _ => this, }; } - #endregion // WithShard + #endregion // WithKey #region AddRoute @@ -438,29 +424,6 @@ public ProducerPlan AddForward(IProducerHooksBuilder forward) #endregion // AddForward - #region class NopChannel - - /// - /// Not operational channel - /// - /// - private class NopChannel : IProducerChannelProvider - { - public static readonly IProducerChannelProvider Empty = new NopChannel(); - /// - /// Send. - /// - /// The payload. - /// The storage strategy. - /// A ValueTask. - public ValueTask SendAsync(Announcement payload, ImmutableArray storageStrategy) - { - throw new NotSupportedException("Channel must be assign"); - } - } - - #endregion // class NopChannel - // --------------------------------------- #region Build diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/ProducerExtensions.cs b/EventSourcing.Backbone.Abstractions/Producer/ProducerExtensions.cs similarity index 91% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/ProducerExtensions.cs rename to EventSourcing.Backbone.Abstractions/Producer/ProducerExtensions.cs index 181c49a3..b2f8dbbd 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/ProducerExtensions.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/ProducerExtensions.cs @@ -4,9 +4,9 @@ using Microsoft.Extensions.Logging; -using static Weknow.EventSource.Backbone.EventSourceConstants; +using static EventSourcing.Backbone.EventSourceConstants; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public static class ProducerExtensions { @@ -38,7 +38,7 @@ ValueTask Local(ILogger logger) /// /// Non strategy implementation /// - /// + /// private class VoidStorageStrategy : IProducerStorageStrategy { private readonly string _providerPrefix; @@ -56,6 +56,12 @@ public VoidStorageStrategy(string providerPrefix) #endregion // Ctor + /// + /// Gets the name of the storage provider. + /// + public string Name { get; } = "void-storage"; + + /// /// Saves the bucket information. /// diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/ProducerPipeline.cs b/EventSourcing.Backbone.Abstractions/Producer/ProducerPipeline.cs similarity index 98% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/ProducerPipeline.cs rename to EventSourcing.Backbone.Abstractions/Producer/ProducerPipeline.cs index dd9c0562..95e53128 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/ProducerPipeline.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/ProducerPipeline.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Handle the producing pipeline @@ -228,8 +228,7 @@ private async ValueTask SendAsync( { MessageId = id, Environment = plan.Environment, - Partition = plan.Partition, - Shard = plan.Shard, + Uri = plan.Uri, Operation = operation }; diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Provider/IProducerChannelProvider.cs b/EventSourcing.Backbone.Abstractions/Producer/Provider/IProducerChannelProvider.cs similarity index 94% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Provider/IProducerChannelProvider.cs rename to EventSourcing.Backbone.Abstractions/Producer/Provider/IProducerChannelProvider.cs index 84e73582..8846af05 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Provider/IProducerChannelProvider.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Provider/IProducerChannelProvider.cs @@ -1,6 +1,6 @@ using System.Collections.Immutable; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Channel provider responsible for passing the actual message diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Provider/IProducerStorageStrategy.cs b/EventSourcing.Backbone.Abstractions/Producer/Provider/IProducerStorageStrategy.cs similarity index 89% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Provider/IProducerStorageStrategy.cs rename to EventSourcing.Backbone.Abstractions/Producer/Provider/IProducerStorageStrategy.cs index 7414fb88..e315973e 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Provider/IProducerStorageStrategy.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Provider/IProducerStorageStrategy.cs @@ -1,6 +1,6 @@ using System.Collections.Immutable; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Responsible to save information to storage. @@ -11,6 +11,11 @@ namespace Weknow.EventSource.Backbone /// public interface IProducerStorageStrategy { + /// + /// Gets the name of the storage provider. + /// + string Name { get; } + /// /// Saves the bucket information. /// diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Segmentation/IProducerAsyncSegmentationStrategy.cs b/EventSourcing.Backbone.Abstractions/Producer/Segmentation/IProducerAsyncSegmentationStrategy.cs similarity index 98% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Segmentation/IProducerAsyncSegmentationStrategy.cs rename to EventSourcing.Backbone.Abstractions/Producer/Segmentation/IProducerAsyncSegmentationStrategy.cs index c82637fc..36300dbe 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Segmentation/IProducerAsyncSegmentationStrategy.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Segmentation/IProducerAsyncSegmentationStrategy.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Responsible of splitting an instance into segments. diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Segmentation/IProducerSegmentationStrategy.cs b/EventSourcing.Backbone.Abstractions/Producer/Segmentation/IProducerSegmentationStrategy.cs similarity index 97% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Segmentation/IProducerSegmentationStrategy.cs rename to EventSourcing.Backbone.Abstractions/Producer/Segmentation/IProducerSegmentationStrategy.cs index 759b2a6a..78b84686 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Segmentation/IProducerSegmentationStrategy.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Segmentation/IProducerSegmentationStrategy.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.Building +namespace EventSourcing.Backbone.Building { /// /// Responsible of splitting an instance into segments. diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Segmentation/ProducerSegmentationStrategyBridge.cs b/EventSourcing.Backbone.Abstractions/Producer/Segmentation/ProducerSegmentationStrategyBridge.cs similarity index 98% rename from Producers/Weknow.EventSource.Backbone.Producers.Contracts/Segmentation/ProducerSegmentationStrategyBridge.cs rename to EventSourcing.Backbone.Abstractions/Producer/Segmentation/ProducerSegmentationStrategyBridge.cs index e39004cf..abf07786 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Segmentation/ProducerSegmentationStrategyBridge.cs +++ b/EventSourcing.Backbone.Abstractions/Producer/Segmentation/ProducerSegmentationStrategyBridge.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.Building +namespace EventSourcing.Backbone.Building { /// /// Bridge segmentation diff --git a/EventSourcing.Backbone.Abstractions/Telemetry/EventSourceTelemetryExtensions.cs b/EventSourcing.Backbone.Abstractions/Telemetry/EventSourceTelemetryExtensions.cs new file mode 100644 index 00000000..3005a0f0 --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Telemetry/EventSourceTelemetryExtensions.cs @@ -0,0 +1,153 @@ +using System.Collections; +using System.Collections.Immutable; +using System.Diagnostics; + +using OpenTelemetry; +using OpenTelemetry.Context.Propagation; + +namespace EventSourcing.Backbone; + +public static class EventSourceTelemetryExtensions +{ + internal static readonly TextMapPropagator Propagator = Propagators.DefaultTextMapPropagator; + + #region InjectSpan + + /// + /// Inject telemetry span to the channel property (Before sending). + /// + /// + /// The activity. + /// The entries builder. + /// The injection strategy. + /// + public static void InjectSpan( + this Activity? activity, + ImmutableArray.Builder entriesBuilder, + Action.Builder, string, string> injectStrategy) + { + // Depending on Sampling (and whether a listener is registered or not), the + // activity above may not be created. + // If it is created, then propagate its context. + // If it is not created, the propagate the Current context, + // if any. + if (activity != null) + { + Activity.Current = activity; + } + ActivityContext contextToInject = Activity.Current?.Context ?? default; + + // Inject the ActivityContext + Propagator.Inject(new PropagationContext(contextToInject, Baggage.Current), entriesBuilder, injectStrategy); + } + + #endregion // InjectSpan + + #region ExtractSpan + + /// + /// Extract telemetry span's parent info (while consuming) + /// + /// + /// The entries for extraction. + /// The injection strategy. + /// + public static ActivityContext ExtractSpan( + T entries, + Func> injectStrategy) + { + PropagationContext parentContext = Propagator.Extract(default, entries, injectStrategy); + Baggage.Current = parentContext.Baggage; + + // Start an activity with a name following the semantic convention of the OpenTelemetry messaging specification. + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/messaging.md#span-name + return parentContext.ActivityContext; + } + + #endregion // ExtractSpan + + #region InjectTelemetryTags + + /// + /// Adds standard open-telemetry tags (for redis). + /// + /// The meta. + /// The activity. + public static void InjectTelemetryTags(this Metadata meta, Activity? activity) + { + // These tags are added demonstrating the semantic conventions of the OpenTelemetry messaging specification + // See: + // * https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/messaging.md#messaging-attributes + + activity?.SetTag("evt-src.env", meta.Environment); + activity?.SetTag("evt-src.uri", meta.Uri); + activity?.SetTag("evt-src.operation", meta.Operation); + activity?.SetTag("evt-src.message-id", meta.MessageId); + activity?.SetTag("evt-src.channel-type", meta.ChannelType); + } + + #endregion // InjectTelemetryTags + + #region StartInternalTrace + + /// + /// Starts a trace. + /// + /// The activity source. + /// The name. + /// The tags action. + /// + public static Activity? StartInternalTrace(this ActivitySource activitySource, + string name, + Action? tagsAction = null) + { + // Start an activity with a name following the semantic convention of the OpenTelemetry messaging specification. + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/messaging.md#span-name + + var tags = new ActivityTagsCollection(); + var t = new TagAddition(tags); + tagsAction?.Invoke(t); + + ActivityContext? context = Activity.Current?.Context; + Activity? activity; + if (context == null) + { + activity = activitySource.StartActivity( + name, + ActivityKind.Internal, + Activity.Current?.Context ?? default, tags); + } + else + { + var link = new ActivityLink((ActivityContext)context); + activity = activitySource.StartActivity( + name, + ActivityKind.Internal, + Activity.Current?.Context ?? default, tags, link.ToEnumerable()); + } + + return activity; + } + + #endregion // StartInternalTrace + + #region AddEvent + + /// + /// Adds the event. + /// + /// The current. + /// The name. + /// The tags action. + /// + public static Activity? AddEvent(this Activity current, string name, Action? tagsAction = null) + { + var tags = new ActivityTagsCollection(); + var t = new TagAddition(tags); + tagsAction?.Invoke(t); + var e = new ActivityEvent(name, tags: tags); + return current?.AddEvent(e); + } + + #endregion // AddEvent +} diff --git a/EventSourcing.Backbone.Abstractions/Telemetry/ITagAddition.cs b/EventSourcing.Backbone.Abstractions/Telemetry/ITagAddition.cs new file mode 100644 index 00000000..074ce88e --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Telemetry/ITagAddition.cs @@ -0,0 +1,6 @@ +namespace EventSourcing.Backbone; + +public interface ITagAddition +{ + ITagAddition Add(string key, T value); +} diff --git a/EventSourcing.Backbone.Abstractions/Telemetry/TagAddition.cs b/EventSourcing.Backbone.Abstractions/Telemetry/TagAddition.cs new file mode 100644 index 00000000..848a7435 --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Telemetry/TagAddition.cs @@ -0,0 +1,22 @@ +using System.Diagnostics; + +namespace EventSourcing.Backbone; + +/// +/// Tag addition +/// +/// +public class TagAddition : ITagAddition +{ + private readonly ActivityTagsCollection _tags; + + public TagAddition(ActivityTagsCollection tags) + { + _tags = tags; + } + public ITagAddition Add(string key, T value) + { + _tags.Add(key, value); + return this; + } +} diff --git a/EventSourcing.Backbone.Abstractions/Telemetry/deprecated/ITelemetryAbstraction.cs b/EventSourcing.Backbone.Abstractions/Telemetry/deprecated/ITelemetryAbstraction.cs new file mode 100644 index 00000000..7f12f4a4 --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Telemetry/deprecated/ITelemetryAbstraction.cs @@ -0,0 +1,7 @@ + +///// +///// Telemetry api +///// +//public interface ITelemetryAbstraction +//{ +//} diff --git a/EventSourcing.Backbone.Abstractions/Telemetry/deprecated/TelemetryAbstraction.cs b/EventSourcing.Backbone.Abstractions/Telemetry/deprecated/TelemetryAbstraction.cs new file mode 100644 index 00000000..cb362cac --- /dev/null +++ b/EventSourcing.Backbone.Abstractions/Telemetry/deprecated/TelemetryAbstraction.cs @@ -0,0 +1,91 @@ +//using System.Diagnostics; +//using System.Runtime.CompilerServices; +//using OpenTelemetry.Context.Propagation; + +//using OpenTelemetry; + +//namespace EventSourcing.Backbone.Telemetry; + +///// +///// Telemetry api +///// +//public class TelemetryAbstraction: ITelemetryAbstraction +//{ +// internal static readonly TextMapPropagator Propagator = Propagators.DefaultTextMapPropagator; + +// /// +// /// Telemetry operators +// /// +// private readonly ActivitySource _telemetry; + +// public TelemetryAbstraction(string source) +// { +// _telemetry = new ActivitySource(source); +// } + +// /// +// /// Starts a trace. +// /// +// /// The name. +// /// The kind. +// /// +// private Activity? Start([CallerMemberName] string name = "", ActivityKind kind = ActivityKind.Internal) +// { +// return _telemetry.StartActivity(name, kind); +// } + +// /// +// /// Starts a trace. +// /// +// /// The name. +// /// The kind. +// /// The parent context. +// /// The tags. +// /// The links. +// /// The start time. +// /// +// private Activity? Start( +// string name, +// ActivityKind kind, +// ActivityContext parentContext, +// IEnumerable>? tags = null, +// IEnumerable? links = null, +// DateTimeOffset startTime = default) +// { +// return _telemetry.StartActivity(name, kind, parentContext, tags, links, startTime); +// } + +// /// +// /// Starts a trace. +// /// +// /// The name. +// /// The kind. +// /// The parent identifier. +// /// The tags. +// /// The links. +// /// The start time. +// /// +// private Activity? Start( +// string name, +// ActivityKind kind, +// string? parentId, +// IEnumerable>? tags = null, +// IEnumerable? links = null, +// DateTimeOffset startTime = default) + +// { +// return _telemetry.StartActivity(name, kind, parentId, tags, links, startTime); +// } + +// private Activity? Start( +// ActivityKind kind, +// ActivityContext parentContext = default, +// IEnumerable>? tags = null, +// IEnumerable? links = null, +// DateTimeOffset startTime = default, +// [CallerMemberName] string name = "") + +// { +// return _telemetry.StartActivity(kind, parentContext, tags, links, startTime, name); +// } +//} diff --git a/EventSourcing.Backbone.Abstractions/icon.png b/EventSourcing.Backbone.Abstractions/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/EventSourcing.Backbone.Abstractions/icon.png differ diff --git a/EventSourcing.Backbone.sln b/EventSourcing.Backbone.sln new file mode 100644 index 00000000..f913f4da --- /dev/null +++ b/EventSourcing.Backbone.sln @@ -0,0 +1,255 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31825.309 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcing.Backbone", "EventSourcing.Backbone\EventSourcing.Backbone.csproj", "{52BFDB45-C61C-4FF8-98BA-41407FFC85F5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{4DE45B98-D18F-4E1D-9D2F-A4E40C11BC1F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcing.Backbone.UnitTests", "Tests\EventSourcing.Backbone.UnitTests\EventSourcing.Backbone.UnitTests.csproj", "{C1618305-4F0B-4AAA-8386-CA31DCBC5F59}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcing.Backbone.Abstractions", "EventSourcing.Backbone.Abstractions\EventSourcing.Backbone.Abstractions.csproj", "{E17F4D78-8645-457E-B4CB-455FCED12F49}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Producers", "Producers", "{3FBE7FA1-700D-4B58-8569-15F533F761D1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Consumers", "Consumers", "{EC34CEBD-EEDE-4CBA-A28A-A71BCB51D5D2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcing.Backbone.Consumers", "Consumers\EventSourcing.Backbone.Consumers\EventSourcing.Backbone.Consumers.csproj", "{6821E250-3BE8-4CDA-AFEA-FB8C9B4C4106}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcing.Backbone.Producers", "Producers\EventSourcing.Backbone.Producers\EventSourcing.Backbone.Producers.csproj", "{90B4EBE9-7CDB-42E0-B5E2-9FCAC4AD52CA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Channels", "Channels", "{75CE8A6F-D63B-491D-8834-8D3BA2127208}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Redis", "Redis", "{64FC6860-BD4C-4C11-BF6E-E99BB4D91723}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcing.Backbone.IntegrationTests", "Tests\EventSourcing.Backbone.IntegrationTests\EventSourcing.Backbone.IntegrationTests.csproj", "{20A4298E-1929-4125-8D66-CABF330DE633}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcing.Backbone.Channels.RedisProvider.Common", "Channels\REDIS\EventSourcing.Backbone.Channels.RedisProvider.Common\EventSourcing.Backbone.Channels.RedisProvider.Common.csproj", "{68387B20-DC7B-4C56-906E-9EC379677DE3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcing.Backbone.Channels.RedisProducerProvider", "Channels\REDIS\EventSourcing.Backbone.Channels.RedisProducerProvider\EventSourcing.Backbone.Channels.RedisProducerProvider.csproj", "{C4D21A30-B556-4272-85BD-8F7793BD7432}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcing.Backbone.Channels.RedisConsumerProvider", "Channels\REDIS\EventSourcing.Backbone.Channels.RedisConsumerProvider\EventSourcing.Backbone.Channels.RedisConsumerProvider.csproj", "{ECC48028-F980-49E9-AAC1-65AF5AF3B16F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcing.Backbone.Channels.S3StoreProvider.Common", "Channels\S3\EventSourcing.Backbone.Channels.S3StoreProvider.Common\EventSourcing.Backbone.Channels.S3StoreProvider.Common.csproj", "{54FA986A-47D9-4709-9C3E-AEB08CE73EBD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcing.Backbone.Channels.S3StoreProducerProvider", "Channels\S3\EventSourcing.Backbone.Channels.S3StoreProducerProvider\EventSourcing.Backbone.Channels.S3StoreProducerProvider.csproj", "{686220F1-A72B-45FE-8A6C-148926973096}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcing.Backbone.Channels.S3StoreConsumerProvider", "Channels\S3\EventSourcing.Backbone.Channels.S3StoreConsumerProvider\EventSourcing.Backbone.Channels.S3StoreConsumerProvider.csproj", "{39B1BBC2-1924-4B85-9BA4-3319F5C4F3E6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6BD7580F-9EAE-4046-8EA9-10FBC8CF2C56}" + ProjectSection(SolutionItems) = preProject + .dockerignore = .dockerignore + .editorconfig = .editorconfig + .gitattributes = .gitattributes + .gitignore = .gitignore + Directory.Build.props = Directory.Build.props + README.md = README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "S3", "S3", "{1B19D36E-8348-4CAE-A2B8-87A81076539A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcing.Backbone.WebEventTest", "Tests\EventSourcing.Backbone.WebEventTest\EventSourcing.Backbone.WebEventTest.csproj", "{D648BE46-4208-4DEB-9F2F-0B009B603D0A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src-gen", "src-gen", "{2AE6684B-47A2-46D3-BE4E-C14B05255BB6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcing.Backbone.SrcGen", "src-gen\EventSourcing.Backbone.SrcGen\EventSourcing.Backbone.SrcGen.csproj", "{230DA8DF-00EE-47E2-A7B8-54F05A8966F6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcing.Backbone.SrcGen.Playground", "src-gen\EventSourcing.Backbone.SrcGen.Playground\EventSourcing.Backbone.SrcGen.Playground.csproj", "{49A8B718-DAB6-4D04-874C-0B1EA12826EB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlayGroundAbstraction", "src-gen\PlayGroundAbstraction\PlayGroundAbstraction.csproj", "{FBD76237-D44F-4DC4-BCB1-0DCA85A73052}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HelloWorld", "HelloWorld", "{9A1275C5-2299-4560-ABB6-CE56B9175CF7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventsAbstractions", "Tests\HelloWorld\EventsAbstractions\EventsAbstractions.csproj", "{81EBE4CC-2E70-4E66-89DC-DCBB591343AB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsumingEvents", "Tests\HelloWorld\ConsumingEvents\ConsumingEvents.csproj", "{A78DF256-6896-4246-8CF5-8CCBB27760A5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProducingEvents", "Tests\HelloWorld\ProducingEvents\ProducingEvents.csproj", "{B528D83E-1AB9-4461-BA5E-993FF882F85B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebSampleS3", "Tests\WebSampleS3\WebSampleS3.csproj", "{7A749C43-60F4-4DDC-894C-5E407B08A3A9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{2C4E653D-8B7A-48BA-A9DB-F9707BC04380}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcing.Backbone.OpenTelemetry.Extensions", "Extensions\EventSourcing.Backbone.OpenTelemetry.Extensions\EventSourcing.Backbone.OpenTelemetry.Extensions.csproj", "{74F90FB5-198F-4B90-ACBD-F89E4F81FA1B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Specialized", "Specialized", "{DB106708-EA65-4FD3-9362-950CA07DB2E6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.Events.WebTest.Abstractions", "Tests\Specialized\Tests.Events.WebTest.Abstractions\Tests.Events.WebTest.Abstractions.csproj", "{8E90962C-870D-49C6-A260-585D16F13614}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.Events.ProducerWebTest.Service", "Tests\Specialized\Tests.Events.ProducerWebTest.Service\Tests.Events.ProducerWebTest.Service.csproj", "{ED906FBD-F8BB-40D5-8629-00CE0F963681}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.Events.ConsumerWebTest.Service", "Tests\Specialized\Tests.Events.ConsumerWebTest.Service\Tests.Events.ConsumerWebTest.Service.csproj", "{842CEAED-FA0D-4EFF-805A-D37F62AEFB0F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleTest", "Tests\ConsoleTest\ConsoleTest.csproj", "{EAE6F008-AB9E-4321-BC31-D50534111971}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Gen|Any CPU = Gen|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {52BFDB45-C61C-4FF8-98BA-41407FFC85F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {52BFDB45-C61C-4FF8-98BA-41407FFC85F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {52BFDB45-C61C-4FF8-98BA-41407FFC85F5}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {52BFDB45-C61C-4FF8-98BA-41407FFC85F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {52BFDB45-C61C-4FF8-98BA-41407FFC85F5}.Release|Any CPU.Build.0 = Release|Any CPU + {C1618305-4F0B-4AAA-8386-CA31DCBC5F59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C1618305-4F0B-4AAA-8386-CA31DCBC5F59}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C1618305-4F0B-4AAA-8386-CA31DCBC5F59}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {C1618305-4F0B-4AAA-8386-CA31DCBC5F59}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C1618305-4F0B-4AAA-8386-CA31DCBC5F59}.Release|Any CPU.Build.0 = Release|Any CPU + {E17F4D78-8645-457E-B4CB-455FCED12F49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E17F4D78-8645-457E-B4CB-455FCED12F49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E17F4D78-8645-457E-B4CB-455FCED12F49}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {E17F4D78-8645-457E-B4CB-455FCED12F49}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E17F4D78-8645-457E-B4CB-455FCED12F49}.Release|Any CPU.Build.0 = Release|Any CPU + {6821E250-3BE8-4CDA-AFEA-FB8C9B4C4106}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6821E250-3BE8-4CDA-AFEA-FB8C9B4C4106}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6821E250-3BE8-4CDA-AFEA-FB8C9B4C4106}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {6821E250-3BE8-4CDA-AFEA-FB8C9B4C4106}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6821E250-3BE8-4CDA-AFEA-FB8C9B4C4106}.Release|Any CPU.Build.0 = Release|Any CPU + {90B4EBE9-7CDB-42E0-B5E2-9FCAC4AD52CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90B4EBE9-7CDB-42E0-B5E2-9FCAC4AD52CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90B4EBE9-7CDB-42E0-B5E2-9FCAC4AD52CA}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {90B4EBE9-7CDB-42E0-B5E2-9FCAC4AD52CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90B4EBE9-7CDB-42E0-B5E2-9FCAC4AD52CA}.Release|Any CPU.Build.0 = Release|Any CPU + {20A4298E-1929-4125-8D66-CABF330DE633}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20A4298E-1929-4125-8D66-CABF330DE633}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20A4298E-1929-4125-8D66-CABF330DE633}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {20A4298E-1929-4125-8D66-CABF330DE633}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20A4298E-1929-4125-8D66-CABF330DE633}.Release|Any CPU.Build.0 = Release|Any CPU + {68387B20-DC7B-4C56-906E-9EC379677DE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68387B20-DC7B-4C56-906E-9EC379677DE3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68387B20-DC7B-4C56-906E-9EC379677DE3}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {68387B20-DC7B-4C56-906E-9EC379677DE3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68387B20-DC7B-4C56-906E-9EC379677DE3}.Release|Any CPU.Build.0 = Release|Any CPU + {C4D21A30-B556-4272-85BD-8F7793BD7432}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4D21A30-B556-4272-85BD-8F7793BD7432}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4D21A30-B556-4272-85BD-8F7793BD7432}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {C4D21A30-B556-4272-85BD-8F7793BD7432}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4D21A30-B556-4272-85BD-8F7793BD7432}.Release|Any CPU.Build.0 = Release|Any CPU + {ECC48028-F980-49E9-AAC1-65AF5AF3B16F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ECC48028-F980-49E9-AAC1-65AF5AF3B16F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ECC48028-F980-49E9-AAC1-65AF5AF3B16F}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {ECC48028-F980-49E9-AAC1-65AF5AF3B16F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ECC48028-F980-49E9-AAC1-65AF5AF3B16F}.Release|Any CPU.Build.0 = Release|Any CPU + {54FA986A-47D9-4709-9C3E-AEB08CE73EBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54FA986A-47D9-4709-9C3E-AEB08CE73EBD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54FA986A-47D9-4709-9C3E-AEB08CE73EBD}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {54FA986A-47D9-4709-9C3E-AEB08CE73EBD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54FA986A-47D9-4709-9C3E-AEB08CE73EBD}.Release|Any CPU.Build.0 = Release|Any CPU + {686220F1-A72B-45FE-8A6C-148926973096}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {686220F1-A72B-45FE-8A6C-148926973096}.Debug|Any CPU.Build.0 = Debug|Any CPU + {686220F1-A72B-45FE-8A6C-148926973096}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {686220F1-A72B-45FE-8A6C-148926973096}.Release|Any CPU.ActiveCfg = Release|Any CPU + {686220F1-A72B-45FE-8A6C-148926973096}.Release|Any CPU.Build.0 = Release|Any CPU + {39B1BBC2-1924-4B85-9BA4-3319F5C4F3E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39B1BBC2-1924-4B85-9BA4-3319F5C4F3E6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39B1BBC2-1924-4B85-9BA4-3319F5C4F3E6}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {39B1BBC2-1924-4B85-9BA4-3319F5C4F3E6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39B1BBC2-1924-4B85-9BA4-3319F5C4F3E6}.Release|Any CPU.Build.0 = Release|Any CPU + {D648BE46-4208-4DEB-9F2F-0B009B603D0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D648BE46-4208-4DEB-9F2F-0B009B603D0A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D648BE46-4208-4DEB-9F2F-0B009B603D0A}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {D648BE46-4208-4DEB-9F2F-0B009B603D0A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D648BE46-4208-4DEB-9F2F-0B009B603D0A}.Release|Any CPU.Build.0 = Release|Any CPU + {230DA8DF-00EE-47E2-A7B8-54F05A8966F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {230DA8DF-00EE-47E2-A7B8-54F05A8966F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {230DA8DF-00EE-47E2-A7B8-54F05A8966F6}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {230DA8DF-00EE-47E2-A7B8-54F05A8966F6}.Gen|Any CPU.Build.0 = Gen|Any CPU + {230DA8DF-00EE-47E2-A7B8-54F05A8966F6}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {230DA8DF-00EE-47E2-A7B8-54F05A8966F6}.Release|Any CPU.Build.0 = Debug|Any CPU + {49A8B718-DAB6-4D04-874C-0B1EA12826EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {49A8B718-DAB6-4D04-874C-0B1EA12826EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {49A8B718-DAB6-4D04-874C-0B1EA12826EB}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {49A8B718-DAB6-4D04-874C-0B1EA12826EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {49A8B718-DAB6-4D04-874C-0B1EA12826EB}.Release|Any CPU.Build.0 = Release|Any CPU + {FBD76237-D44F-4DC4-BCB1-0DCA85A73052}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBD76237-D44F-4DC4-BCB1-0DCA85A73052}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBD76237-D44F-4DC4-BCB1-0DCA85A73052}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {FBD76237-D44F-4DC4-BCB1-0DCA85A73052}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBD76237-D44F-4DC4-BCB1-0DCA85A73052}.Release|Any CPU.Build.0 = Release|Any CPU + {81EBE4CC-2E70-4E66-89DC-DCBB591343AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {81EBE4CC-2E70-4E66-89DC-DCBB591343AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {81EBE4CC-2E70-4E66-89DC-DCBB591343AB}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {81EBE4CC-2E70-4E66-89DC-DCBB591343AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {81EBE4CC-2E70-4E66-89DC-DCBB591343AB}.Release|Any CPU.Build.0 = Release|Any CPU + {A78DF256-6896-4246-8CF5-8CCBB27760A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A78DF256-6896-4246-8CF5-8CCBB27760A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A78DF256-6896-4246-8CF5-8CCBB27760A5}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {A78DF256-6896-4246-8CF5-8CCBB27760A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A78DF256-6896-4246-8CF5-8CCBB27760A5}.Release|Any CPU.Build.0 = Release|Any CPU + {B528D83E-1AB9-4461-BA5E-993FF882F85B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B528D83E-1AB9-4461-BA5E-993FF882F85B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B528D83E-1AB9-4461-BA5E-993FF882F85B}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {B528D83E-1AB9-4461-BA5E-993FF882F85B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B528D83E-1AB9-4461-BA5E-993FF882F85B}.Release|Any CPU.Build.0 = Release|Any CPU + {7A749C43-60F4-4DDC-894C-5E407B08A3A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A749C43-60F4-4DDC-894C-5E407B08A3A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A749C43-60F4-4DDC-894C-5E407B08A3A9}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {7A749C43-60F4-4DDC-894C-5E407B08A3A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A749C43-60F4-4DDC-894C-5E407B08A3A9}.Release|Any CPU.Build.0 = Release|Any CPU + {74F90FB5-198F-4B90-ACBD-F89E4F81FA1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74F90FB5-198F-4B90-ACBD-F89E4F81FA1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74F90FB5-198F-4B90-ACBD-F89E4F81FA1B}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {74F90FB5-198F-4B90-ACBD-F89E4F81FA1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74F90FB5-198F-4B90-ACBD-F89E4F81FA1B}.Release|Any CPU.Build.0 = Release|Any CPU + {8E90962C-870D-49C6-A260-585D16F13614}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8E90962C-870D-49C6-A260-585D16F13614}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8E90962C-870D-49C6-A260-585D16F13614}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {8E90962C-870D-49C6-A260-585D16F13614}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8E90962C-870D-49C6-A260-585D16F13614}.Release|Any CPU.Build.0 = Release|Any CPU + {ED906FBD-F8BB-40D5-8629-00CE0F963681}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED906FBD-F8BB-40D5-8629-00CE0F963681}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED906FBD-F8BB-40D5-8629-00CE0F963681}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {ED906FBD-F8BB-40D5-8629-00CE0F963681}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED906FBD-F8BB-40D5-8629-00CE0F963681}.Release|Any CPU.Build.0 = Release|Any CPU + {842CEAED-FA0D-4EFF-805A-D37F62AEFB0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {842CEAED-FA0D-4EFF-805A-D37F62AEFB0F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {842CEAED-FA0D-4EFF-805A-D37F62AEFB0F}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {842CEAED-FA0D-4EFF-805A-D37F62AEFB0F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {842CEAED-FA0D-4EFF-805A-D37F62AEFB0F}.Release|Any CPU.Build.0 = Release|Any CPU + {EAE6F008-AB9E-4321-BC31-D50534111971}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EAE6F008-AB9E-4321-BC31-D50534111971}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EAE6F008-AB9E-4321-BC31-D50534111971}.Gen|Any CPU.ActiveCfg = Gen|Any CPU + {EAE6F008-AB9E-4321-BC31-D50534111971}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EAE6F008-AB9E-4321-BC31-D50534111971}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {C1618305-4F0B-4AAA-8386-CA31DCBC5F59} = {4DE45B98-D18F-4E1D-9D2F-A4E40C11BC1F} + {6821E250-3BE8-4CDA-AFEA-FB8C9B4C4106} = {EC34CEBD-EEDE-4CBA-A28A-A71BCB51D5D2} + {90B4EBE9-7CDB-42E0-B5E2-9FCAC4AD52CA} = {3FBE7FA1-700D-4B58-8569-15F533F761D1} + {64FC6860-BD4C-4C11-BF6E-E99BB4D91723} = {75CE8A6F-D63B-491D-8834-8D3BA2127208} + {20A4298E-1929-4125-8D66-CABF330DE633} = {4DE45B98-D18F-4E1D-9D2F-A4E40C11BC1F} + {68387B20-DC7B-4C56-906E-9EC379677DE3} = {64FC6860-BD4C-4C11-BF6E-E99BB4D91723} + {C4D21A30-B556-4272-85BD-8F7793BD7432} = {64FC6860-BD4C-4C11-BF6E-E99BB4D91723} + {ECC48028-F980-49E9-AAC1-65AF5AF3B16F} = {64FC6860-BD4C-4C11-BF6E-E99BB4D91723} + {54FA986A-47D9-4709-9C3E-AEB08CE73EBD} = {1B19D36E-8348-4CAE-A2B8-87A81076539A} + {686220F1-A72B-45FE-8A6C-148926973096} = {1B19D36E-8348-4CAE-A2B8-87A81076539A} + {39B1BBC2-1924-4B85-9BA4-3319F5C4F3E6} = {1B19D36E-8348-4CAE-A2B8-87A81076539A} + {1B19D36E-8348-4CAE-A2B8-87A81076539A} = {75CE8A6F-D63B-491D-8834-8D3BA2127208} + {D648BE46-4208-4DEB-9F2F-0B009B603D0A} = {4DE45B98-D18F-4E1D-9D2F-A4E40C11BC1F} + {230DA8DF-00EE-47E2-A7B8-54F05A8966F6} = {2AE6684B-47A2-46D3-BE4E-C14B05255BB6} + {49A8B718-DAB6-4D04-874C-0B1EA12826EB} = {2AE6684B-47A2-46D3-BE4E-C14B05255BB6} + {FBD76237-D44F-4DC4-BCB1-0DCA85A73052} = {2AE6684B-47A2-46D3-BE4E-C14B05255BB6} + {9A1275C5-2299-4560-ABB6-CE56B9175CF7} = {4DE45B98-D18F-4E1D-9D2F-A4E40C11BC1F} + {81EBE4CC-2E70-4E66-89DC-DCBB591343AB} = {9A1275C5-2299-4560-ABB6-CE56B9175CF7} + {A78DF256-6896-4246-8CF5-8CCBB27760A5} = {9A1275C5-2299-4560-ABB6-CE56B9175CF7} + {B528D83E-1AB9-4461-BA5E-993FF882F85B} = {9A1275C5-2299-4560-ABB6-CE56B9175CF7} + {7A749C43-60F4-4DDC-894C-5E407B08A3A9} = {4DE45B98-D18F-4E1D-9D2F-A4E40C11BC1F} + {74F90FB5-198F-4B90-ACBD-F89E4F81FA1B} = {2C4E653D-8B7A-48BA-A9DB-F9707BC04380} + {DB106708-EA65-4FD3-9362-950CA07DB2E6} = {4DE45B98-D18F-4E1D-9D2F-A4E40C11BC1F} + {8E90962C-870D-49C6-A260-585D16F13614} = {DB106708-EA65-4FD3-9362-950CA07DB2E6} + {ED906FBD-F8BB-40D5-8629-00CE0F963681} = {DB106708-EA65-4FD3-9362-950CA07DB2E6} + {842CEAED-FA0D-4EFF-805A-D37F62AEFB0F} = {DB106708-EA65-4FD3-9362-950CA07DB2E6} + {EAE6F008-AB9E-4321-BC31-D50534111971} = {4DE45B98-D18F-4E1D-9D2F-A4E40C11BC1F} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {103E4CA1-859C-4E6A-9F2D-7FD54A8E687C} + EndGlobalSection +EndGlobal diff --git a/Weknow.EventSource.Backbone/Weknow.EventSource.Backbone.xml b/EventSourcing.Backbone/EventSource.Backbone.xml similarity index 65% rename from Weknow.EventSource.Backbone/Weknow.EventSource.Backbone.xml rename to EventSourcing.Backbone/EventSource.Backbone.xml index 05b0920d..1236099c 100644 --- a/Weknow.EventSource.Backbone/Weknow.EventSource.Backbone.xml +++ b/EventSourcing.Backbone/EventSource.Backbone.xml @@ -1,7 +1,7 @@ - Weknow.EventSource.Backbone + EventSourcing.Backbone diff --git a/EventSourcing.Backbone/EventSourcing.Backbone.csproj b/EventSourcing.Backbone/EventSourcing.Backbone.csproj new file mode 100644 index 00000000..229e3ee3 --- /dev/null +++ b/EventSourcing.Backbone/EventSourcing.Backbone.csproj @@ -0,0 +1,26 @@ + + + + + + + + + README.md + + + + + True + \ + + + + + + + + + + + diff --git a/EventSourcing.Backbone/icon.png b/EventSourcing.Backbone/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/EventSourcing.Backbone/icon.png differ diff --git a/Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions.csproj b/Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions.csproj new file mode 100644 index 00000000..8ac3d16f --- /dev/null +++ b/Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions.csproj @@ -0,0 +1,38 @@ + + + + EventSourcing.Backbone + https://github.com/bnayae/Event-Source-Backbone + + + + README.md + + + + + \ + True + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions/EventSourcingOtel.cs b/Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions/EventSourcingOtel.cs new file mode 100644 index 00000000..6f7c3667 --- /dev/null +++ b/Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions/EventSourcingOtel.cs @@ -0,0 +1,125 @@ +using EventSourcing.Backbone; + +using Microsoft.Extensions.Hosting; + +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + + +// Configuration: https://medium.com/@gparlakov/the-confusion-of-asp-net-configuration-with-environment-variables-c06c545ef732 +// see: +// https://opentelemetry.io/docs/instrumentation/net/getting-started/ +// https://opentelemetry.io/docs/demo/services/cart/ +// https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.Jaeger/README.md#environment-variables +// https://opentelemetry.io/docs/demo/docker-deployment/ + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// core extensions for ASP.NET Core +/// +public static class EventSourcingOtel +{ + #region WithEventSourcingTracing + + #region Overloads + + /// + /// Adds the open-telemetry tracing binding. + /// + /// The build. + /// The host env. + /// Enable to inject additional setting. + /// + public static OpenTelemetryBuilder WithEventSourcingTracing( + this OpenTelemetryBuilder builder, + IHostEnvironment hostEnv, + Action? injection = null) + { + var env = hostEnv.ApplicationName; + return builder.WithEventSourcingTracing(env, injection); + } + + #endregion // Overloads + + /// + /// Adds the open-telemetry tracing binding. + /// + /// The build. + /// The environment. + /// Enable to inject additional setting. + /// + public static OpenTelemetryBuilder WithEventSourcingTracing( + this OpenTelemetryBuilder builder, + Env env, + Action? injection = null) + { + builder + .WithTracing(tracerProviderBuilder => + { + var sources = new[] { (string)env, + EventSourceConstants.TELEMETRY_SOURCE }; + + tracerProviderBuilder + .AddSource(sources) + .ConfigureResource(resource => resource.AddService(env)); + + injection?.Invoke(tracerProviderBuilder); + }); + + return builder; + } + + #endregion // WithEventSourcingTracing + + #region WithEventSourcingMetrics + + #region Overloads + + /// + /// Adds the open-telemetry metrics binding. + /// + /// The build. + /// The host env. + /// The injection. + /// + public static OpenTelemetryBuilder WithEventSourcingMetrics( + this OpenTelemetryBuilder builder, + IHostEnvironment hostEnv, + Action? injection = null) + { + var env = hostEnv.ApplicationName; + return builder.WithEventSourcingMetrics(env, injection); + } + + #endregion // Overloads + + /// + /// Adds the open-telemetry metrics binding. + /// + /// The build. + /// The host environment. + /// The injection. + /// + public static OpenTelemetryBuilder WithEventSourcingMetrics( + this OpenTelemetryBuilder builder, + Env env, + Action? injection = null) + { + builder.WithMetrics(metricsProviderBuilder => + { + injection?.Invoke(metricsProviderBuilder); + metricsProviderBuilder + //.ConfigureResource(resource => resource.AddService(env) + // .AddService(ConsumerChannelConstants.REDIS_CHANNEL_SOURCE)) + .AddMeter(env, + EventSourceConstants.TELEMETRY_SOURCE); + }); + + return builder; + } + + #endregion // WithEventSourcingMetrics +} diff --git a/Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions/Telemetry-Readme.md b/Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions/Telemetry-Readme.md new file mode 100644 index 00000000..d805eee2 --- /dev/null +++ b/Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions/Telemetry-Readme.md @@ -0,0 +1,10 @@ +# Open Telemetry Extensions + + +## Run Jaeger trace collector + +``` docker +docker run --name jaeger-otel --rm -it -e COLLECTOR_OTLP_ENABLED=true -p 16686:16686 -p 4317:4317 -p 4318:4318 jaegertracing/all-in-one:latest +``` + +Check it on: `http://localhost:16686/search` diff --git a/Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions/Types/MetricsExporters.cs b/Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions/Types/MetricsExporters.cs new file mode 100644 index 00000000..3dbaf238 --- /dev/null +++ b/Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions/Types/MetricsExporters.cs @@ -0,0 +1,14 @@ +namespace EventSourcing.Backbone; + +/// +/// Metrics exporter options (open-telemetry) +/// +[Flags] +public enum MetricsExporters +{ + None = 0, + Otlp = 1, + DevConsole = Otlp * 2, + Prometheus = DevConsole * 2, + Default = Otlp | Prometheus +} \ No newline at end of file diff --git a/Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions/Types/TraceExporters.cs b/Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions/Types/TraceExporters.cs new file mode 100644 index 00000000..7beafe98 --- /dev/null +++ b/Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions/Types/TraceExporters.cs @@ -0,0 +1,13 @@ +namespace EventSourcing.Backbone; + +/// +/// Tracing exporter options (open-telemetry) +/// +[Flags] +public enum TraceExporters +{ + None = 0, + Otlp = 1, + DevConsole = Otlp * 2, + Default = Otlp | DevConsole +} \ No newline at end of file diff --git a/Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions/icon.png b/Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/Extensions/EventSourcing.Backbone.OpenTelemetry.Extensions/icon.png differ diff --git a/NuGet.config b/NuGet.config index 15c361f6..1a962935 100644 --- a/NuGet.config +++ b/NuGet.config @@ -8,5 +8,7 @@ + + diff --git a/PlayGroundConsumer/IFooX.cs b/PlayGroundConsumer/IFooX.cs new file mode 100644 index 00000000..d1258b18 --- /dev/null +++ b/PlayGroundConsumer/IFooX.cs @@ -0,0 +1,7 @@ +namespace PlayGroundAbstraction; + +//[EventsContract(EventsContractType.Consumer)] +public interface IFooX : IFoo +{ + +} \ No newline at end of file diff --git a/PlayGroundConsumer/PlayGroundConsumer.csproj b/PlayGroundConsumer/PlayGroundConsumer.csproj new file mode 100644 index 00000000..e524145e --- /dev/null +++ b/PlayGroundConsumer/PlayGroundConsumer.csproj @@ -0,0 +1,26 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + + + True + + + + + + + + diff --git a/PlayGroundConsumer/Program.cs b/PlayGroundConsumer/Program.cs new file mode 100644 index 00000000..3751555c --- /dev/null +++ b/PlayGroundConsumer/Program.cs @@ -0,0 +1,2 @@ +// See https://aka.ms/new-console-template for more information +Console.WriteLine("Hello, World!"); diff --git a/PlayGroundConsumer/icon.png b/PlayGroundConsumer/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/PlayGroundConsumer/icon.png differ diff --git a/Producers/Weknow.EventSource.Backbone.Producers/Builder/FilteredStorageStrategy.cs b/Producers/EventSourcing.Backbone.Producers/Builder/FilteredStorageStrategy.cs similarity index 94% rename from Producers/Weknow.EventSource.Backbone.Producers/Builder/FilteredStorageStrategy.cs rename to Producers/EventSourcing.Backbone.Producers/Builder/FilteredStorageStrategy.cs index 8eee4fbf..c9a87c8c 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers/Builder/FilteredStorageStrategy.cs +++ b/Producers/EventSourcing.Backbone.Producers/Builder/FilteredStorageStrategy.cs @@ -1,6 +1,6 @@ using System.Collections.Immutable; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Wrap Channel Storage with key filtering of the bucket. @@ -12,7 +12,6 @@ internal class FilteredStorageStrategy : IProducerStorageStrategyWithFilter private readonly IProducerStorageStrategy _storage; private readonly Predicate? _filter; private readonly EventBucketCategories _targetType; - private readonly static (string key, string metadata)[] EMPTY_RESULT = Array.Empty<(string key, string metadata)>(); /// /// Initializes a new instance. @@ -30,6 +29,11 @@ public FilteredStorageStrategy( _targetType = targetType; } + /// + /// Gets the name of the storage provider. + /// + public string Name => _storage.Name; + /// /// Determines whether is of the right target type. /// diff --git a/Producers/Weknow.EventSource.Backbone.Producers/Builder/ProducerBuilder.cs b/Producers/EventSourcing.Backbone.Producers/Builder/ProducerBuilder.cs similarity index 81% rename from Producers/Weknow.EventSource.Backbone.Producers/Builder/ProducerBuilder.cs rename to Producers/EventSourcing.Backbone.Producers/Builder/ProducerBuilder.cs index e0e3be08..ec0335ea 100644 --- a/Producers/Weknow.EventSource.Backbone.Producers/Builder/ProducerBuilder.cs +++ b/Producers/EventSourcing.Backbone.Producers/Builder/ProducerBuilder.cs @@ -1,19 +1,18 @@ -using Microsoft.Extensions.Logging; +using EventSourcing.Backbone.Building; -using Weknow.EventSource.Backbone.Building; +using Microsoft.Extensions.Logging; using static System.String; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// Event Source producer builder. /// public class ProducerBuilder : IProducerBuilder, - IProducerShardBuilder, IProducerHooksBuilder, - IProducerStoreStrategyBuilder + IProducerIocStoreStrategyBuilder { /// @@ -70,7 +69,7 @@ IProducerHooksBuilder IProducerBuilder.Merge( #region Validation if (Plan == null) - throw new ArgumentNullException(nameof(Plan)); + throw new EventSourcingException($"{nameof(Plan)} is null"); #endregion // Validation @@ -99,8 +98,32 @@ IProducerStoreStrategyBuilder IProducerBuilder.UseChannel( return new ProducerBuilder(prms); } + /// + /// Choose the communication channel provider. + /// + /// The channel provider. + /// Dependency injection provider. + /// + IProducerIocStoreStrategyBuilder IProducerBuilder.UseChannel( + IServiceProvider serviceProvider, + Func channel) + { + var prms = Plan.UseChannel(channel, serviceProvider); + return new ProducerBuilder(prms); + } + #endregion // UseChannel + #region ServiceProvider + + /// + /// Gets the service provider. + /// + IServiceProvider IProducerIocStoreStrategyBuilder.ServiceProvider => + Plan?.ServiceProvider ?? throw new EventSourcingException("ServiceProvider is null"); + + #endregion // ServiceProvider + #region AddStorageStrategy /// @@ -138,7 +161,7 @@ async Task Local(ILogger logger) /// Origin environment of the message /// /// - IProducerPartitionBuilder IProducerBuilderEnvironment.Environment(Env? environment) + IProducerUriBuilder IProducerBuilderEnvironment.Environment(Env? environment) { if (environment == null) return this; @@ -163,49 +186,51 @@ IProducerSpecializeBuilder IProducerBuilderEnvironment + /// Fetch the origin environment of the message from an environment variable. + /// + /// The environment variable key. + /// + /// EnvironmentFromVariable failed, [{environmentVariableKey}] not found! + IProducerUriBuilder IProducerBuilderEnvironment.EnvironmentFromVariable(string environmentVariableKey) + { + string environment = Environment.GetEnvironmentVariable(environmentVariableKey) ?? throw new EventSourcingException($"EnvironmentFromVariable failed, [{environmentVariableKey}] not found!"); + var prms = Plan.WithEnvironment(environment); + return new ProducerBuilder(prms); + } + /// - /// Partition key represent logical group of - /// event source shards. - /// For example assuming each ORDERING flow can have its - /// own messaging sequence, yet can live concurrency with - /// other ORDER's sequences. - /// The partition will let consumer the option to be notify and - /// consume multiple shards from single consumer. - /// This way the consumer can handle all orders in - /// central place without affecting sequence of specific order - /// flow or limiting the throughput. + /// Fetch the origin environment of the message from an environment variable. /// - /// The partition key. + /// The environment variable key. /// - IProducerShardBuilder IProducerPartitionBuilder.Partition(string partition) + /// EnvironmentFromVariable failed, [{environmentVariableKey}] not found! + IProducerSpecializeBuilder IProducerBuilderEnvironment.EnvironmentFromVariable(string environmentVariableKey) { - var prms = Plan.WithPartition(partition); + string environment = Environment.GetEnvironmentVariable(environmentVariableKey) ?? throw new EventSourcingException($"EnvironmentFromVariable failed, [{environmentVariableKey}] not found!"); + var prms = Plan.WithEnvironment(environment); return new ProducerBuilder(prms); } - #endregion // Partition + #endregion // EnvironmentFromVariable - #region Shard + #region Key /// - /// Shard key represent physical sequence. - /// Use same shard when order is matter. - /// For example: assuming each ORDERING flow can have its - /// own messaging sequence, in this case you can split each - /// ORDER into different shard and gain performance bust.. + /// The stream's key /// - /// The shard key. + /// The URI. /// - /// - IProducerHooksBuilder IProducerShardBuilder.Shard(string shard) + IProducerHooksBuilder IProducerUriBuilder.Uri(string uri) { - var prms = Plan.WithShard(shard); + var prms = Plan.WithKey(uri); return new ProducerBuilder(prms); } - #endregion // Shard + #endregion // Key #region WithOptions @@ -317,26 +342,6 @@ private ProducerBuilder WithLogger(ILogger logger) return new ProducerBuilder(prms); } - /// - /// Attach logger. - /// - /// The logger. - /// - IProducerShardBuilder IProducerLoggerBuilder.WithLogger(ILogger logger) - { - return WithLogger(logger); - } - - /// - /// Attach logger. - /// - /// The logger. - /// - IProducerOptionsBuilder IProducerLoggerBuilder.WithLogger(ILogger logger) - { - return WithLogger(logger); - } - /// /// Attach logger. /// @@ -352,7 +357,7 @@ IProducerSpecializeBuilder IProducerLoggerBuilder.Wi /// /// The logger. /// - IProducerPartitionBuilder IProducerLoggerBuilder.WithLogger(ILogger logger) + IProducerUriBuilder IProducerLoggerBuilder.WithLogger(ILogger logger) { return WithLogger(logger); } @@ -391,7 +396,9 @@ T IProducerSpecializeBuilder.Build(Func factory) /// /// + /// Useful for data migration at the raw data level. + /// This producer can be attached to a subscriber and route it data elesewhere. + /// For example, alternative cloud provider, qa environement, etc.]]> /// /// /// @@ -435,7 +442,7 @@ IRawProducer IProducerRawBuilder.BuildRaw(RawProducerOptions? options) /// Can use for scenario like routing between environment like dev vs. prod or aws vs azure. /// /// - private class Router : IProducerOverrideBuilder where T : class + private sealed class Router : IProducerOverrideBuilder where T : class { private readonly ProducerPlan _plan; @@ -482,14 +489,14 @@ T IProducerOverrideBuildBuilder.Build(Func factory) /// /// Dynamic override of the stream id before sending. - /// Can use for scenario like routing between environment like dev vs. prod or aws vs azure. + /// Can use for scenario like routing between environment like dev vs. prod or AWS vs azure. /// /// The routing strategy. /// - IProducerOverrideBuildBuilder IProducerOverrideBuilder.Strategy(Func routeStrategy) + IProducerOverrideBuildBuilder IProducerOverrideBuilder.Strategy(Func routeStrategy) { - var (environment, partition, shard) = routeStrategy(_plan); - var plan = _plan.WithEnvironment(environment ?? _plan.Environment, partition, shard); + var (environment, uri) = routeStrategy(_plan); + var plan = _plan.WithEnvironment(environment ?? _plan.Environment, uri); return new Router(plan); } @@ -503,7 +510,7 @@ IProducerOverrideBuildBuilder IProducerOverrideBuilder.Strategy(Func /// The environment. /// - IProducerOverridePartitionBuilder IProducerOverrideEnvironmentBuilder.Environment(Env environment) + IProducerOverrideUriBuilder IProducerOverrideEnvironmentBuilder.Environment(Env environment) { var plan = _plan.WithEnvironment(environment ?? _plan.Environment); return new Router(plan); @@ -511,39 +518,22 @@ IProducerOverridePartitionBuilder IProducerOverrideEnvironmentBuilder.Envi #endregion // Environment - #region Partition - - /// - /// Override the partition. - /// Can use for scenario like routing between environment like dev vs. prod or aws vs azure. - /// - /// The partition. - /// The type. - /// - IProducerOverrideShardBuilder IProducerOverridePartitionBuilder.Partition(string partition, RouteAssignmentType type) - { - var plan = _plan.WithPartition(partition, type: type); - return new Router(plan); - } - - #endregion // Partition - - #region Shard + #region Uri /// - /// Override the shard. + /// Override the URI. /// Can use for scenario like routing between environment like dev vs. prod or aws vs azure. /// - /// The shard. + /// The URI. /// The type. /// - IProducerOverrideBuildBuilder IProducerOverrideShardBuilder.Shard(string shard, RouteAssignmentType type) + IProducerOverrideBuildBuilder IProducerOverrideUriBuilder.Uri(string uri, RouteAssignmentType type) { - var plan = _plan.WithShard(shard, type); + var plan = _plan.WithKey(uri, type: type); return new Router(plan); } - #endregion // Shard + #endregion // Uri } #endregion // Router @@ -553,8 +543,8 @@ IProducerOverrideBuildBuilder IProducerOverrideShardBuilder.Shard(string s /// /// Raw producer (useful for cluster migration) /// - /// - private class RawProducer : IRawProducer + /// + private sealed class RawProducer : IRawProducer { private readonly IProducerPlan _plan; private readonly RawProducerOptions? _options; @@ -585,14 +575,13 @@ public async ValueTask Produce(Announcement data) var strategies = await _plan.StorageStrategiesAsync; Metadata metadata = data.Metadata; Metadata meta = metadata; - if ((_options?.KeepOriginalMeta ?? false) == false) + if (!(_options?.KeepOriginalMeta ?? false)) { meta = meta with { MessageId = Guid.NewGuid().ToString("N"), Environment = IsNullOrEmpty(_plan.Environment) ? metadata.Environment : _plan.Environment, - Partition = IsNullOrEmpty(_plan.Partition) ? metadata.Partition : _plan.Partition, - Shard = IsNullOrEmpty(_plan.Shard) ? metadata.Shard : _plan.Shard, + Uri = IsNullOrEmpty(_plan.Uri) ? metadata.Uri : _plan.Uri, Origin = MessageOrigin.Copy, Linked = metadata, }; diff --git a/Producers/EventSourcing.Backbone.Producers/EventSourcing.Backbone.Producers.csproj b/Producers/EventSourcing.Backbone.Producers/EventSourcing.Backbone.Producers.csproj new file mode 100644 index 00000000..18e4efe6 --- /dev/null +++ b/Producers/EventSourcing.Backbone.Producers/EventSourcing.Backbone.Producers.csproj @@ -0,0 +1,22 @@ + + + + README.md + + + + + True + \ + + + + + + + + + + + + diff --git a/Producers/EventSourcing.Backbone.Producers/ProducerDefaultSegmentationStrategy.cs b/Producers/EventSourcing.Backbone.Producers/ProducerDefaultSegmentationStrategy.cs new file mode 100644 index 00000000..3c66589f --- /dev/null +++ b/Producers/EventSourcing.Backbone.Producers/ProducerDefaultSegmentationStrategy.cs @@ -0,0 +1,19 @@ +namespace EventSourcing.Backbone; + + +public class ProducerDefaultSegmentationStrategy : + IProducerAsyncSegmentationStrategy +{ + ValueTask IProducerAsyncSegmentationStrategy. + TryClassifyAsync( + Bucket segments, + string operation, + string argumentName, + T producedData, + EventSourceOptions options) + { + ReadOnlyMemory data = options.Serializer.Serialize(producedData); + string key = argumentName; + return segments.Add(key, data).ToValueTask(); + } +} diff --git a/Producers/EventSourcing.Backbone.Producers/ProducerTelemetryExtensions.cs b/Producers/EventSourcing.Backbone.Producers/ProducerTelemetryExtensions.cs new file mode 100644 index 00000000..da6e15a8 --- /dev/null +++ b/Producers/EventSourcing.Backbone.Producers/ProducerTelemetryExtensions.cs @@ -0,0 +1,40 @@ +using System.Diagnostics; + +namespace EventSourcing.Backbone.Producers; + +public static class ProducerTelemetryExtensions +{ + #region StartProducerTrace + + /// + /// Starts a consumer trace. + /// + /// The activity source. + /// The metadata. + /// The tags action. + /// + public static Activity? StartProducerTrace(this ActivitySource activitySource, + Metadata meta, + Action? tagsAction = null) + { + // Start an activity with a name following the semantic convention of the OpenTelemetry messaging specification. + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/messaging.md#span-name + + if(!activitySource.HasListeners()) + return null; + + var tags = new ActivityTagsCollection(); + var t = new TagAddition(tags); + tagsAction?.Invoke(t); + + + var activityName = activitySource.HasListeners() ? $"producer.{meta.Operation.ToDash()}.send" : string.Empty; + Activity? activity = activitySource.StartActivity(activityName, ActivityKind.Producer); + + meta.InjectTelemetryTags(activity); + + return activity; + } + + #endregion // StartProducerTrace +} diff --git a/Producers/EventSourcing.Backbone.Producers/icon.png b/Producers/EventSourcing.Backbone.Producers/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/Producers/EventSourcing.Backbone.Producers/icon.png differ diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerBuilderEnvironment.cs b/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerBuilderEnvironment.cs deleted file mode 100644 index 4f5895d2..00000000 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerBuilderEnvironment.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Weknow.EventSource.Backbone.Building -{ - /// - /// Origin environment of the message - /// - public interface IProducerBuilderEnvironment - { - /// - /// Origin environment of the message - /// - /// The environment (null: keep current environment, empty: reset the environment to nothing). - /// - T Environment(Env? environment); - } -} diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerEnvironmentBuilder.cs b/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerEnvironmentBuilder.cs deleted file mode 100644 index 51f9eea4..00000000 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerEnvironmentBuilder.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Weknow.EventSource.Backbone.Building -{ - /// - /// Origin environment of the message - /// - public interface IProducerEnvironmentBuilder : IProducerPartitionBuilder, IProducerBuilderEnvironment, - IProducerRawBuilder - { - } -} diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerPartitionBuilder.cs b/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerPartitionBuilder.cs deleted file mode 100644 index eac5ef22..00000000 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerPartitionBuilder.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Weknow.EventSource.Backbone.Building -{ - - /// - /// Partition key represent logical group - /// - public interface IProducerPartitionBuilder : IProducerRawBuilder, IProducerLoggerBuilder - { - /// - /// Partition key represent logical group of - /// event source shards. - /// For example assuming each ORDERING flow can have its - /// own messaging sequence, yet can live concurrency with - /// other ORDER's sequences. - /// The partition will let consumer the option to be notify and - /// consume multiple shards from single consumer. - /// This way the consumer can handle all orders in - /// central place without affecting sequence of specific order - /// flow or limiting the throughput. - /// - /// The partition key. - /// - IProducerShardBuilder Partition(string partition); - } -} diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerShardBuilder.cs b/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerShardBuilder.cs deleted file mode 100644 index a6719a85..00000000 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IProducerShardBuilder.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Weknow.EventSource.Backbone.Building -{ - /// - /// Enable configuration. - /// - public interface IProducerShardBuilder : IProducerRawBuilder, IProducerLoggerBuilder - { - /// - /// Shard key represent physical sequence. - /// Use same shard when order is matter. - /// For example: assuming each ORDERING flow can have its - /// own messaging sequence, in this case you can split each - /// ORDER into different shard and gain performance bust.. - /// - /// The shard key. - /// - IProducerHooksBuilder Shard(string shard); - } -} diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IRawProducer.cs b/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IRawProducer.cs deleted file mode 100644 index 79a1c129..00000000 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/IRawProducer.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Weknow.EventSource.Backbone -{ - /// - /// Event Source raw producer. - /// - public interface IRawProducer - { - /// - /// - /// - /// - ValueTask Produce(Announcement data); - } -} diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/Override/IProducerOverridePartitionBuilder.cs b/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/Override/IProducerOverridePartitionBuilder.cs deleted file mode 100644 index b33ba84e..00000000 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Builder/Building/Override/IProducerOverridePartitionBuilder.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Weknow.EventSource.Backbone.Building -{ - /// - /// Enable dynamic transformation of the stream id before sending. - /// Can use for scenario like routing between environment like dev vs. prod or aws vs azure. - /// - public interface IProducerOverridePartitionBuilder : IProducerOverrideShardBuilder where T : class - { - - /// - /// Override the partition. - /// Can use for scenario like routing between environment like dev vs. prod or aws vs azure. - /// - /// The partition. - /// The type. - /// - IProducerOverrideShardBuilder Partition(string partition, RouteAssignmentType type = RouteAssignmentType.Prefix); - } -} diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/TelemetryrExtensions.cs b/Producers/Weknow.EventSource.Backbone.Producers.Contracts/TelemetryrExtensions.cs deleted file mode 100644 index b2d94f41..00000000 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/TelemetryrExtensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Collections.Immutable; -using System.Diagnostics; - -using OpenTelemetry; -using OpenTelemetry.Context.Propagation; - -namespace Weknow.EventSource.Backbone -{ - public static class TelemetryrExtensions - { - private static readonly TextMapPropagator Propagator = Propagators.DefaultTextMapPropagator; - - #region StartSpanScope - - /// - /// Inject telemetry span to the channel property. - /// - /// - /// The activity. - /// The meta. - /// The entries builder. - /// The injection strategy. - /// - public static void InjectSpan( - this Activity? activity, - Metadata meta, - ImmutableArray.Builder entriesBuilder, - Action.Builder, string, string> injectStrategy) - { - // Depending on Sampling (and whether a listener is registered or not), the - // activity above may not be created. - // If it is created, then propagate its context. - // If it is not created, the propagate the Current context, - // if any. - if (activity != null) - { - Activity.Current = activity; - } - ActivityContext contextToInject = Activity.Current?.Context ?? default; - - // Inject the ActivityContext into the message metadata to propagate trace context to the receiving service. - Propagator.Inject(new PropagationContext(contextToInject, Baggage.Current), entriesBuilder, injectStrategy); - } - - #endregion // StartSpanScope - } -} diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Weknow.EventSource.Backbone.Producers.Contracts.csproj b/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Weknow.EventSource.Backbone.Producers.Contracts.csproj deleted file mode 100644 index 5444bc49..00000000 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Weknow.EventSource.Backbone.Producers.Contracts.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Weknow.EventSource.Backbone.Producers.Contracts.xml b/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Weknow.EventSource.Backbone.Producers.Contracts.xml deleted file mode 100644 index bf5da7b8..00000000 --- a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/Weknow.EventSource.Backbone.Producers.Contracts.xml +++ /dev/null @@ -1,985 +0,0 @@ - - - - Weknow.EventSource.Backbone.Producers.Contracts - - - - - Origin environment of the message - - - - - Origin environment of the message - - The environment. - - - - - Event Source producer builder. - - - - - Gets the producer's plan. - - - - - Adds Producer interceptor (stage = after serialization). - - The interceptor. - - - - - Adds Producer interceptor (Timing: after serialization). - - The interceptor. - - - - - Register segmentation strategy, - Segmentation responsible of splitting an instance into segments. - Segments is how the producer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - - A strategy of segmentation. - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Register segmentation strategy, - Segmentation responsible of splitting an instance into segments. - Segments is how the producer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - - A strategy of segmentation. - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Event Source producer builder. - - - - - Attach logger. - - The logger. - - - - - Enable configuration. - - - - - Apply configuration. - - - - - Partition key represent logical group - - - - - Partition key represent logical group of - event source shards. - For example assuming each ORDERING flow can have its - own messaging sequence, yet can live concurrency with - other ORDER's sequences. - The partition will let consumer the option to be notify and - consume multiple shards from single consumer. - This way the consumer can handle all orders in - central place without affecting sequence of specific order - flow or limiting the throughput. - - The partition key. - - - - - Enable configuration. - - - - - Shard key represent physical sequence. - Use same shard when order is matter. - For example: assuming each ORDERING flow can have its - own messaging sequence, in this case you can split each - ORDER into different shard and gain performance bust.. - - The shard key. - - - - - Event Source producer builder. - - - - - or ValueTask). - Nothing but method allowed on this interface]]> - - The contract of the proxy / adapter - The factory. - - - - - Enable dynamic transformation of the stream id before sending. - Can use for scenario like routing between environment like dev vs. prod or aws vs azure. - - - - - Bridge segmentation - - - - - Initializes a new instance. - - The synchronize. - - - - Unique name which represent the correlation - between the producer and consumer interceptor. - It's recommended to use URL format. - - - - - Interception operation. - - The metadata. - The segments. - - Data which will be available to the - consumer stage of the interception. - - - - - Responsible of splitting an instance into segments. - Segments is how the producer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Classifies instance into different segments. - Segments is how the producer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - - - The segments which was collect so far. - It start as Empty and flow though all the registered segmentation strategies. - The operation's key which represent the method call at the - producer proxy. - This way you can segment same type into different slot. - Name of the argument. - The produced data. - The options. - - bytes for each segment or - Empty if don't responsible for segmentation of the type. - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Bridge segmentation - - - - - Initializes a new instance. - - The synchronize. - - - - Try to classifies instance into different segments. - Segments is how the producer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - EXPECTED to return the segments argument if it not responsible of - specific parameter handling. - - - The segments which was collect so far. - It start as Empty and flow though all the registered segmentation strategies. - The operation's key which represent the method call at the - producer proxy. - This way you can segment same type into different slot. - Name of the argument. - The produced data. - The options. - - bytes for each segment or - the segments argument if don't responsible for segmentation of the type. - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Responsible to save information to storage. - The information can be either Segmentation or Interception. - When adding it via the builder it can be arrange in a chain in order of having - 'Chain of Responsibility' for saving different parts into different storage (For example GDPR's PII). - Alternative, chain can serve as a cache layer. - - - - - Determines whether is of the right target type. - - The type. - - - - Enable configuration. - - - - - - Adds the storage strategy (Segment / Interceptions). - Will use default storage (REDIS Hash) when empty. - When adding more than one it will to all, act as a fall-back (first win, can use for caching). - It important the consumer's storage will be in sync with this setting. - - Storage strategy provider. - Type of the target. - The filter of which keys in the bucket will be store into this storage. - - - - - Enable dynamic transformation of the stream id before sending. - Can use for scenario like routing between environment like dev vs. prod or aws vs azure. - - - - - or ValueTask). - Nothing but method allowed on this interface]]> - - The factory. - - - - - Enable dynamic transformation of the stream id before sending. - Can use for scenario like routing between environment like dev vs. prod or aws vs azure. - - - - - Dynamic override of the stream id before sending. - Can use for scenario like routing between environment like dev vs. prod or aws vs azure. - - The routing strategy. - - - - - Enable dynamic transformation of the stream id before sending. - Can use for scenario like routing between environment like dev vs. prod or aws vs azure. - - - - - Override the environment. - Can use for scenario like routing between environment like dev vs. prod or aws vs azure. - - The environment. - - - - - Enable dynamic transformation of the stream id before sending. - Can use for scenario like routing between environment like dev vs. prod or aws vs azure. - - - - - Override the partition. - Can use for scenario like routing between environment like dev vs. prod or aws vs azure. - - The partition. - The type. - - - - - Enable dynamic transformation of the stream id before sending. - Can use for scenario like routing between environment like dev vs. prod or aws vs azure. - - - - - Override the shard. - Can use for scenario like routing between environment like dev vs. prod or aws vs azure. - - The shard. - The type. - - - - - the type of the routing assignment - - - - - Event Source producer builder. - - - - - Choose the communication channel provider. - - The channel provider. - - - - - Merges multiple channels of same contract into single - producer for broadcasting messages via all channels. - - The first channel. - The second channel. - The others channels. - - - - - Producer stage of an interception operation provider. - It can be use for variety of responsibilities like - flowing auth context or traces, producing metrics, etc. - - - - - - Interception operation. - - The metadata. - The segments. - - Data which will be available to the - consumer stage of the interception. - - - - - Producer stage of an interception operation provider. - It can be use for variety of responsibilities like - flowing auth context or traces, producing metrics, etc. - - - - - - Interception operation. - - The metadata. - Data which will be available to the - consumer stage of the interception. - - - - Plan contract - - - - - - Gets the communication channel provider. - - - - - Gets the storage strategy. - By design the stream should hold minimal information while the main payload - is segmented and can stored outside of the stream. - This pattern will help us to split data for different reasons, for example GDPR PII (personally identifiable information). - - - - - Gets the forwards pipelines. - Result of merging multiple channels. - - - - - common plan properties - - - - - Producer interceptors (Timing: after serialization). - - - The interceptors. - - - - - Gets the logger. - - - - - Gets the configuration. - - - - - Segmentation responsible of splitting an instance into segments. - Segments is how the producer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Plan builder contract - - - - - - Gets the communication channel provider factory. - - - - - Gets the storage strategy. - By design the stream should hold minimal information while the main payload - is segmented and can stored outside of the stream. - This pattern will help us to split data for different reasons, for example GDPR PII (personally identifiable information). - - - - - Gets the forwards pipelines. - Result of merging multiple channels. - - - - - Buildings the plan. - - - - - - common plan routing properties - - - - - The origin environment of the message - - - - - Partition key represent logical group of - event source shards. - For example assuming each ORDERING flow can have its - own messaging sequence, yet can live concurrency with - other ORDER's sequences. - The partition will let consumer the option to be notify and - consume multiple shards from single consumer. - This way the consumer can handle all orders in - central place without affecting sequence of specific order - flow or limiting the throughput. - - - - - Shard key represent physical sequence. - Use same shard when order is matter. - For example: assuming each ORDERING flow can have its - own messaging sequence, in this case you can split each - ORDER into different shard and gain performance bust.. - - - - - Hold builder definitions. - Define the consumer execution pipeline. - - - - - Initializes a new instance. - - - - - Initializes a new instance. - - The copy from. - The channel. - The channel. - The environment. - The partition. - The shard. - The logger. - The options. - The segmentation strategies. - The interceptors. - The routes. - Result of merging multiple channels. - The forward channels. - The storage strategy. - - - - Gets the communication channel provider. - - - - - Gets the communication channel provider. - - - - - Gets the storage strategy. - By design the stream should hold minimal information while the main payload - is segmented and can stored outside of the stream. - This pattern will help us to split data for different reasons, for example GDPR PII (personally identifiable information). - - - - - Gets the storage strategy. - By design the stream should hold minimal information while the main payload - is segmented and can stored outside of the stream. - This pattern will help us to split data for different reasons, for example GDPR PII (personally identifiable information). - - - - - Gets the logger. - - - - - Origin environment of the message - - - - - Partition key represent logical group of - event source shards. - For example assuming each ORDERING flow can have its - own messaging sequence, yet can live concurrency with - other ORDER's sequences. - The partition will let consumer the option to be notify and - consume multiple shards from single consumer. - This way the consumer can handle all orders in - central place without affecting sequence of specific order - flow or limiting the throughput. - - - - - Shard key represent physical sequence. - Use same shard when order is matter. - For example: assuming each ORDERING flow can have its - own messaging sequence, in this case you can split each - ORDER into different shard and gain performance bust.. - - - - - Gets the configuration. - - - - - Segmentation responsible of splitting an instance into segments. - Segments is how the producer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Producer interceptors (Timing: after serialization). - - - The interceptors. - - - - - Gets the forwards pipelines. - Result of merging multiple channels. - - - - - Gets the forwards pipelines. - Result of merging multiple channels. - - - - - Routes are sub-pipelines are results of merge operation - which can split same payload into multiple partitions or shards. - - - - - Assign channel. - - The channel factory. - - - - - Attach logger. - - The logger. - - - - - Attach Storage Strategy. - - The storage strategy. - - - - - Withes the options. - - The options. - - - - - Withes the environment. - - The environment. - The partition. - The shard. - The type (only for partition and shard). - - - - - Withes the partition. - - The partition. - The shard. - The type. - - - - - Withes the shard. - - The shard. - The type. - - - - - Adds the route. - - The route. - - - - - Adds the segmentation. - - The segmentation. - - - - - Adds the interceptor. - - The interceptor. - - - - - Not operational channel - - - - - - Builds this instance. - - - - - - Handle the producing pipeline - CodeGenerator : generate class which inherit from ProducerPipeline - ---------- ProducerPipeline - pipeline which invoke on each call ----------- - classify-commands = - parameters.Select - CreateClassificationAdaptor(operation, argumentName, producedData) - return ClassifyArgumentAsync - SendAsync(operation, classifyAdaptors) // recursive - Channel.SendAsync(announcement) - - - - - Initializes a new instance. - - The plan. - - - - Initializes a new instance. - - The plan. - - - - Classify the operation payload from method arguments. - - - The operation. - Name of the argument. - The produced data. - - - MUST BE PROTECTED, called from the generated code - - - - - Classifies the operation's argument. - - - The plan. - The payload. - The operation. - Name of the argument. - The produced data. - - - - - Bridge classification of single operation's argument. - Get the argument data and pass it to the segmentation strategies. - - - The strategy. - The options. - The operation. - Name of the argument. - The produced data. - - - - - Call interceptors and store their intercepted data - (which will be use by the consumer's interceptors). - - The interceptors. - The metadata. - The payload. - The interceptors data. - - - - - Sends the produced data via the channel. - - The operation. - The classify strategy adaptors. - - - MUST BE PROTECTED, called from the generated code - - - - - Sends the produced data via the channel. - - The plan. - The identifier. - The payload. - The interceptors data. - The operation. - The classify strategy adaptors. - - - - - Channel provider responsible for passing the actual message - from producer to consumer. - - - - - Sends raw announcement. - - The raw announcement data. - The storage strategy. - - Return the message id - - - - - Responsible to save information to storage. - The information can be either Segmentation or Interception. - When adding it via the builder it can be arrange in a chain in order of having - 'Chain of Responsibility' for saving different parts into different storage (For example GDPR's PII). - Alternative, chain can serve as a cache layer. - - - - - Saves the bucket information. - - The identifier. - Either Segments or Interceptions. - The type. - The metadata. - The cancellation. - - Array of metadata entries which can be used by the consumer side storage strategy, in order to fetch the data. - - - - - Responsible of splitting an instance into segments. - Segments is how the producer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Try to classifies instance into different segments. - Segments is how the producer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - EXPECTED to return the segments argument if it not responsible of - specific parameter handling. - - - The segments which was collect so far. - It start as Empty and flow though all the registered segmentation strategies. - The operation's key which represent the method call at the - producer proxy. - This way you can segment same type into different slot. - Name of the argument. - The produced data. - The options. - - bytes for each segment or - the segments argument if don't responsible for segmentation of the type. - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Inject telemetry span to the channel property. - - - The activity. - The meta. - The entries builder. - The injection strategy. - - - - diff --git a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/icon.png b/Producers/Weknow.EventSource.Backbone.Producers.Contracts/icon.png deleted file mode 100644 index d6811ad8..00000000 Binary files a/Producers/Weknow.EventSource.Backbone.Producers.Contracts/icon.png and /dev/null differ diff --git a/Producers/Weknow.EventSource.Backbone.Producers/ProducerDefaultSegmentationStrategy.cs b/Producers/Weknow.EventSource.Backbone.Producers/ProducerDefaultSegmentationStrategy.cs deleted file mode 100644 index e2a8eb6d..00000000 --- a/Producers/Weknow.EventSource.Backbone.Producers/ProducerDefaultSegmentationStrategy.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Weknow.EventSource.Backbone -{ - - public class ProducerDefaultSegmentationStrategy : - IProducerAsyncSegmentationStrategy - { - ValueTask IProducerAsyncSegmentationStrategy. - TryClassifyAsync( - Bucket segments, - string operation, - string argumentName, - T producedData, - EventSourceOptions options) - { - ReadOnlyMemory data = options.Serializer.Serialize(producedData); - string key = argumentName; - return segments.Add(key, data).ToValueTask(); - } - } -} diff --git a/Producers/Weknow.EventSource.Backbone.Producers/Weknow.EventSource.Backbone.Producers.csproj b/Producers/Weknow.EventSource.Backbone.Producers/Weknow.EventSource.Backbone.Producers.csproj deleted file mode 100644 index f9fe182f..00000000 --- a/Producers/Weknow.EventSource.Backbone.Producers/Weknow.EventSource.Backbone.Producers.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/Producers/Weknow.EventSource.Backbone.Producers/Weknow.EventSource.Backbone.Producers.xml b/Producers/Weknow.EventSource.Backbone.Producers/Weknow.EventSource.Backbone.Producers.xml deleted file mode 100644 index c2f1e6a5..00000000 --- a/Producers/Weknow.EventSource.Backbone.Producers/Weknow.EventSource.Backbone.Producers.xml +++ /dev/null @@ -1,286 +0,0 @@ - - - - Weknow.EventSource.Backbone.Producers - - - - - Wrap Channel Storage with key filtering of the bucket. - Useful for 'Chain of Responsibility' by saving different parts - into different storage (For example GDPR's PII). - - - - - Initializes a new instance. - - The actual storage provider. - The filter according to keys. - Type of the target. - - - - Determines whether is of the right target type. - - The type. - - - - - Saves the bucket information. - - The identifier. - Either Segments or Interceptions. - The type. - The metadata. - The cancellation. - - Array of metadata entries which can be used by the consumer side storage strategy, in order to fetch the data. - - - - - Event Source producer builder. - - - - - Event Source producer builder. - - - - - Initializes a new instance. - - - - - Initializes a new instance. - - The plan. - - - - Gets the producer's plan. - - - - - Merges multiple channels of same contract into single - producer for broadcasting messages via all channels. - - The first channel. - The second channel. - The others channels. - - - - - - Choose the communication channel provider. - - The channel provider. - - - - - Adds the storage strategy (Segment / Interceptions). - Will use default storage (REDIS Hash) when empty. - When adding more than one it will to all, act as a fall-back (first win, can use for caching). - It important the consumer's storage will be in sync with this setting. - - Storage strategy provider. - Type of the target. - The filter of which keys in the bucket will be store into this storage. - - - - - Origin environment of the message - - - - - - Partition key represent logical group of - event source shards. - For example assuming each ORDERING flow can have its - own messaging sequence, yet can live concurrency with - other ORDER's sequences. - The partition will let consumer the option to be notify and - consume multiple shards from single consumer. - This way the consumer can handle all orders in - central place without affecting sequence of specific order - flow or limiting the throughput. - - The partition key. - - - - - Shard key represent physical sequence. - Use same shard when order is matter. - For example: assuming each ORDERING flow can have its - own messaging sequence, in this case you can split each - ORDER into different shard and gain performance bust.. - - The shard key. - - - - - - Apply configuration. - - - - - - - - Register segmentation strategy, - Segmentation responsible of splitting an instance into segments. - Segments is how the producer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - - A strategy of segmentation. - - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Register segmentation strategy, - Segmentation responsible of splitting an instance into segments. - Segments is how the producer sending its raw data to - the consumer. It's in a form of dictionary when - keys represent the different segments - and the value represent serialized form of the segment's data. - - A strategy of segmentation. - - - - Examples for segments can be driven from regulation like - GDPR (personal, non-personal data), - Technical vs Business aspects, etc. - - - - - Adds Producer interceptor (stage = after serialization). - - The interceptor. - - - - - - Adds Producer interceptor (Timing: after serialization). - - The interceptor. - - - - - - Attach logger. - - The logger. - - - - - or ValueTask). - Nothing but method allowed on this interface]]> - - The contract of the proxy / adapter - The factory. - - - - - Enable dynamic transformation of the stream id before sending. - Can use for scenario like routing between environment like dev vs. prod or aws vs azure. - - - - - - - Enable dynamic transformation of the stream id before sending. - Can use for scenario like routing between environment like dev vs. prod or aws vs azure. - - - - - - Initializes a new instance. - - The plan. - - - - or ValueTask). - Nothing but method allowed on this interface]]> - - The factory. - - - - - Dynamic override of the stream id before sending. - Can use for scenario like routing between environment like dev vs. prod or aws vs azure. - - The routing strategy. - - - - - Override the environment. - Can use for scenario like routing between environment like dev vs. prod or aws vs azure. - - The environment. - - - - - Override the partition. - Can use for scenario like routing between environment like dev vs. prod or aws vs azure. - - The partition. - The type. - - - - - Override the shard. - Can use for scenario like routing between environment like dev vs. prod or aws vs azure. - - The shard. - The type. - - - - diff --git a/Producers/Weknow.EventSource.Backbone.Producers/icon.png b/Producers/Weknow.EventSource.Backbone.Producers/icon.png deleted file mode 100644 index d6811ad8..00000000 Binary files a/Producers/Weknow.EventSource.Backbone.Producers/icon.png and /dev/null differ diff --git a/README.md b/README.md index 44bdf7a3..741a9d78 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,36 @@ -# Event-Source-Backbone [![Deploy](https://github.com/weknow-network/Event-Source-Backbone/actions/workflows/build-publish-v2.yml/badge.svg)](https://github.com/weknow-network/Event-Source-Backbone/actions/workflows/build-publish-v2.yml) [![Source Generator](https://github.com/weknow-network/Event-Source-Backbone/actions/workflows/src-gen.yml/badge.svg)](https://github.com/weknow-network/Event-Source-Backbone/actions/workflows/src-gen.yml) -[![NuGet](https://img.shields.io/nuget/v/Weknow.EventSource.Backbone.Contracts.svg)](https://www.nuget.org/packages/Weknow.EventSource.Backbone.Contracts/) +[![Deploy](https://github.com/bnayae/Event-Sourcing-Backbone/actions/workflows/build-publish-v2.yml/badge.svg)](https://github.com/bnayae/Event-Sourcing-Backbone/actions/workflows/build-publish-v2.yml) -Event-Source-Backbone is a framework for working with Event-Source under .NET (6.x). +[![NuGet](https://img.shields.io/nuget/v/EventSourcing.Backbone.SrcGen.svg)](https://www.nuget.org/packages/EventSourcing.Backbone.SrcGen/) +[![NuGet](https://img.shields.io/nuget/v/EventSourcing.Backbone.Abstractions.svg)](https://www.nuget.org/packages/EventSourcing.Backbone.Abstractions/) -!! This Repo is still in **Alpha**, which means that its API still not stable (expecting breaking compatibility). +# Event-Source-Backbone -[Documentation](https://github.com/weknow-network/Event-Source-Backbone/wiki/Home---Event-Source-Backbone) +## Understanding Event Sourcing +Event sourcing is an architectural pattern that captures and persists every change as a sequence of events. It provides a historical log of events that can be used to reconstruct the current state of an application at any given point in time. This approach offers various benefits, such as auditability, scalability, and the ability to build complex workflows. + +Event sourcing, when combined with the Command Query Responsibility Segregation (CQRS) pattern, offers even more advantages. CQRS separates the read and write concerns of an application, enabling the generation of dedicated databases optimized for specific read or write needs. This separation of concerns allows for more agile and flexible database schema designs, as they are less critical to set up in advance. + +By leveraging EventSource.Backbone, developers can implement event sourcing and CQRS together, resulting in a powerful architecture that promotes scalability, flexibility, and maintainability. + +## Introducing EventSource.Backbone +One notable aspect of EventSource.Backbone is its unique approach to event sourcing. Instead of inventing a new event source database, EventSource.Backbone leverages a combination of existing message streams like Kafka, Redis Stream, or similar technologies, along with key-value databases or services like Redis. + +This architecture enables several benefits. Message streams, while excellent for handling event sequences and ensuring reliable message delivery, may not be optimal for heavy payloads. By combining key-value databases with message streams, EventSource.Backbone allows the message payload to be stored in the key-value database while the stream holds the sequence and metadata. This approach improves performance and facilitates compliance with GDPR standards by allowing the splitting of messages into different keys based on a standardized key format. + +It's worth noting that EventSource.Backbone currently provides a software development kit (SDK) for the .NET ecosystem. While the framework may expand to other programming languages and frameworks in the future, at present, it is specifically focused on the .NET platform. + +## Leveraging Existing Infrastructure +One of the major advantages of EventSource.Backbone is its compatibility with widely adopted message streaming platforms like Kafka or Redis Stream. These platforms provide robust message delivery guarantees and high throughput, making them ideal for handling event streams at scale. + +Furthermore, EventSource.Backbone seamlessly integrates with popular key-value databases such as Redis, Couchbase, or Amazon DynamoDB. This integration allows developers to leverage the strengths of these databases for efficient storage and retrieval of auxiliary data related to events. + +## Conclusion +In this introductory post, we've explored the concept of event sourcing and introduced the unique aspects of EventSource.Backbone. This framework stands out by leveraging a combination of message streams and key-value databases to achieve better performance, support GDPR standards, and provide flexibility in event sourcing implementations. + +EventSource.Backbone integrates seamlessly with popular message streaming platforms and key-value databases, enabling developers to leverage their strengths for scalable and efficient event sourcing. + +When combined with CQRS, event sourcing can enhance database schema design, making it more agile and flexible, and enabling the generation of dedicated databases optimized for specific needs. + +Stay tuned for more exciting content on event sourcing and EventSource.Backbone! If you have any questions or topics you'd like me to cover, feel free to leave a comment below. + +Feel free to further customize this blog post to align with your writing style and any additional information you wish to provide. Happy blogging! diff --git a/Tests/ConsoleTest/ConsoleTest.csproj b/Tests/ConsoleTest/ConsoleTest.csproj new file mode 100644 index 00000000..fdead6be --- /dev/null +++ b/Tests/ConsoleTest/ConsoleTest.csproj @@ -0,0 +1,50 @@ + + + + Debug;Release;Gen + false + Exe + + + + + + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ConsoleTest/Constants.cs b/Tests/ConsoleTest/Constants.cs new file mode 100644 index 00000000..830bf968 --- /dev/null +++ b/Tests/ConsoleTest/Constants.cs @@ -0,0 +1,12 @@ +using FakeItEasy; + +namespace ConsoleTest; + +internal static class Constants +{ + public const int MAX = 1_000; + public const string END_POINT_KEY = "REDIS_EVENT_SOURCE_ENDPOINT"; + public const string ENV = $"console-test"; + public static readonly IFooConsumer Subscriber = A.Fake(); + +} diff --git a/Tests/ConsoleTest/IFoo.cs b/Tests/ConsoleTest/IFoo.cs new file mode 100644 index 00000000..57326a79 --- /dev/null +++ b/Tests/ConsoleTest/IFoo.cs @@ -0,0 +1,12 @@ +using EventSourcing.Backbone; + +namespace ConsoleTest; + +[EventsContract(EventsContractType.Producer)] +[EventsContract(EventsContractType.Consumer)] +public interface IFoo +{ + ValueTask Event1Async(); + ValueTask Event2Async(int i); + ValueTask Event3Async(string message); +} diff --git a/Tests/ConsoleTest/OpenTelemetryExtensions.cs b/Tests/ConsoleTest/OpenTelemetryExtensions.cs new file mode 100644 index 00000000..0f653da8 --- /dev/null +++ b/Tests/ConsoleTest/OpenTelemetryExtensions.cs @@ -0,0 +1,54 @@ +using EventSourcing.Backbone; + +using Microsoft.Extensions.DependencyInjection; + +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +// see: +// https://opentelemetry.io/docs/instrumentation/net/getting-started/ +// https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.Jaeger/README.md#environment-variables + +namespace ConsoleTest; + +/// +/// Open telemetry extensions for ASP.NET Core +/// +internal static class OpenTelemetryExtensions +{ + #region AddOpenTelemetryEventSourcing + + /// + /// Adds open telemetry for event sourcing. + /// + /// The services. + /// The environment. + /// + public static IServiceCollection AddOpenTelemetryEventSourcing(this IServiceCollection services, Env environment) + { + // see: + // https://opentelemetry.io/docs/instrumentation/net/getting-started/ + // https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.Jaeger/README.md#environment-variables + services.AddOpenTelemetry() + .WithEventSourcingTracing(environment, + cfg => + { + cfg.SetResourceBuilder(ResourceBuilder.CreateDefault() + .AddService("ConsoleTest")) + .AddOtlpExporter(); + // .AddConsoleExporter(); + }) + .WithEventSourcingMetrics(environment, cfg => + { + cfg + .AddOtlpExporter() + .AddPrometheusExporter() + .AddMeter("ConsoleTest"); + }); + + return services; + } + + #endregion // AddOpenTelemetryEventSourcing +} diff --git a/Tests/ConsoleTest/Program.cs b/Tests/ConsoleTest/Program.cs new file mode 100644 index 00000000..abe44438 --- /dev/null +++ b/Tests/ConsoleTest/Program.cs @@ -0,0 +1,96 @@ +#pragma warning disable HAA0601 // Value type to reference type conversion causing boxing allocation +#pragma warning disable HAA0301 // Closure Allocation Source + +using System.Diagnostics; +using System.Threading.Tasks.Dataflow; + +using ConsoleTest; + +using EventSourcing.Backbone; +using EventSourcing.Backbone.Enums; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +using StackExchange.Redis; + +using static ConsoleTest.Constants; + +CancellationTokenSource cancellation = new CancellationTokenSource( + Debugger.IsAttached + ? TimeSpan.FromMinutes(10) + : TimeSpan.FromSeconds(400)); +string URI = $"console-{DateTime.UtcNow:yyyy-MM-dd HH_mm_ss}"; +IHostBuilder host = Host.CreateDefaultBuilder(args) + .ConfigureServices(services => + { + services.AddLogging(b => b.AddConsole()); + services.AddEventSourceRedisConnection(); + services.AddSingleton(cancellation); + services.AddSingleton(ioc => + { + IFooProducer producer = ioc.ResolveRedisProducerChannel() + .Environment(ENV) + .Uri(URI) + .BuildFooProducer(); + return producer; + }); + services.AddSingleton(ioc => + { + var consumerOptions = new ConsumerOptions + { + AckBehavior = AckBehavior.OnSucceed, + PartialBehavior = PartialConsumerBehavior.Loose, + MaxMessages = MAX * 3 /* detach consumer after 2 messages*/ + }; + IConsumerLifetime subscription = + ioc.ResolveRedisConsumerChannel() + .WithOptions(o => consumerOptions) + .WithCancellation(cancellation.Token) + .Environment(ENV) + .Uri(URI) + .Group("CONSUMER_GROUP_1") + .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") + .SubscribeFooConsumer(Subscriber); + return subscription; + }); + services.AddOpenTelemetryEventSourcing(ENV); + + services.AddHostedService(); + }); + +IHost hosing = host.Build(); +ILogger _logger = hosing.Services.GetService>() ?? throw new NullReferenceException(); +await Cleanup(END_POINT_KEY, URI, _logger); + +await hosing.RunAsync(cancellation.Token); + +Console.WriteLine("Done!"); +Console.ReadKey(false); + +static async Task Cleanup(string END_POINT_KEY, string URI, ILogger fakeLogger) +{ + IConnectionMultiplexer conn = RedisClientFactory.CreateProviderAsync( + logger: fakeLogger, + configurationHook: cfg => cfg.AllowAdmin = true).Result; + string serverNames = Environment.GetEnvironmentVariable(END_POINT_KEY) ?? "localhost:6379"; + foreach (var serverName in serverNames.Split(',')) + { + var server = conn.GetServer(serverName); + if (!server.IsConnected) + continue; + IEnumerable keys = server.Keys(pattern: $"*{URI}*"); + IDatabaseAsync db = conn.GetDatabase(); + + var ab = new ActionBlock(k => db.KeyDeleteAsync(k, CommandFlags.DemandMaster), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 30 }); + foreach (string? key in keys) + { + ab.Post(key!); + } + + ab.Complete(); + await ab.Completion; + } +} \ No newline at end of file diff --git a/Tests/ConsoleTest/Properties/launchSettings.json b/Tests/ConsoleTest/Properties/launchSettings.json new file mode 100644 index 00000000..0cd18a71 --- /dev/null +++ b/Tests/ConsoleTest/Properties/launchSettings.json @@ -0,0 +1,16 @@ +{ + "profiles": { + "ConsoleTest": { + "commandName": "Project", + "environmentVariables": { + "REDIS_EVENT_SOURCE_ENDPOINT": "localhost:6379" + } + }, + "WSL": { + "commandName": "Project", + "environmentVariables": { + "REDIS_EVENT_SOURCE_ENDPOINT": "localhost:6379" + } + } + } +} \ No newline at end of file diff --git a/Tests/ConsoleTest/Worker.cs b/Tests/ConsoleTest/Worker.cs new file mode 100644 index 00000000..0a523763 --- /dev/null +++ b/Tests/ConsoleTest/Worker.cs @@ -0,0 +1,107 @@ +using System.Diagnostics; +using System.Threading.Tasks.Dataflow; + +using EventSourcing.Backbone; + +using FakeItEasy; + +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +using static ConsoleTest.Constants; + +// see: +// https://opentelemetry.io/docs/instrumentation/net/getting-started/ +// https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.Jaeger/README.md#environment-variables + +namespace ConsoleTest; + +/// +/// Open telemetry extensions for ASP.NET Core +/// +internal class Worker : BackgroundService +{ + private readonly ILogger _logger; + private readonly IFooProducer _producer; + private readonly IConsumerLifetime _subscription; + private readonly CancellationTokenSource _cancellation; + + public Worker(ILogger logger, + IFooProducer producer, + IConsumerLifetime subscription, + CancellationTokenSource cancellation) + { + _logger = logger; + _producer = producer; + _subscription = subscription; + _cancellation = cancellation; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Console Test"); + + Prepare(Subscriber); + + var sw = Stopwatch.StartNew(); + + var snapshot = sw.Elapsed; + _logger.LogInformation($"Build producer = {snapshot:mm\\:ss\\.ff}"); + snapshot = sw.Elapsed; + + var ab = new ActionBlock(async i => + { + await _producer.Event1Async(); + await _producer.Event2Async(i); + await _producer.Event3Async($"e-{1}"); + }, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 100 }); + for (int i = 0; i < MAX; i++) + { + ab.Post(i); + //await _producer.Event1Async(); + //await _producer.Event2Async(i); + //await _producer.Event3Async($"e-{1}"); + } + ab.Complete(); + await ab.Completion; + + snapshot = sw.Elapsed - snapshot; + _logger.LogInformation($"Produce = {snapshot:mm\\:ss\\.ff}"); + snapshot = sw.Elapsed; + + await _subscription.Completion; + + snapshot = sw.Elapsed - snapshot; + _logger.LogInformation($"Consumed = {snapshot:mm\\:ss\\.ff}"); + + try + { + A.CallTo(() => Subscriber.Event1Async(A.Ignored)) + .MustHaveHappened(MAX, Times.Exactly); + A.CallTo(() => Subscriber.Event2Async(A.Ignored, A.Ignored)) + .MustHaveHappened(MAX, Times.Exactly); + A.CallTo(() => Subscriber.Event3Async(A.Ignored, A.Ignored)) + .MustHaveHappened(MAX, Times.Exactly); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + _logger.LogInformation(ex.FormatLazy()); + Console.ResetColor(); + } + + _logger.LogInformation("Done"); + _cancellation.CancelSafe(); + } + + + static void Prepare(IFooConsumer subscriber) + { + A.CallTo(() => subscriber.Event1Async(A.Ignored)) + .ReturnsLazily(() => ValueTask.CompletedTask); + A.CallTo(() => subscriber.Event2Async(A.Ignored, A.Ignored)) + .ReturnsLazily(() => ValueTask.CompletedTask); + A.CallTo(() => subscriber.Event3Async(A.Ignored, A.Ignored)) + .ReturnsLazily(() => ValueTask.CompletedTask); + } +} diff --git a/Tests/ConsoleTest/icon.png b/Tests/ConsoleTest/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/Tests/ConsoleTest/icon.png differ diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/.editorconfig b/Tests/EventSourcing.Backbone.IntegrationTests/.editorconfig similarity index 100% rename from Tests/Weknow.EventSource.Backbone.IntegrationTests/.editorconfig rename to Tests/EventSourcing.Backbone.IntegrationTests/.editorconfig diff --git a/Tests/EventSourcing.Backbone.IntegrationTests/AckPatternsTests.cs b/Tests/EventSourcing.Backbone.IntegrationTests/AckPatternsTests.cs new file mode 100644 index 00000000..0adaeb90 --- /dev/null +++ b/Tests/EventSourcing.Backbone.IntegrationTests/AckPatternsTests.cs @@ -0,0 +1,355 @@ +using System.Diagnostics; + +using EventSourcing.Backbone.Building; +using EventSourcing.Backbone.Channels.RedisProvider; +using EventSourcing.Backbone.Enums; +using EventSourcing.Backbone.UnitTests.Entities; + +using FakeItEasy; + +using Microsoft.Extensions.Logging; + +using Polly; + +using Xunit; +using Xunit.Abstractions; + +#pragma warning disable S3881 // "IDisposable" should be implemented correctly +#pragma warning disable HAA0601 // Value type to reference type conversion causing boxing allocation + +// docker run -p 6379:6379 -it --rm --name redis-evt-src redislabs/rejson:latest + +namespace EventSourcing.Backbone.Tests +{ + /// + /// The end to end tests. + /// + public class AckPatternsTests : TestsBase + { + private readonly ISequenceOperationsConsumer _subscriber = A.Fake(); + private readonly SequenceOperationsConsumerBridge _subscriberBridge; + private readonly IProducerStoreStrategyBuilder _producerBuilder; + private readonly IConsumerStoreStrategyBuilder _consumerBuilder; + + private readonly string ENV = $"test"; + protected override string URI { get; } = $"{DateTime.UtcNow:yyyy-MM-dd HH_mm_ss}:{Guid.NewGuid():N}"; + + private static readonly User USER = new User { Eracure = new Personal { Name = "mike", GovernmentId = "A25" }, Comment = "Do it" }; + + #region Ctor + + /// + /// Initializes a new instance of the class. + /// + /// The output helper. + /// The producer channel builder. + /// The consumer channel builder. + public AckPatternsTests( + ITestOutputHelper outputHelper, + Func? producerChannelBuilder = null, + Func? consumerChannelBuilder = null) + : base(outputHelper) + { + _producerBuilder = ProducerBuilder.Empty.UseRedisChannel( /*, + configuration: (cfg) => cfg.ServiceName = "mymaster" */); + _producerBuilder = producerChannelBuilder?.Invoke(_producerBuilder, _fakeLogger) ?? _producerBuilder; + var stg = new RedisConsumerChannelSetting + { + DelayWhenEmptyBehavior = new DelayWhenEmptyBehavior + { + CalcNextDelay = ((d, _) => TimeSpan.FromMilliseconds(2)) + } + }; + var consumerBuilder = stg.CreateRedisConsumerBuilder(); + _consumerBuilder = consumerChannelBuilder?.Invoke(consumerBuilder, _fakeLogger) ?? consumerBuilder; + + A.CallTo(() => _subscriber.RegisterAsync(A.Ignored, A.Ignored)) + .ReturnsLazily(() => + { + Metadata meta = ConsumerMetadata.Context; + if (string.IsNullOrEmpty(meta.EventKey)) + return ValueTask.FromException(new EventSourcingException("Event Key is missing")); + return ValueTask.CompletedTask; + }); + A.CallTo(() => _subscriber.LoginAsync(A.Ignored, A.Ignored, A.Ignored)) + .ReturnsLazily(() => Delay()); + A.CallTo(() => _subscriber.EarseAsync(A.Ignored, A.Ignored)) + .ReturnsLazily(() => Delay()); + + #region A.CallTo(() => _fakeLogger...) + + A.CallTo(() => _fakeLogger.Log( + A.Ignored, + A.Ignored, + A.Ignored, + A.Ignored, + A>.Ignored + )) + .Invokes>((level, id, msg, ex, fn) => + _outputHelper.WriteLine( + $"Info: {fn(msg, ex)}")); + + #endregion // A.CallTo(() => _fakeLogger...) + + async ValueTask Delay() => await Task.Delay(200); + + _subscriberBridge = new SequenceOperationsConsumerBridge(_subscriber); + } + + #endregion // Ctor + + #region DefaultOptions + + private ConsumerOptions DefaultOptions( + ConsumerOptions options, + uint? maxMessages = null, + AckBehavior? ackBehavior = null, + PartialConsumerBehavior? behavior = null) + { + var claimTrigger = new ClaimingTrigger { EmptyBatchCount = 5, MinIdleTime = TimeSpan.FromSeconds(3) }; + return options with + { + ClaimingTrigger = claimTrigger, + MaxMessages = maxMessages ?? options.MaxMessages, + AckBehavior = ackBehavior ?? options.AckBehavior, + PartialBehavior = behavior ?? options.PartialBehavior + }; + } + + #endregion // DefaultOptions + + #region OnSucceed_ACK_WithFailure_Test + + [Fact(Timeout = TIMEOUT)] + public async Task OnSucceed_ACK_WithFailure_Test() + { + #region ISequenceOperations producer = ... + + ISequenceOperationsProducer producer = _producerBuilder + .Environment(ENV) + //.WithOptions(producerOption) + .Uri(URI) + .WithLogger(_fakeLogger) + .BuildSequenceOperationsProducer(); + + #endregion // ISequenceOperations producer = ... + + #region A.CallTo(() => _subscriber.LoginAsync(throw 1 time)) + + int tryNumber = 0; + A.CallTo(() => _subscriber.LoginAsync(A.Ignored, A.Ignored, A.Ignored)) + .ReturnsLazily(() => + { + // 3 error will be catch by Polly, the 4th one will catch outside of Polly + if (Interlocked.Increment(ref tryNumber) < 5) + throw new ApplicationException("test intensional exception"); + + return ValueTask.CompletedTask; + }); + + #endregion // A.CallTo(() => _subscriber.LoginAsync(throw 1 time)) + + await SendSequenceAsync(producer); + + CancellationToken cancellation = GetCancellationToken(); + + #region await using IConsumerLifetime subscription = ...Subscribe(...) + + await using IConsumerLifetime subscription = _consumerBuilder + .WithOptions(o => DefaultOptions(o, 4, AckBehavior.OnSucceed)) + .WithCancellation(cancellation) + .Environment(ENV) + .Uri(URI) + .WithResiliencePolicy(Policy.Handle().RetryAsync(3, (ex, i) => _outputHelper.WriteLine($"Retry {i}"))) + .WithLogger(_fakeLogger) + .Group("CONSUMER_GROUP_1") + .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") + .Subscribe(_subscriberBridge); + + #endregion // await using IConsumerLifetime subscription = ...Subscribe(...) + + await subscription.Completion; + + #region Validation + + A.CallTo(() => _subscriber.RegisterAsync(A.Ignored, A.Ignored)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _subscriber.LoginAsync(A.Ignored, "admin", "1234")) + .MustHaveHappened( + 3 /* Polly retry */ + 1 /* error */ + 1 /* succeed */, + Times.Exactly); + A.CallTo(() => _subscriber.EarseAsync(A.Ignored, 4335)) + .MustHaveHappenedOnceExactly(); + + #endregion // Validation + } + + #endregion // OnSucceed_ACK_WithFailure_Test + + #region OnFinally_ACK_WithFailure_Test + + [Fact(Timeout = TIMEOUT)] + public async Task OnFinally_ACK_WithFailure_Test() + { + #region ISequenceOperations producer = ... + + ISequenceOperationsProducer producer = _producerBuilder + .Environment(ENV) + .Uri(URI) + .WithLogger(_fakeLogger) + .BuildSequenceOperationsProducer(); + + #endregion // ISequenceOperations producer = ... + + #region A.CallTo(() => _subscriber.LoginAsync(throw 1 time)) + + int tryNumber = 0; + A.CallTo(() => _subscriber.LoginAsync(A.Ignored, A.Ignored, A.Ignored)) + .ReturnsLazily(() => + { + // 3 error will be catch by Polly, the 4th one will catch outside of Polly + if (Interlocked.Increment(ref tryNumber) < 5) + throw new ApplicationException("test intensional exception"); + + return ValueTask.CompletedTask; + }); + + #endregion // A.CallTo(() => _subscriber.LoginAsync(throw 1 time)) + + await SendSequenceAsync(producer); + + + CancellationToken cancellation = GetCancellationToken(); + + #region await using IConsumerLifetime subscription = ...Subscribe(...) + + await using IConsumerLifetime subscription = _consumerBuilder + .WithOptions(o => DefaultOptions(o, 3, AckBehavior.OnFinally)) + .WithCancellation(cancellation) + .Environment(ENV) + .Uri(URI) + .WithResiliencePolicy(Policy.Handle().RetryAsync(3, (ex, i) => _outputHelper.WriteLine($"Retry {i}"))) + .WithLogger(_fakeLogger) + .Group("CONSUMER_GROUP_1") + .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") + .Subscribe(_subscriberBridge); + + #endregion // await using IConsumerLifetime subscription = ...Subscribe(...) + + await subscription.Completion; + + #region Validation + + A.CallTo(() => _subscriber.RegisterAsync(A.Ignored, A.Ignored)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _subscriber.LoginAsync(A.Ignored, "admin", "1234")) + .MustHaveHappened( + 3 /* Polly retry */ + 1 /* error */ , + Times.Exactly); + A.CallTo(() => _subscriber.EarseAsync(A.Ignored, 4335)) + .MustHaveHappenedOnceExactly(); + + #endregion // Validation + } + + #endregion // OnFinally_ACK_WithFailure_Test + + #region Manual_ACK_Test + + [Fact(Timeout = TIMEOUT)] + [Trait("Profile", "true")] + public async Task Manual_ACK_Test() + { + #region ISequenceOperations producer = ... + + ISequenceOperationsProducer producer = _producerBuilder + .Environment(ENV) + //.WithOptions(producerOption) + .Uri(URI) + .BuildSequenceOperationsProducer(); + + #endregion // ISequenceOperations producer = ... + + #region A.CallTo(...).ReturnsLazily(...) + + int tryNumber = 0; + A.CallTo(() => _subscriber.RegisterAsync(A.Ignored, A.Ignored)) + .ReturnsLazily(() => Ack.Current.AckAsync()); + A.CallTo(() => _subscriber.LoginAsync(A.Ignored, A.Ignored, A.Ignored)) + .ReturnsLazily(async (meta, email, pass) => + { + // 3 error will be catch by Polly, the 4th one will catch outside of Polly + if (Interlocked.Increment(ref tryNumber) < 5) + throw new ApplicationException("test intensional exception"); + await meta.AckAsync(); + }); + A.CallTo(() => _subscriber.EarseAsync(A.Ignored, A.Ignored)) + .ReturnsLazily(() => Ack.Current.AckAsync()); + + #endregion // A.CallTo(...).ReturnsLazily(...) + + await SendSequenceAsync(producer); + + CancellationToken cancellation = GetCancellationToken(); + + #region await using IConsumerLifetime subscription = ...Subscribe(...) + + await using IConsumerLifetime subscription = _consumerBuilder + .WithOptions(o => DefaultOptions(o, 4, AckBehavior.Manual)) + .WithCancellation(cancellation) + .Environment(ENV) + .Uri(URI) + .WithResiliencePolicy(Policy.Handle().RetryAsync(3, (ex, i) => _outputHelper.WriteLine($"Retry {i}"))) + .WithLogger(_fakeLogger) + .Group("CONSUMER_GROUP_1") + .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") + .Subscribe(_subscriberBridge); + + #endregion // await using IConsumerLifetime subscription = ...Subscribe(...) + + await subscription.Completion; + + #region Validation + + A.CallTo(() => _subscriber.RegisterAsync(A.Ignored, A.Ignored)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _subscriber.LoginAsync(A.Ignored, "admin", "1234")) + .MustHaveHappened( + 3 /* Polly retry */ + 1 /* error */ + 1 /* succeed */, + Times.Exactly); + A.CallTo(() => _subscriber.EarseAsync(A.Ignored, 4335)) + .MustHaveHappenedOnceExactly(); + + #endregion // Validation + } + + #endregion // Manual_ACK_Test + + #region SendSequenceAsync + + private static async Task SendSequenceAsync(ISequenceOperationsProducer producer, string pass = "1234") + { + EventKey r1 = await producer.RegisterAsync(USER with { Comment = null }); + EventKey r2 = await producer.LoginAsync("admin", pass); + EventKey r3 = await producer.EarseAsync(4335); + return new[] { r1, r2, r3 }; + } + + #endregion // SendSequenceAsync + + #region GetCancellationToken + + /// + /// Gets the cancellation token. + /// + /// + private static CancellationToken GetCancellationToken() + { + return new CancellationTokenSource(Debugger.IsAttached + ? TimeSpan.FromMinutes(10) + : TimeSpan.FromSeconds(10)).Token; + } + + #endregion // GetCancellationToken + } +} diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/IEventFlow.cs b/Tests/EventSourcing.Backbone.IntegrationTests/Contracts/IEventFlow.cs similarity index 78% rename from Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/IEventFlow.cs rename to Tests/EventSourcing.Backbone.IntegrationTests/Contracts/IEventFlow.cs index 8fbcf656..c0bd30c6 100644 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/IEventFlow.cs +++ b/Tests/EventSourcing.Backbone.IntegrationTests/Contracts/IEventFlow.cs @@ -1,9 +1,9 @@ using System.Text.Json; -namespace Weknow.EventSource.Backbone.UnitTests.Entities +namespace EventSourcing.Backbone.UnitTests.Entities { - [GenerateEventSource(EventSourceGenType.Producer)] - [GenerateEventSource(EventSourceGenType.Consumer)] + [EventsContract(EventsContractType.Producer)] + [EventsContract(EventsContractType.Consumer)] public interface IEventFlow { /// diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/IEventFlowStage1.cs b/Tests/EventSourcing.Backbone.IntegrationTests/Contracts/IEventFlowStage1.cs similarity index 73% rename from Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/IEventFlowStage1.cs rename to Tests/EventSourcing.Backbone.IntegrationTests/Contracts/IEventFlowStage1.cs index 9f3aa59e..ccaddacc 100644 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/IEventFlowStage1.cs +++ b/Tests/EventSourcing.Backbone.IntegrationTests/Contracts/IEventFlowStage1.cs @@ -1,6 +1,6 @@ -namespace Weknow.EventSource.Backbone.UnitTests.Entities +namespace EventSourcing.Backbone.UnitTests.Entities { - [GenerateEventSource(EventSourceGenType.Consumer)] + [EventsContract(EventsContractType.Consumer)] public interface IEventFlowStage1 { /// diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/IEventFlowStage2.cs b/Tests/EventSourcing.Backbone.IntegrationTests/Contracts/IEventFlowStage2.cs similarity index 80% rename from Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/IEventFlowStage2.cs rename to Tests/EventSourcing.Backbone.IntegrationTests/Contracts/IEventFlowStage2.cs index cf6ba9af..0db12f39 100644 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/IEventFlowStage2.cs +++ b/Tests/EventSourcing.Backbone.IntegrationTests/Contracts/IEventFlowStage2.cs @@ -1,11 +1,11 @@ using System.Text.Json; -namespace Weknow.EventSource.Backbone.UnitTests.Entities +namespace EventSourcing.Backbone.UnitTests.Entities { /// /// use to test consumer which have partial operation from IEventFlow /// - [GenerateEventSource(EventSourceGenType.Consumer)] + [EventsContract(EventsContractType.Consumer)] public interface IEventFlowStage2 { /// diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/ISequenceOperations.cs b/Tests/EventSourcing.Backbone.IntegrationTests/Contracts/ISequenceOperations.cs similarity index 55% rename from Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/ISequenceOperations.cs rename to Tests/EventSourcing.Backbone.IntegrationTests/Contracts/ISequenceOperations.cs index 43d43f99..913d9faf 100644 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/ISequenceOperations.cs +++ b/Tests/EventSourcing.Backbone.IntegrationTests/Contracts/ISequenceOperations.cs @@ -1,14 +1,14 @@ -namespace Weknow.EventSource.Backbone.UnitTests.Entities +namespace EventSourcing.Backbone.UnitTests.Entities { /// /// The sequence operations. /// - //[GenerateEventSource(EventSourceGenType.Producer, Name = "IProducerSequenceOperations", ContractOnly = true)] - [GenerateEventSource(EventSourceGenType.Producer, Name = "IProducerSequenceOperations")] - [GenerateEventSource(EventSourceGenType.Producer)] - //[GenerateEventSourceBridge(EventSourceGenType.Producer)] - [GenerateEventSource(EventSourceGenType.Consumer)] - //[GenerateEventSourceBridge(EventSourceGenType.Consumer)] + //[EventsContract(EventSourceGenType.Producer, Name = "IProducerSequenceOperations", ContractOnly = true)] + [EventsContract(EventsContractType.Producer, Name = "IProducerSequenceOperations")] + [EventsContract(EventsContractType.Producer)] + //[EventsContractBridge(EventSourceGenType.Producer)] + [EventsContract(EventsContractType.Consumer)] + //[EventsContractBridge(EventSourceGenType.Consumer)] public interface ISequenceOperations { /// diff --git a/Tests/EventSourcing.Backbone.IntegrationTests/Contracts/ISimple.cs b/Tests/EventSourcing.Backbone.IntegrationTests/Contracts/ISimple.cs new file mode 100644 index 00000000..24b4f972 --- /dev/null +++ b/Tests/EventSourcing.Backbone.IntegrationTests/Contracts/ISimple.cs @@ -0,0 +1,9 @@ +namespace EventSourcing.Backbone.UnitTests.Entities; + +[EventsContract(EventsContractType.Producer)] +[EventsContract(EventsContractType.Consumer)] +public interface ISimple +{ + ValueTask Step1Async(int value); + ValueTask Step2Async(int value); +} diff --git a/Tests/EventSourcing.Backbone.IntegrationTests/Contracts/Inheritance/IFlowA.cs b/Tests/EventSourcing.Backbone.IntegrationTests/Contracts/Inheritance/IFlowA.cs new file mode 100644 index 00000000..82b42d10 --- /dev/null +++ b/Tests/EventSourcing.Backbone.IntegrationTests/Contracts/Inheritance/IFlowA.cs @@ -0,0 +1,8 @@ +namespace EventSourcing.Backbone.UnitTests.Entities; + +[EventsContract(EventsContractType.Producer)] +[EventsContract(EventsContractType.Consumer)] +public interface IFlowA +{ + ValueTask AAsync(int id); +} diff --git a/Tests/EventSourcing.Backbone.IntegrationTests/Contracts/Inheritance/IFlowAB.cs b/Tests/EventSourcing.Backbone.IntegrationTests/Contracts/Inheritance/IFlowAB.cs new file mode 100644 index 00000000..5f9999ee --- /dev/null +++ b/Tests/EventSourcing.Backbone.IntegrationTests/Contracts/Inheritance/IFlowAB.cs @@ -0,0 +1,8 @@ +namespace EventSourcing.Backbone.UnitTests.Entities; + +[EventsContract(EventsContractType.Producer)] +[EventsContract(EventsContractType.Consumer)] +public interface IFlowAB : IFlowA, IFlowB +{ + ValueTask DerivedAsync(string key); +} diff --git a/Tests/EventSourcing.Backbone.IntegrationTests/Contracts/Inheritance/IFlowB.cs b/Tests/EventSourcing.Backbone.IntegrationTests/Contracts/Inheritance/IFlowB.cs new file mode 100644 index 00000000..a8a36e10 --- /dev/null +++ b/Tests/EventSourcing.Backbone.IntegrationTests/Contracts/Inheritance/IFlowB.cs @@ -0,0 +1,8 @@ +namespace EventSourcing.Backbone.UnitTests.Entities; + +[EventsContract(EventsContractType.Producer)] +[EventsContract(EventsContractType.Consumer)] +public interface IFlowB +{ + ValueTask BAsync(DateTimeOffset date); +} diff --git a/Tests/EventSourcing.Backbone.IntegrationTests/DeleteKeysTests.cs b/Tests/EventSourcing.Backbone.IntegrationTests/DeleteKeysTests.cs new file mode 100644 index 00000000..c72e658c --- /dev/null +++ b/Tests/EventSourcing.Backbone.IntegrationTests/DeleteKeysTests.cs @@ -0,0 +1,79 @@ +using System.Diagnostics; +using System.Threading.Tasks.Dataflow; + +using StackExchange.Redis; + +using Xunit; +using Xunit.Abstractions; + +using static EventSourcing.Backbone.Channels.RedisProvider.Common.RedisChannelConstants; + +namespace EventSourcing.Backbone.Tests +{ + public class DeleteKeysTests + { + private readonly ITestOutputHelper _outputHelper; + + public DeleteKeysTests( + ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + + } + #region DELETE_KEYS_TEST + + + [Theory(Skip = "cleanup")] + [InlineData("*test*")] + [InlineData("dev:*")] + [Trait("type", "delete-keys")] + public async Task DELETE_KEYS_TEST(string pattern) + { + IConnectionMultiplexer conn = await RedisClientFactory.CreateProviderAsync( + configurationHook: cfg => cfg.AllowAdmin = true); + + string serverNames = Environment.GetEnvironmentVariable(END_POINT_KEY) ?? "localhost:6379"; + foreach (var serverName in serverNames.Split(',')) + { + var server = conn.GetServer(serverName); + if (!server.IsConnected) + continue; + IEnumerable keys = server.Keys(pattern: pattern).ToArray(); + IDatabaseAsync db = conn.GetDatabase(); + + var ab = new ActionBlock(k => LocalAsync(k), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 30 }); + foreach (string? key in keys) + { + if (string.IsNullOrWhiteSpace(key)) continue; + ab.Post(key); + } + + ab.Complete(); + await ab.Completion; + + async Task LocalAsync(string k) + { + try + { + await db.KeyDeleteAsync(k, CommandFlags.DemandMaster); + Trace.WriteLine(k); + } + #region Exception Handling + + catch (RedisTimeoutException ex) + { + _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); + } + catch (Exception ex) + { + _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); + } + + #endregion // Exception Handling + } + } + } + + #endregion // DELETE_KEYS_TEST + } +} diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/EndToEndExplicitTests.cs b/Tests/EventSourcing.Backbone.IntegrationTests/EndToEndExplicitTests.cs similarity index 75% rename from Tests/Weknow.EventSource.Backbone.IntegrationTests/EndToEndExplicitTests.cs rename to Tests/EventSourcing.Backbone.IntegrationTests/EndToEndExplicitTests.cs index 7eb5bce8..0d0a5bb5 100644 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/EndToEndExplicitTests.cs +++ b/Tests/EventSourcing.Backbone.IntegrationTests/EndToEndExplicitTests.cs @@ -1,25 +1,27 @@ using System.Diagnostics; using System.Text.Json; +using EventSourcing.Backbone.Building; +using EventSourcing.Backbone.UnitTests.Entities; + using FakeItEasy; using Microsoft.Extensions.Logging; using StackExchange.Redis; -using Weknow.EventSource.Backbone.Building; -using Weknow.EventSource.Backbone.UnitTests.Entities; - using Xunit; using Xunit.Abstractions; -// docker run -p 6379:6379 -it --rm --name redis-event-source redislabs/rejson:latest +#pragma warning disable S3881 // "IDisposable" should be implemented correctly + +// docker run -p 6379:6379 -it --rm --name redis-evt-src redislabs/rejson:latest // TODO: [bnaya 2020-10] ensure message order(cancel ack should cancel all following messages) // TODO: [bnaya 2020-10] check for no pending -namespace Weknow.EventSource.Backbone.Tests +namespace EventSourcing.Backbone.Tests { /// /// The end to end explicit tests. @@ -31,8 +33,7 @@ public class EndToEndExplicitTests : IDisposable private readonly IProducerStoreStrategyBuilder _producerBuilder; private readonly IConsumerStoreStrategyBuilder _consumerBuilder; - private readonly string PARTITION = $"{DateTime.UtcNow:yyyy-MM-dd HH_mm_ss}:{Guid.NewGuid():N}"; - private readonly string SHARD = $"some-shard-{DateTime.UtcNow.Second}"; + private readonly string URI = $"{DateTime.UtcNow:yyyy-MM-dd HH_mm_ss}:{Guid.NewGuid():N}"; private readonly ILogger _fakeLogger = A.Fake(); @@ -62,9 +63,9 @@ public EndToEndExplicitTests( _consumerBuilder = ConsumerBuilder.Empty.UseRedisChannel(); _consumerBuilder = consumerChannelBuilder?.Invoke(_consumerBuilder, _fakeLogger) ?? _consumerBuilder; - A.CallTo(() => _subscriber.Stage1Async(A.Ignored, A.Ignored)) + A.CallTo(() => _subscriber.Stage1Async(A.Ignored, A.Ignored, A.Ignored)) .ReturnsLazily(() => ValueTask.CompletedTask); - A.CallTo(() => _subscriber.Stage2Async(A.Ignored, A.Ignored)) + A.CallTo(() => _subscriber.Stage2Async(A.Ignored, A.Ignored, A.Ignored)) .ReturnsLazily(() => Delay()); #region A.CallTo(() => _fakeLogger...) @@ -90,23 +91,33 @@ public EndToEndExplicitTests( #region OnSucceed_ACK_Test - [Fact(Timeout = TIMEOUT)] + [Fact] + //[Fact(Timeout = TIMEOUT)] public async Task OnSucceed_ACK_Test() { + var sw = Stopwatch.StartNew(); + #region IEventFlow producer = ... IEventFlowProducer producer = _producerBuilder .Environment(ENV) //.WithOptions(producerOption) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) //.WithLogger(_fakeLogger) .BuildEventFlowProducer(); #endregion // IEventFlow producer = ... + var snapshot = sw.Elapsed; + _outputHelper.WriteLine($"Build producer = {snapshot:mm\\:ss\\.ff}"); + snapshot = sw.Elapsed; + await SendSequenceAsync(producer); + snapshot = sw.Elapsed - snapshot; + _outputHelper.WriteLine($"Produce = {snapshot:mm\\:ss\\.ff}"); + snapshot = sw.Elapsed; + var consumerOptions = new ConsumerOptions { AckBehavior = AckBehavior.OnSucceed, @@ -120,24 +131,29 @@ public async Task OnSucceed_ACK_Test() .WithOptions(o => consumerOptions) .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger); await using IConsumerLifetime subscription = builder .Group("CONSUMER_GROUP_1") .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") .SubscribeEventFlowConsumer(_subscriber); - //.SubscribeEventFlow(_subscriber, "CONSUMER_GROUP_1", $"TEST {DateTime.UtcNow:HH:mm:ss}"); #endregion // await using IConsumerLifetime subscription = ...Subscribe(...) + snapshot = sw.Elapsed - snapshot; + _outputHelper.WriteLine($"Build Consumer = {snapshot:mm\\:ss\\.ff}"); + snapshot = sw.Elapsed; + await subscription.Completion; + snapshot = sw.Elapsed - snapshot; + _outputHelper.WriteLine($"Consumed = {snapshot:mm\\:ss\\.ff}"); + #region Validation - A.CallTo(() => _subscriber.Stage1Async(A.Ignored, A.Ignored)) + A.CallTo(() => _subscriber.Stage1Async(A.Ignored, A.Ignored, A.Ignored)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.Stage2Async(A.Ignored, A.Ignored)) + A.CallTo(() => _subscriber.Stage2Async(A.Ignored, A.Ignored, A.Ignored)) .MustHaveHappenedOnceExactly(); #endregion // Validation @@ -176,7 +192,6 @@ private static CancellationToken GetCancellationToken() #region Dispose pattern - ~EndToEndExplicitTests() { Dispose(); @@ -185,37 +200,15 @@ private static CancellationToken GetCancellationToken() public void Dispose() { GC.SuppressFinalize(this); - string key = $"{PARTITION}:{SHARD}"; - IConnectionMultiplexer conn = RedisClientFactory.CreateProviderBlocking( - _fakeLogger, - cfg => cfg.AllowAdmin = true); + string key = URI; + IConnectionMultiplexer conn = RedisClientFactory.CreateProviderAsync( + logger: _fakeLogger, + configurationHook: cfg => cfg.AllowAdmin = true).Result; IDatabaseAsync db = conn.GetDatabase(); db.KeyDeleteAsync(key, CommandFlags.DemandMaster).Wait(); } #endregion // Dispose pattern - - /// - /// The subscriber. - /// - private class Subscriber : IEventFlow - { - private readonly IEventFlow _subscriber; - - /// - /// Initializes a new instance of the class. - /// - /// The subscriber. - public Subscriber(IEventFlow subscriber) - { - _subscriber = subscriber; - } - - ValueTask IEventFlow.Stage1Async(Person PII, string payload) => _subscriber.Stage1Async(PII, payload); - - ValueTask IEventFlow.Stage2Async(JsonElement PII, JsonElement data) => _subscriber.Stage2Async(PII, data); - } - } } diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/EndToEndStressTests.cs b/Tests/EventSourcing.Backbone.IntegrationTests/EndToEndStressTests.cs similarity index 52% rename from Tests/Weknow.EventSource.Backbone.IntegrationTests/EndToEndStressTests.cs rename to Tests/EventSourcing.Backbone.IntegrationTests/EndToEndStressTests.cs index add93a95..4c4cda65 100644 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/EndToEndStressTests.cs +++ b/Tests/EventSourcing.Backbone.IntegrationTests/EndToEndStressTests.cs @@ -1,38 +1,30 @@ using System.Diagnostics; -using System.Threading.Tasks.Dataflow; + +using EventSourcing.Backbone.Building; +using EventSourcing.Backbone.Channels.RedisProvider; +using EventSourcing.Backbone.UnitTests.Entities; using FakeItEasy; using Microsoft.Extensions.Logging; -using StackExchange.Redis; - -using Weknow.EventSource.Backbone.Building; -using Weknow.EventSource.Backbone.UnitTests.Entities; - using Xunit; using Xunit.Abstractions; -using static Weknow.EventSource.Backbone.Channels.RedisProvider.Common.RedisChannelConstants; +#pragma warning disable S3881 // "IDisposable" should be implemented correctly -// docker run -p 6379:6379 -it --rm --name redis-event-source redislabs/rejson:latest +// docker run -p 6379:6379 -it --rm --name redis-evt-src redislabs/rejson:latest -namespace Weknow.EventSource.Backbone.Tests +namespace EventSourcing.Backbone.Tests { - public class EndToEndStressTests : IDisposable + public class EndToEndStressTests : TestsBase { - private readonly ITestOutputHelper _outputHelper; private readonly ISequenceOperationsConsumer _subscriber = A.Fake(); private readonly IProducerStoreStrategyBuilder _producerBuilder; private readonly IConsumerHooksBuilder _consumerBuilder; private readonly string ENV = $"test"; - private readonly string PARTITION = $"{DateTime.UtcNow:yyyy-MM-dd HH_mm_ss}:{Guid.NewGuid():N}"; - private readonly string SHARD = $"some-shard-{DateTime.UtcNow.Second}"; - - private readonly ILogger _fakeLogger = A.Fake(); - private static readonly User USER = new User { Eracure = new Personal { Name = "mike", GovernmentId = "A25" }, Comment = "Do it" }; - private const int TIMEOUT = 1000 * 30; + protected override string URI { get; } = $"{DateTime.UtcNow:yyyy-MM-dd HH_mm_ss}:{Guid.NewGuid():N}"; #region Ctor @@ -40,30 +32,29 @@ public EndToEndStressTests( ITestOutputHelper outputHelper, Func? producerChannelBuilder = null, Func? consumerChannelBuilder = null) + : base(outputHelper) { - _outputHelper = outputHelper; _producerBuilder = ProducerBuilder.Empty.UseRedisChannel( /*, configuration: (cfg) => cfg.ServiceName = "mymaster" */); _producerBuilder = producerChannelBuilder?.Invoke(_producerBuilder, _fakeLogger) ?? _producerBuilder; - var consumerBuilder = ConsumerBuilder.Empty.UseRedisChannel( - stg => stg with - { - DelayWhenEmptyBehavior = - stg.DelayWhenEmptyBehavior with - { - CalcNextDelay = (d => TimeSpan.FromMilliseconds(2)) - } - }); + var stg = new RedisConsumerChannelSetting + { + DelayWhenEmptyBehavior = new DelayWhenEmptyBehavior + { + CalcNextDelay = ((d, _) => TimeSpan.FromMilliseconds(2)) + } + }; + var consumerBuilder = stg.CreateRedisConsumerBuilder(); consumerBuilder = consumerChannelBuilder?.Invoke(consumerBuilder, _fakeLogger) ?? consumerBuilder; var claimTrigger = new ClaimingTrigger { EmptyBatchCount = 5, MinIdleTime = TimeSpan.FromSeconds(3) }; _consumerBuilder = consumerBuilder.WithOptions(o => o with { ClaimingTrigger = claimTrigger }); - A.CallTo(() => _subscriber.RegisterAsync(A.Ignored)) + A.CallTo(() => _subscriber.RegisterAsync(A.Ignored, A.Ignored)) .ReturnsLazily(() => ValueTask.CompletedTask); - A.CallTo(() => _subscriber.LoginAsync(A.Ignored, A.Ignored)) + A.CallTo(() => _subscriber.LoginAsync(A.Ignored, A.Ignored, A.Ignored)) .ReturnsLazily(() => Delay()); - A.CallTo(() => _subscriber.EarseAsync(A.Ignored)) + A.CallTo(() => _subscriber.EarseAsync(A.Ignored, A.Ignored)) .ReturnsLazily(() => Delay()); #region A.CallTo(() => _fakeLogger...) @@ -97,8 +88,7 @@ public async Task Receiver_Stress_Test() ISequenceOperationsProducer producer = _producerBuilder //.WithOptions(producerOption) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildSequenceOperationsProducer(); @@ -113,8 +103,7 @@ public async Task Receiver_Stress_Test() IConsumerReceiver receiver = _consumerBuilder .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildReceiver(); @@ -138,8 +127,7 @@ public async Task Receiver_Json_Stress_Test() ISequenceOperationsProducer producer = _producerBuilder //.WithOptions(producerOption) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildSequenceOperationsProducer(); @@ -152,8 +140,7 @@ public async Task Receiver_Json_Stress_Test() IConsumerReceiver receiver = _consumerBuilder .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildReceiver(); @@ -184,37 +171,13 @@ private async Task ValidateLongSequenceAsync( #endregion // ValidateLongSequenceAsync - #region SendSequenceAsync - - private static async Task SendSequenceAsync(ISequenceOperations producer, string pass = "1234") - { - await producer.RegisterAsync(USER); - await producer.LoginAsync("admin", pass); - await producer.EarseAsync(4335); - } - - private static async Task SendSequenceAsync(ISequenceOperationsProducer producer, string pass = "1234") - { - EventKey r1 = await producer.RegisterAsync(USER with { Comment = null }); - EventKey r2 = await producer.LoginAsync("admin", pass); - EventKey r3 = await producer.EarseAsync(4335); - return new[] { r1, r2, r3 }; - } - private static async Task SendSequenceAsync(IProducerSequenceOperations producer, string pass = "1234") - { - EventKey r1 = await producer.RegisterAsync(USER); - EventKey r2 = await producer.LoginAsync("admin", pass); - EventKey r3 = await producer.EarseAsync(4335); - return new[] { r1, r2, r3 }; - } - - #endregion // SendSequenceAsync - #region SendLongSequenceAsync - private static async Task SendLongSequenceAsync(ISequenceOperationsProducer producer, string pass = "1234") + private static async Task SendLongSequenceAsync( + ISequenceOperationsProducer producer) { - var tasks = Enumerable.Range(1, 1500).Select(async m => await producer.SuspendAsync(m)); + var tasks = Enumerable.Range(1, 1500) + .Select(async m => await producer.SuspendAsync(m)); EventKeys[] ids = await Task.WhenAll(tasks); return ids[ids.Length - 1].First(); } @@ -235,74 +198,5 @@ private static CancellationToken GetCancellationToken() } #endregion // GetCancellationToken - - #region Dispose pattern - - - ~EndToEndStressTests() - { - Dispose(); - } - - public void Dispose() - { - GC.SuppressFinalize(this); - try - { - IConnectionMultiplexer conn = RedisClientFactory.CreateProviderBlocking( - cfg => cfg.AllowAdmin = true); - string serverName = Environment.GetEnvironmentVariable(END_POINT_KEY) ?? "localhost:6379"; - var server = conn.GetServer(serverName); - IEnumerable keys = server.Keys(pattern: $"*{PARTITION}*"); - IDatabaseAsync db = conn.GetDatabase(); - - var ab = new ActionBlock(k => LocalAsync(k), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 30 }); - foreach (string? key in keys) - { - if (string.IsNullOrEmpty(key)) continue; - ab.Post(key); - } - - ab.Complete(); - ab.Completion.Wait(); - - async Task LocalAsync(string k) - { - try - { - await db.KeyDeleteAsync(k, CommandFlags.DemandMaster); - _outputHelper.WriteLine($"Cleanup: delete key [{k}]"); - } - #region Exception Handling - - catch (RedisTimeoutException ex) - { - _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); - } - catch (Exception ex) - { - _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); - } - - #endregion // Exception Handling - } - - - } - #region Exception Handling - - catch (RedisTimeoutException ex) - { - _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); - } - catch (Exception ex) - { - _outputHelper.WriteLine($"Test dispose error (delete keys) {ex.FormatLazy()}"); - } - - #endregion // Exception Handling - } - - #endregion // Dispose pattern } } diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/EndToEndTests.cs b/Tests/EventSourcing.Backbone.IntegrationTests/EndToEndTests.cs similarity index 65% rename from Tests/Weknow.EventSource.Backbone.IntegrationTests/EndToEndTests.cs rename to Tests/EventSourcing.Backbone.IntegrationTests/EndToEndTests.cs index 1edcdc70..ba2092c3 100644 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/EndToEndTests.cs +++ b/Tests/EventSourcing.Backbone.IntegrationTests/EndToEndTests.cs @@ -1,6 +1,10 @@ using System.Diagnostics; using System.Text.Json; -using System.Threading.Tasks.Dataflow; + +using EventSourcing.Backbone.Building; +using EventSourcing.Backbone.Channels.RedisProvider; +using EventSourcing.Backbone.Enums; +using EventSourcing.Backbone.UnitTests.Entities; using FakeItEasy; @@ -8,28 +12,22 @@ using Polly; -using StackExchange.Redis; - -using Weknow.EventSource.Backbone.Building; -using Weknow.EventSource.Backbone.Enums; -using Weknow.EventSource.Backbone.UnitTests.Entities; - using Xunit; using Xunit.Abstractions; -using static Weknow.EventSource.Backbone.Channels.RedisProvider.Common.RedisChannelConstants; -using static Weknow.EventSource.Backbone.EventSourceConstants; +using static EventSourcing.Backbone.EventSourceConstants; -// docker run -p 6379:6379 -it --rm --name redis-event-source redislabs/rejson:latest +#pragma warning disable S3881 // "IDisposable" should be implemented correctly -namespace Weknow.EventSource.Backbone.Tests +// docker run -p 6379:6379 -it --rm --name redis-evt-src redislabs/rejson:latest + +namespace EventSourcing.Backbone.Tests { /// /// The end to end tests. /// - public class EndToEndTests : IDisposable + public class EndToEndTests : TestsBase { - private readonly ITestOutputHelper _outputHelper; private readonly ISequenceOperationsConsumer _subscriber = A.Fake(); private readonly SequenceOperationsConsumerBridge _subscriberBridge; private readonly ISequenceOperationsConsumer _autoSubscriber = A.Fake(); @@ -43,13 +41,10 @@ public class EndToEndTests : IDisposable private readonly IEventFlowStage1Consumer _stage1Consumer = A.Fake(); private readonly IEventFlowStage2Consumer _stage2Consumer = A.Fake(); - private readonly string ENV = $"Development"; - private readonly string PARTITION = $"{DateTime.UtcNow:yyyy-MM-dd HH_mm_ss}:{Guid.NewGuid():N}"; - private readonly string SHARD = $"some-shard-{DateTime.UtcNow.Second}"; + private readonly string ENV = $"test"; + protected override string URI { get; } = $"{DateTime.UtcNow:yyyy-MM-dd HH_mm_ss}:{Guid.NewGuid():N}"; - private readonly ILogger _fakeLogger = A.Fake(); private static readonly User USER = new User { Eracure = new Personal { Name = "mike", GovernmentId = "A25" }, Comment = "Do it" }; - private const int TIMEOUT = 1_000 * 50; #region Ctor @@ -63,33 +58,32 @@ public EndToEndTests( ITestOutputHelper outputHelper, Func? producerChannelBuilder = null, Func? consumerChannelBuilder = null) + : base(outputHelper) { - _outputHelper = outputHelper; _producerBuilder = ProducerBuilder.Empty.UseRedisChannel( /*, configuration: (cfg) => cfg.ServiceName = "mymaster" */); _producerBuilder = producerChannelBuilder?.Invoke(_producerBuilder, _fakeLogger) ?? _producerBuilder; - var consumerBuilder = ConsumerBuilder.Empty.UseRedisChannel( - stg => stg with - { - DelayWhenEmptyBehavior = - stg.DelayWhenEmptyBehavior with - { - CalcNextDelay = (d => TimeSpan.FromMilliseconds(2)) - } - }); + var stg = new RedisConsumerChannelSetting + { + DelayWhenEmptyBehavior = new DelayWhenEmptyBehavior + { + CalcNextDelay = ((d, _) => TimeSpan.FromMilliseconds(2)) + } + }; + var consumerBuilder = stg.CreateRedisConsumerBuilder(); _consumerBuilder = consumerChannelBuilder?.Invoke(consumerBuilder, _fakeLogger) ?? consumerBuilder; - A.CallTo(() => _subscriber.RegisterAsync(A.Ignored)) + A.CallTo(() => _subscriber.RegisterAsync(A.Ignored, A.Ignored)) .ReturnsLazily(() => { Metadata meta = ConsumerMetadata.Context; if (string.IsNullOrEmpty(meta.EventKey)) - return ValueTask.FromException(new Exception("Event Key is missing")); + return ValueTask.FromException(new EventSourcingException("Event Key is missing")); return ValueTask.CompletedTask; }); - A.CallTo(() => _subscriber.LoginAsync(A.Ignored, A.Ignored)) + A.CallTo(() => _subscriber.LoginAsync(A.Ignored, A.Ignored, A.Ignored)) .ReturnsLazily(() => Delay()); - A.CallTo(() => _subscriber.EarseAsync(A.Ignored)) + A.CallTo(() => _subscriber.EarseAsync(A.Ignored, A.Ignored)) .ReturnsLazily(() => Delay()); #region A.CallTo(() => _fakeLogger...) @@ -144,8 +138,7 @@ public async Task Environmet_Test() ISequenceOperationsProducer producer = _producerBuilder //.WithOptions(producerOption) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildSequenceOperationsProducer(); @@ -161,8 +154,7 @@ public async Task Environmet_Test() .WithOptions(o => DefaultOptions(o, 3, AckBehavior.OnSucceed)) .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .Group("CONSUMER_GROUP_1") .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") @@ -170,15 +162,18 @@ public async Task Environmet_Test() #endregion // await using IConsumerLifetime subscription = ...Subscribe(...) + var sw = Stopwatch.StartNew(); await subscription.Completion; + sw.Stop(); + _outputHelper.WriteLine($"Consume Duration = {sw.Elapsed:mm\\:ss\\.ff}"); #region Validation - A.CallTo(() => _subscriber.RegisterAsync(A.Ignored)) + A.CallTo(() => _subscriber.RegisterAsync(A.Ignored, A.Ignored)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.LoginAsync("admin", "1234")) + A.CallTo(() => _subscriber.LoginAsync(A.Ignored, "admin", "1234")) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.EarseAsync(4335)) + A.CallTo(() => _subscriber.EarseAsync(A.Ignored, 4335)) .MustHaveHappenedOnceExactly(); #endregion // Validation @@ -196,17 +191,22 @@ public async Task PartialConsumer_Strict_Succeed_Test() IEventFlowProducer producer = _producerBuilder //.WithOptions(producerOption) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildEventFlowProducer(); #endregion // ISequenceOperations producer = ... + var sw = Stopwatch.StartNew(); + var p = new Person(100, "bnaya"); await producer.Stage1Async(p, "ABC"); await producer.Stage2Async(p.ToJson(), "ABC".ToJson()); + var snapshot = sw.Elapsed; + _outputHelper.WriteLine($"Produce = {snapshot:mm\\:ss\\.ff}"); + snapshot = sw.Elapsed; + CancellationToken cancellation = GetCancellationToken(); #region await using IConsumerLifetime subscription = ...Subscribe(...) @@ -215,8 +215,7 @@ public async Task PartialConsumer_Strict_Succeed_Test() .WithOptions(o => DefaultOptions(o, 2, AckBehavior.OnSucceed)) .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .Group("CONSUMER_GROUP_X_1") .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") @@ -227,13 +226,16 @@ public async Task PartialConsumer_Strict_Succeed_Test() await subscription.Completion; + snapshot = sw.Elapsed - snapshot; + _outputHelper.WriteLine($"Consumed = {snapshot:mm\\:ss\\.ff}"); + #region Validation A.CallTo(_fakeLogger).Where(call => call.Method.Name == "Log" && call.GetArgument(0) == LogLevel.Critical).MustNotHaveHappened(); - A.CallTo(() => _stage1Consumer.Stage1Async(A.Ignored, A.Ignored)) + A.CallTo(() => _stage1Consumer.Stage1Async(A.Ignored, A.Ignored, A.Ignored)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _stage2Consumer.Stage2Async(A.Ignored, A.Ignored)) + A.CallTo(() => _stage2Consumer.Stage2Async(A.Ignored, A.Ignored, A.Ignored)) .MustHaveHappenedOnceExactly(); #endregion // Validation @@ -252,8 +254,7 @@ public async Task PartialConsumer_Strict_Fail_Test() IEventFlowProducer producer = _producerBuilder //.WithOptions(producerOption) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildEventFlowProducer(); @@ -271,8 +272,7 @@ public async Task PartialConsumer_Strict_Fail_Test() .WithOptions(o => DefaultOptions(o, 2, AckBehavior.OnSucceed) with { PartialBehavior = PartialConsumerBehavior.ThrowIfNotHandled }) .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .Group("CONSUMER_GROUP_X_1") .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") @@ -299,8 +299,7 @@ public async Task PartialConsumer_Allow_Test() IEventFlowProducer producer = _producerBuilder //.WithOptions(producerOption) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildEventFlowProducer(); @@ -308,12 +307,11 @@ public async Task PartialConsumer_Allow_Test() for (int i = 0; i < times; i++) { - var p = new Person(100, "bnaya"); + var p = new Person(100 + i, "bnaya"); _outputHelper.WriteLine($"Cycle {i}"); await producer.Stage1Async(p, "ABC"); await producer.Stage2Async(p.ToJson(), "ABC".ToJson()); - } CancellationToken cancellation = GetCancellationToken(); @@ -324,8 +322,7 @@ public async Task PartialConsumer_Allow_Test() .WithOptions(o => DefaultOptions(o, times * 2, AckBehavior.OnSucceed, PartialConsumerBehavior.Loose)) .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .Group("CONSUMER_GROUP_X_1") .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") @@ -337,7 +334,7 @@ public async Task PartialConsumer_Allow_Test() #region Validation - A.CallTo(() => _stage2Consumer.Stage2Async(A.Ignored, A.Ignored)) + A.CallTo(() => _stage2Consumer.Stage2Async(A.Ignored, A.Ignored, A.Ignored)) .MustHaveHappened((int)times, Times.Exactly); #endregion // Validation @@ -355,8 +352,7 @@ public async Task Receiver_Test() ISequenceOperationsProducer producer = _producerBuilder //.WithOptions(producerOption) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildSequenceOperationsProducer(); @@ -372,20 +368,22 @@ public async Task Receiver_Test() .WithOptions(o => DefaultOptions(o)) .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildReceiver(); #endregion // IConsumerReceiver receiver = ... AnnouncementData? res0 = await receiver.GetByIdAsync(keys[0]); + Assert.NotNull(res0); var res1 = await receiver.GetByIdAsync(keys[1]); var hasEmail = res1.Data.TryGet("email", out string? email); + Assert.True(hasEmail); Assert.Equal("admin", email); var res2 = await receiver.GetByIdAsync(keys[2]); var hasId = res2.Data.TryGet("id", out int id); Assert.Equal(4335, id); + Assert.True(hasId); } #endregion // Receiver_Test @@ -403,8 +401,7 @@ public async Task Receiver_Json_Test() ISequenceOperationsProducer producer = _producerBuilder //.WithOptions(producerOption) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildSequenceOperationsProducer(); @@ -420,8 +417,7 @@ public async Task Receiver_Json_Test() .WithOptions(o => DefaultOptions(o)) .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildReceiver(); @@ -458,8 +454,7 @@ public async Task Receiver_ChangeEnvironment_AfterBuild() ISequenceOperationsProducer producer = _producerBuilder //.WithOptions(producerOption) .Environment("FakeTest") - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .Environment(ENV) .BuildSequenceOperationsProducer(); @@ -476,8 +471,7 @@ public async Task Receiver_ChangeEnvironment_AfterBuild() .WithOptions(o => DefaultOptions(o)) .WithCancellation(cancellation) .Environment("DemoTest") - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildReceiver() .Environment(ENV); @@ -491,6 +485,9 @@ public async Task Receiver_ChangeEnvironment_AfterBuild() var res2 = await receiver.GetByIdAsync(keys[2]); var hasId = res2.Data.TryGet("id", out int id); Assert.Equal(4335, id); + Assert.NotNull(res0); + Assert.True(hasEmail); + Assert.True(hasId); } #endregion // Receiver_ChangeEnvironment_AfterBuild @@ -505,8 +502,7 @@ public async Task Receiver_ChangeEnvironment_Test() ISequenceOperationsProducer producer = _producerBuilder //.WithOptions(producerOption) .Environment("FakeTest") - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .Environment(ENV) .BuildSequenceOperationsProducer(); @@ -523,8 +519,7 @@ public async Task Receiver_ChangeEnvironment_Test() .WithOptions(o => DefaultOptions(o)) .WithCancellation(cancellation) .Environment("DemoTest") - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .Environment(ENV) .BuildReceiver(); @@ -538,6 +533,9 @@ public async Task Receiver_ChangeEnvironment_Test() var res2 = await receiver.GetByIdAsync(keys[2]); var hasId = res2.Data.TryGet("id", out int id); Assert.Equal(4335, id); + Assert.NotNull(res0); + Assert.True(hasEmail); + Assert.True(hasId); } #endregion // Receiver_ChangeEnvironment_Test @@ -552,14 +550,13 @@ public async Task Iterator_Test() ISequenceOperationsProducer producer = _producerBuilder //.WithOptions(producerOption) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildSequenceOperationsProducer(); #endregion // ISequenceOperations producer = ... - EventKeys keys = await SendSequenceAsync(producer); + await SendSequenceAsync(producer); CancellationToken cancellation = GetCancellationToken(); @@ -569,8 +566,7 @@ public async Task Iterator_Test() .WithOptions(o => DefaultOptions(o)) .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildIterator(); @@ -611,14 +607,13 @@ public async Task Iterator_Cancellation_Test() ISequenceOperationsProducer producer = _producerBuilder //.WithOptions(producerOption) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildSequenceOperationsProducer(); #endregion // ISequenceOperations producer = ... - EventKeys keys = await SendSequenceAsync(producer); + await SendSequenceAsync(producer); var cts = new CancellationTokenSource(); CancellationToken globalCancellation = GetCancellationToken(); @@ -631,8 +626,7 @@ public async Task Iterator_Cancellation_Test() .WithOptions(o => DefaultOptions(o)) .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildIterator(); @@ -651,7 +645,7 @@ public async Task Iterator_Cancellation_Test() { throw new OperationCanceledException("Should have been canceled"); } - else throw new Exception("Should have been canceled"); + else throw new EventSourcingException("Should have been canceled"); i++; } @@ -684,14 +678,13 @@ public async Task Iterator_Json_NoMeta_Test() ISequenceOperationsProducer producer = _producerBuilder //.WithOptions(producerOption) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildSequenceOperationsProducer(); #endregion // ISequenceOperations producer = ... - EventKeys keys = await SendSequenceAsync(producer); + await SendSequenceAsync(producer); CancellationToken cancellation = GetCancellationToken(); @@ -701,8 +694,7 @@ public async Task Iterator_Json_NoMeta_Test() .WithOptions(o => DefaultOptions(o)) .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildIterator(); @@ -750,14 +742,13 @@ public async Task Iterator_Json_Test() ISequenceOperationsProducer producer = _producerBuilder //.WithOptions(producerOption) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildSequenceOperationsProducer(); #endregion // ISequenceOperations producer = ... - EventKeys keys = await SendSequenceAsync(producer); + await SendSequenceAsync(producer); CancellationToken cancellation = GetCancellationToken(); @@ -767,8 +758,7 @@ public async Task Iterator_Json_Test() .WithOptions(o => DefaultOptions(o)) .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildIterator(); @@ -812,17 +802,13 @@ public async Task Iterator_WithFilter_Json_Test() { #region ISequenceOperations producer = ... - ISequenceOperationsProducer producer = _producerBuilder - //.WithOptions(producerOption) - .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + ISequenceOperationsProducer producer = _producerBuilder.Environment(ENV).Uri(URI) .WithLogger(_fakeLogger) .BuildSequenceOperationsProducer(); #endregion // ISequenceOperations producer = ... - EventKeys keys = await SendSequenceAsync(producer); + await SendSequenceAsync(producer); CancellationToken cancellation = GetCancellationToken(); @@ -832,8 +818,7 @@ public async Task Iterator_WithFilter_Json_Test() .WithOptions(o => DefaultOptions(o)) .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildIterator(); @@ -859,7 +844,7 @@ public async Task Iterator_WithFilter_Json_Test() var pj2 = json.GetProperty("id"); Assert.Equal(4335, pj2.GetInt32()); } - else throw new Exception("Should have been filtered"); + else throw new EventSourcingException("Should have been filtered"); i++; } } @@ -876,14 +861,13 @@ public async Task Iterator_MapByType_Test() ISequenceOperationsProducer producer = _producerBuilder //.WithOptions(producerOption) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildSequenceOperationsProducer(); #endregion // ISequenceOperations producer = ... - EventKeys keys = await SendSequenceAsync(producer); + await SendSequenceAsync(producer); CancellationToken cancellation = GetCancellationToken(); @@ -893,8 +877,7 @@ public async Task Iterator_MapByType_Test() .WithOptions(o => DefaultOptions(o)) .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildIterator() .SpecializeSequenceOperationsConsumer(); @@ -924,14 +907,13 @@ public async Task Iterator_MapByType_WithExtension_Test() ISequenceOperationsProducer producer = _producerBuilder //.WithOptions(producerOption) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildSequenceOperationsProducer(); #endregion // ISequenceOperations producer = ... - EventKeys keys = await SendSequenceAsync(producer); + await SendSequenceAsync(producer); CancellationToken cancellation = GetCancellationToken(); @@ -941,8 +923,7 @@ public async Task Iterator_MapByType_WithExtension_Test() .WithOptions(o => DefaultOptions(o)) .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildIterator() .Specialize(UnitTests.Entities.SequenceOperationsConsumerEntityMapper.Default); @@ -962,58 +943,6 @@ public async Task Iterator_MapByType_WithExtension_Test() #endregion // Iterator_MapByType_WithExtension_Test - #region OnSucceed_ACK_Test - - [Fact(Timeout = TIMEOUT)] - public async Task OnSucceed_ACK_Test() - { - #region ISequenceOperations producer = ... - - ISequenceOperationsProducer producer = _producerBuilder - .Environment(ENV) - //.WithOptions(producerOption) - .Partition(PARTITION) - .Shard(SHARD) - .WithLogger(_fakeLogger) - .BuildSequenceOperationsProducer(); - - #endregion // ISequenceOperations producer = ... - - await SendSequenceAsync(producer); - - CancellationToken cancellation = GetCancellationToken(); - - #region await using IConsumerLifetime subscription = ...Subscribe(...) - - await using IConsumerLifetime subscription = _consumerBuilder - .WithOptions(o => DefaultOptions(o, 3, AckBehavior.OnSucceed)) - .WithCancellation(cancellation) - .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) - .WithLogger(_fakeLogger) - .Group("CONSUMER_GROUP_1") - .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") - .Subscribe(_subscriberBridge); - - #endregion // await using IConsumerLifetime subscription = ...Subscribe(...) - - await subscription.Completion; - - #region Validation - - A.CallTo(() => _subscriber.RegisterAsync(A.Ignored)) - .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.LoginAsync("admin", "1234")) - .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.EarseAsync(4335)) - .MustHaveHappenedOnceExactly(); - - #endregion // Validation - } - - #endregion // OnSucceed_ACK_Test - #region Until_Test [Fact(Timeout = TIMEOUT)] @@ -1024,8 +953,7 @@ public async Task Until_Test() ISequenceOperationsProducer producer = _producerBuilder .Environment(ENV) //.WithOptions(producerOption) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildSequenceOperationsProducer(); @@ -1046,8 +974,7 @@ public async Task Until_Test() .WithOptions(o => DefaultOptions(o, 3, AckBehavior.OnSucceed) with { FetchUntilDateOrEmpty = DateTimeOffset.UtcNow }) .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .Group("CONSUMER_GROUP_1") .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") @@ -1060,11 +987,11 @@ public async Task Until_Test() #region Validation - A.CallTo(() => _subscriber.RegisterAsync(A.Ignored)) + A.CallTo(() => _subscriber.RegisterAsync(A.Ignored, A.Ignored)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.LoginAsync("admin", "1234")) + A.CallTo(() => _subscriber.LoginAsync(A.Ignored, "admin", "1234")) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.EarseAsync(4335)) + A.CallTo(() => _subscriber.EarseAsync(A.Ignored, 4335)) .MustHaveHappenedOnceExactly(); #endregion // Validation @@ -1082,8 +1009,7 @@ public async Task GeneratedContract_Test() ISequenceOperationsProducer producer1 = _producerBuilder .Environment(ENV) //.WithOptions(producerOption) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildSequenceOperationsProducer(); @@ -1094,8 +1020,7 @@ public async Task GeneratedContract_Test() IProducerSequenceOperations producer2 = _producerBuilder .Environment(ENV) //.WithOptions(producerOption) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .Build(ProducerSequenceOperationsBridgePipeline.Create); @@ -1112,8 +1037,7 @@ public async Task GeneratedContract_Test() .WithOptions(o => DefaultOptions(o, 6, AckBehavior.OnSucceed)) /* detach consumer after 6 messages*/ .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .Group("CONSUMER_GROUP_1") .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") @@ -1126,11 +1050,11 @@ public async Task GeneratedContract_Test() #region Validation - A.CallTo(() => _autoSubscriber.RegisterAsync(A.Ignored)) + A.CallTo(() => _autoSubscriber.RegisterAsync(A.Ignored, A.Ignored)) .MustHaveHappenedTwiceExactly(); - A.CallTo(() => _autoSubscriber.LoginAsync("admin", "1234")) + A.CallTo(() => _autoSubscriber.LoginAsync(A.Ignored, "admin", "1234")) .MustHaveHappenedTwiceExactly(); - A.CallTo(() => _autoSubscriber.EarseAsync(4335)) + A.CallTo(() => _autoSubscriber.EarseAsync(A.Ignored, 4335)) .MustHaveHappenedTwiceExactly(); #endregion // Validation @@ -1138,286 +1062,6 @@ public async Task GeneratedContract_Test() #endregion // GeneratedContract_Test - #region GeneratedContract_Factory_Test - - [Fact(Timeout = TIMEOUT)] - public async Task GeneratedContract_Factory_Test() - { - #region ISequenceOperations producer1 = ... - - ISequenceOperationsProducer producer1 = _producerBuilder - //.WithOptions(producerOption) - .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) - .WithLogger(_fakeLogger) - .BuildSequenceOperationsProducer(); - - #endregion // ISequenceOperations producer1 = ... - - #region ISequenceOperations producer2 = ... - - IProducerSequenceOperations producer2 = _producerBuilder - .Environment(ENV) - //.WithOptions(producerOption) - .Partition(PARTITION) - .Shard(SHARD) - .WithLogger(_fakeLogger) - .Build(ProducerSequenceOperationsBridgePipeline.Create); - - #endregion // ISequenceOperations producer2 = ... - - await SendSequenceAsync(producer1); - await SendSequenceAsync(producer2); - - CancellationToken cancellation = GetCancellationToken(); - - #region await using IConsumerLifetime subscription = ...Subscribe(...) - - await using IConsumerLifetime subscription = _consumerBuilder - .WithOptions(o => DefaultOptions(o, 6, AckBehavior.OnSucceed)) - .WithCancellation(cancellation) - .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) - .WithLogger(_fakeLogger) - .Group("CONSUMER_GROUP_1") - .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") - .Subscribe(new SequenceOperationsConsumerBridge(_autoSubscriber)); - - #endregion // await using IConsumerLifetime subscription = ...Subscribe(...) - - await subscription.Completion; - - #region Validation - - A.CallTo(() => _autoSubscriber.RegisterAsync(A.Ignored)) - .MustHaveHappenedTwiceExactly(); - A.CallTo(() => _autoSubscriber.LoginAsync("admin", "1234")) - .MustHaveHappenedTwiceExactly(); - A.CallTo(() => _autoSubscriber.EarseAsync(4335)) - .MustHaveHappenedTwiceExactly(); - - #endregion // Validation - } - - #endregion // GeneratedContract_Factory_Test - - #region OnSucceed_ACK_WithFailure_Test - - [Fact(Timeout = TIMEOUT)] - public async Task OnSucceed_ACK_WithFailure_Test() - { - #region ISequenceOperations producer = ... - - ISequenceOperationsProducer producer = _producerBuilder - .Environment(ENV) - //.WithOptions(producerOption) - .Partition(PARTITION) - .Shard(SHARD) - .WithLogger(_fakeLogger) - .BuildSequenceOperationsProducer(); - - #endregion // ISequenceOperations producer = ... - - #region A.CallTo(() => _subscriber.LoginAsync(throw 1 time)) - - int tryNumber = 0; - A.CallTo(() => _subscriber.LoginAsync(A.Ignored, A.Ignored)) - .ReturnsLazily(() => - { - // 3 error will be catch by Polly, the 4th one will catch outside of Polly - if (Interlocked.Increment(ref tryNumber) < 5) - throw new ApplicationException("test intensional exception"); - - return ValueTask.CompletedTask; - }); - - #endregion // A.CallTo(() => _subscriber.LoginAsync(throw 1 time)) - - await SendSequenceAsync(producer); - - CancellationToken cancellation = GetCancellationToken(); - - #region await using IConsumerLifetime subscription = ...Subscribe(...) - - await using IConsumerLifetime subscription = _consumerBuilder - .WithOptions(o => DefaultOptions(o, 4, AckBehavior.OnSucceed)) - .WithCancellation(cancellation) - .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) - .WithResiliencePolicy(Policy.Handle().RetryAsync(3, (ex, i) => _outputHelper.WriteLine($"Retry {i}"))) - .WithLogger(_fakeLogger) - .Group("CONSUMER_GROUP_1") - .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") - .Subscribe(_subscriberBridge); - - #endregion // await using IConsumerLifetime subscription = ...Subscribe(...) - - await subscription.Completion; - - #region Validation - - A.CallTo(() => _subscriber.RegisterAsync(A.Ignored)) - .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.LoginAsync("admin", "1234")) - .MustHaveHappened( - 3 /* Polly retry */ + 1 /* error */ + 1 /* succeed */, - Times.Exactly); - A.CallTo(() => _subscriber.EarseAsync(4335)) - .MustHaveHappenedOnceExactly(); - - #endregion // Validation - } - - #endregion // OnSucceed_ACK_WithFailure_Test - - #region OnFinaly_ACK_WithFailure_Test - - [Fact(Timeout = TIMEOUT)] - public async Task OnFinaly_ACK_WithFailure_Test() - { - #region ISequenceOperations producer = ... - - ISequenceOperationsProducer producer = _producerBuilder - .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) - .WithLogger(_fakeLogger) - .BuildSequenceOperationsProducer(); - - #endregion // ISequenceOperations producer = ... - - #region A.CallTo(() => _subscriber.LoginAsync(throw 1 time)) - - int tryNumber = 0; - A.CallTo(() => _subscriber.LoginAsync(A.Ignored, A.Ignored)) - .ReturnsLazily(() => - { - // 3 error will be catch by Polly, the 4th one will catch outside of Polly - if (Interlocked.Increment(ref tryNumber) < 5) - throw new ApplicationException("test intensional exception"); - - return ValueTask.CompletedTask; - }); - - #endregion // A.CallTo(() => _subscriber.LoginAsync(throw 1 time)) - - await SendSequenceAsync(producer); - - - CancellationToken cancellation = GetCancellationToken(); - - #region await using IConsumerLifetime subscription = ...Subscribe(...) - - await using IConsumerLifetime subscription = _consumerBuilder - .WithOptions(o => DefaultOptions(o, 3, AckBehavior.OnFinally)) - .WithCancellation(cancellation) - .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) - .WithResiliencePolicy(Policy.Handle().RetryAsync(3, (ex, i) => _outputHelper.WriteLine($"Retry {i}"))) - .WithLogger(_fakeLogger) - .Group("CONSUMER_GROUP_1") - .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") - .Subscribe(_subscriberBridge); - - #endregion // await using IConsumerLifetime subscription = ...Subscribe(...) - - await subscription.Completion; - - #region Validation - - A.CallTo(() => _subscriber.RegisterAsync(A.Ignored)) - .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.LoginAsync("admin", "1234")) - .MustHaveHappened( - 3 /* Polly retry */ + 1 /* error */ , - Times.Exactly); - A.CallTo(() => _subscriber.EarseAsync(4335)) - .MustHaveHappenedOnceExactly(); - - #endregion // Validation - } - - #endregion // OnFinaly_ACK_WithFailure_Test - - #region Manual_ACK_Test - - [Fact(Timeout = TIMEOUT)] - public async Task Manual_ACK_Test() - { - #region ISequenceOperations producer = ... - - ISequenceOperationsProducer producer = _producerBuilder - .Environment(ENV) - //.WithOptions(producerOption) - .Partition(PARTITION) - .Shard(SHARD) - .BuildSequenceOperationsProducer(); - - #endregion // ISequenceOperations producer = ... - - #region A.CallTo(...).ReturnsLazily(...) - - - int tryNumber = 0; - A.CallTo(() => _subscriber.RegisterAsync(A.Ignored)) - .ReturnsLazily(() => Ack.Current.AckAsync()); - A.CallTo(() => _subscriber.LoginAsync(A.Ignored, A.Ignored)) - .ReturnsLazily(async () => - { - // 3 error will be catch by Polly, the 4th one will catch outside of Polly - if (Interlocked.Increment(ref tryNumber) < 5) - throw new ApplicationException("test intensional exception"); - - await Ack.Current.AckAsync(); - }); - A.CallTo(() => _subscriber.EarseAsync(A.Ignored)) - .ReturnsLazily(() => Ack.Current.AckAsync()); - - #endregion // A.CallTo(...).ReturnsLazily(...) - - - await SendSequenceAsync(producer); - - CancellationToken cancellation = GetCancellationToken(); - - #region await using IConsumerLifetime subscription = ...Subscribe(...) - - await using IConsumerLifetime subscription = _consumerBuilder - .WithOptions(o => DefaultOptions(o, 4, AckBehavior.Manual)) - .WithCancellation(cancellation) - .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) - .WithResiliencePolicy(Policy.Handle().RetryAsync(3, (ex, i) => _outputHelper.WriteLine($"Retry {i}"))) - .WithLogger(_fakeLogger) - .Group("CONSUMER_GROUP_1") - .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") - .Subscribe(_subscriberBridge); - - #endregion // await using IConsumerLifetime subscription = ...Subscribe(...) - - await subscription.Completion; - - #region Validation - - A.CallTo(() => _subscriber.RegisterAsync(A.Ignored)) - .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.LoginAsync("admin", "1234")) - .MustHaveHappened( - 3 /* Polly retry */ + 1 /* error */ + 1 /* succeed */, - Times.Exactly); - A.CallTo(() => _subscriber.EarseAsync(4335)) - .MustHaveHappenedOnceExactly(); - - #endregion // Validation - } - - #endregion // Manual_ACK_Test - #region Resilience_Test [Fact(Timeout = TIMEOUT)] @@ -1428,16 +1072,15 @@ public async Task Resilience_Test() ISequenceOperationsProducer producer = _producerBuilder .Environment(ENV) //.WithOptions(producerOption) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .BuildSequenceOperationsProducer(); #endregion // ISequenceOperations producer = ... int tryNumber = 0; - A.CallTo(() => _subscriber.RegisterAsync(A.Ignored)) + A.CallTo(() => _subscriber.RegisterAsync(A.Ignored, A.Ignored)) .ReturnsLazily(() => Ack.Current.AckAsync()); - A.CallTo(() => _subscriber.LoginAsync(A.Ignored, A.Ignored)) + A.CallTo(() => _subscriber.LoginAsync(A.Ignored, A.Ignored, A.Ignored)) .ReturnsLazily(async () => { if (Interlocked.Increment(ref tryNumber) == 1) @@ -1445,7 +1088,7 @@ public async Task Resilience_Test() await Ack.Current.AckAsync(); }); - A.CallTo(() => _subscriber.EarseAsync(A.Ignored)) + A.CallTo(() => _subscriber.EarseAsync(A.Ignored, A.Ignored)) .ReturnsLazily(() => Ack.Current.AckAsync()); @@ -1459,8 +1102,7 @@ public async Task Resilience_Test() .WithOptions(o => DefaultOptions(o, 3, AckBehavior.Manual)) .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithResiliencePolicy(Policy.Handle().RetryAsync(3)) .WithLogger(_fakeLogger) .Group("CONSUMER_GROUP_1") @@ -1473,11 +1115,11 @@ public async Task Resilience_Test() #region Validation - A.CallTo(() => _subscriber.RegisterAsync(A.Ignored)) + A.CallTo(() => _subscriber.RegisterAsync(A.Ignored, A.Ignored)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.LoginAsync("admin", "1234")) + A.CallTo(() => _subscriber.LoginAsync(A.Ignored, "admin", "1234")) .MustHaveHappenedTwiceExactly(); /* 1 Polly, 1 succeed */ - A.CallTo(() => _subscriber.EarseAsync(4335)) + A.CallTo(() => _subscriber.EarseAsync(A.Ignored, 4335)) .MustHaveHappenedOnceExactly(); #endregion // Validation @@ -1495,31 +1137,28 @@ public async Task Override_Test() var producerBuilder = _producerBuilder .Environment(ENV) //.WithOptions(producerOption) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger); ISequenceOperationsProducer producer = producerBuilder.BuildSequenceOperationsProducer(); ISequenceOperationsProducer producerPrefix = producerBuilder .Specialize() - .Environment("dev") - .Partition("p0.") - .Shard("p1.") + .Environment("test-override") + .Uri("p0.") .BuildSequenceOperationsProducer(); ISequenceOperationsProducer producerPrefix1 = producerBuilder .Specialize() - .Partition("p2.").BuildSequenceOperationsProducer(); + .Uri("p2.").BuildSequenceOperationsProducer(); ISequenceOperationsProducer producerSuffix = producerBuilder .Specialize() - .Partition(".s0", RouteAssignmentType.Suffix) - .Shard(".s1", RouteAssignmentType.Suffix) + .Uri(".s0", RouteAssignmentType.Suffix) .BuildSequenceOperationsProducer(); ISequenceOperationsProducer producerSuffix1 = producerBuilder .Specialize() - .Partition(".s2", RouteAssignmentType.Suffix) + .Uri(".s2", RouteAssignmentType.Suffix) .BuildSequenceOperationsProducer(); ISequenceOperationsProducer producerDynamic = producerBuilder.Environment("Fake Env") .Specialize() - .Strategy(m => (ENV, $"d.{m.Partition}", $"{m.Shard}.d")) + .Strategy(m => (ENV, $"d.{m.Uri}")) .BuildSequenceOperationsProducer(); #endregion // ISequenceOperations producer = ... @@ -1539,8 +1178,7 @@ public async Task Override_Test() .WithOptions(o => DefaultOptions(o, 3, AckBehavior.OnSucceed)) .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .Group("CONSUMER_GROUP_1") .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") @@ -1549,9 +1187,8 @@ public async Task Override_Test() await using IConsumerLifetime subscriptionPrefix = _consumerBuilder .WithOptions(o => DefaultOptions(o, 3, AckBehavior.OnSucceed)) .WithCancellation(cancellation) - .Environment("dev") - .Partition($"p0.{PARTITION}") - .Shard($"p1.{SHARD}") + .Environment("test-override") + .Uri($"p0.{URI}") .WithLogger(_fakeLogger) .Group("CONSUMER_GROUP_1") .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") @@ -1561,8 +1198,7 @@ public async Task Override_Test() .WithOptions(o => DefaultOptions(o, 3, AckBehavior.OnSucceed)) .WithCancellation(cancellation) .Environment(ENV) - .Partition($"p2.{PARTITION}") - .Shard(SHARD) + .Uri($"p2.{URI}") .WithLogger(_fakeLogger) .Group("CONSUMER_GROUP_1") .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") @@ -1572,8 +1208,7 @@ public async Task Override_Test() .WithOptions(o => DefaultOptions(o, 3, AckBehavior.OnSucceed)) .WithCancellation(cancellation) .Environment(ENV) - .Partition($"{PARTITION}.s0") - .Shard($"{SHARD}.s1") + .Uri($"{URI}.s0") .WithLogger(_fakeLogger) .Group("CONSUMER_GROUP_1") .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") @@ -1583,8 +1218,7 @@ public async Task Override_Test() .WithOptions(o => DefaultOptions(o, 3, AckBehavior.OnSucceed)) .WithCancellation(cancellation) .Environment(ENV) - .Partition($"{PARTITION}.s2") - .Shard(SHARD) + .Uri($"{URI}.s2") .WithLogger(_fakeLogger) .Group("CONSUMER_GROUP_1") .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") @@ -1594,8 +1228,7 @@ public async Task Override_Test() .WithOptions(o => DefaultOptions(o, 3, AckBehavior.OnSucceed)) .WithCancellation(cancellation) .Environment(ENV) - .Partition($"d.{PARTITION}") - .Shard($"{SHARD}.d") + .Uri($"d.{URI}") .WithLogger(_fakeLogger) .Group("CONSUMER_GROUP_D") .Name($"TEST_D {DateTime.UtcNow:HH:mm:ss}") @@ -1613,46 +1246,46 @@ await Task.WhenAll( #region Validation - A.CallTo(() => _subscriber.RegisterAsync(A.Ignored)) + A.CallTo(() => _subscriber.RegisterAsync(A.Ignored, A.Ignored)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.LoginAsync("admin", "1234")) + A.CallTo(() => _subscriber.LoginAsync(A.Ignored, "admin", "1234")) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.EarseAsync(4335)) + A.CallTo(() => _subscriber.EarseAsync(A.Ignored, 4335)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriberPrefix.RegisterAsync(A.Ignored)) + A.CallTo(() => _subscriberPrefix.RegisterAsync(A.Ignored, A.Ignored)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriberPrefix.LoginAsync("admin", "p0")) + A.CallTo(() => _subscriberPrefix.LoginAsync(A.Ignored, "admin", "p0")) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriberPrefix.EarseAsync(4335)) + A.CallTo(() => _subscriberPrefix.EarseAsync(A.Ignored, 4335)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriberPrefix1.RegisterAsync(A.Ignored)) + A.CallTo(() => _subscriberPrefix1.RegisterAsync(A.Ignored, A.Ignored)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriberPrefix1.LoginAsync("admin", "p1")) + A.CallTo(() => _subscriberPrefix1.LoginAsync(A.Ignored, "admin", "p1")) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriberPrefix1.EarseAsync(4335)) + A.CallTo(() => _subscriberPrefix1.EarseAsync(A.Ignored, 4335)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriberSuffix.RegisterAsync(A.Ignored)) + A.CallTo(() => _subscriberSuffix.RegisterAsync(A.Ignored, A.Ignored)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriberSuffix.LoginAsync("admin", "s0")) + A.CallTo(() => _subscriberSuffix.LoginAsync(A.Ignored, "admin", "s0")) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriberSuffix.EarseAsync(4335)) + A.CallTo(() => _subscriberSuffix.EarseAsync(A.Ignored, 4335)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriberSuffix1.RegisterAsync(A.Ignored)) + A.CallTo(() => _subscriberSuffix1.RegisterAsync(A.Ignored, A.Ignored)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriberSuffix1.LoginAsync("admin", "s1")) + A.CallTo(() => _subscriberSuffix1.LoginAsync(A.Ignored, "admin", "s1")) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriberSuffix1.EarseAsync(4335)) + A.CallTo(() => _subscriberSuffix1.EarseAsync(A.Ignored, 4335)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriberDynamic.RegisterAsync(A.Ignored)) + A.CallTo(() => _subscriberDynamic.RegisterAsync(A.Ignored, A.Ignored)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriberDynamic.LoginAsync("admin", "d")) + A.CallTo(() => _subscriberDynamic.LoginAsync(A.Ignored, "admin", "d")) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriberDynamic.EarseAsync(4335)) + A.CallTo(() => _subscriberDynamic.EarseAsync(A.Ignored, 4335)) .MustHaveHappenedOnceExactly(); #endregion // Validation @@ -1673,22 +1306,21 @@ public async Task Claim_Test() ISequenceOperationsProducer producer = _producerBuilder .Environment(ENV) //.WithOptions(producerOption) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .BuildSequenceOperationsProducer(); #endregion // ISequenceOperations producer = ... #region A.CallTo(...).ReturnsLazily(...) - A.CallTo(() => otherSubscriber.RegisterAsync(A.Ignored)) + A.CallTo(() => otherSubscriber.RegisterAsync(A.Ignored, A.Ignored)) .ReturnsLazily(() => { throw new ApplicationException("test intensional exception"); }); - A.CallTo(() => otherSubscriber.LoginAsync(A.Ignored, A.Ignored)) + A.CallTo(() => otherSubscriber.LoginAsync(A.Ignored, A.Ignored, A.Ignored)) .ReturnsLazily(() => ValueTask.CompletedTask); - A.CallTo(() => otherSubscriber.EarseAsync(A.Ignored)) + A.CallTo(() => otherSubscriber.EarseAsync(A.Ignored, A.Ignored)) .ReturnsLazily(() => ValueTask.CompletedTask); #endregion // A.CallTo(...).ReturnsLazily(...) @@ -1704,8 +1336,7 @@ public async Task Claim_Test() .WithOptions(o => DefaultOptions(o, 3, AckBehavior.OnSucceed)) .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithResiliencePolicy(Policy.Handle().RetryAsync(3)) .WithLogger(_fakeLogger); @@ -1735,18 +1366,18 @@ public async Task Claim_Test() #region Validation - A.CallTo(() => otherSubscriber.RegisterAsync(A.Ignored)) + A.CallTo(() => otherSubscriber.RegisterAsync(A.Ignored, A.Ignored)) .MustHaveHappened( (3 /* Polly retry */), Times.OrMore); //.MustHaveHappened( // (3 /* Polly retry */ + 1 /* throw */ ) * 3 /* disconnect after 3 messaged */ , // Times.Exactly); - A.CallTo(() => _subscriber.RegisterAsync(A.Ignored)) + A.CallTo(() => _subscriber.RegisterAsync(A.Ignored, A.Ignored)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.LoginAsync("admin", "1234")) + A.CallTo(() => _subscriber.LoginAsync(A.Ignored, "admin", "1234")) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.EarseAsync(4335)) + A.CallTo(() => _subscriber.EarseAsync(A.Ignored, 4335)) .MustHaveHappenedOnceExactly(); #endregion // Validation @@ -1756,13 +1387,6 @@ public async Task Claim_Test() #region SendSequenceAsync - private static async Task SendSequenceAsync(ISequenceOperations producer, string pass = "1234") - { - await producer.RegisterAsync(USER); - await producer.LoginAsync("admin", pass); - await producer.EarseAsync(4335); - } - private static async Task SendSequenceAsync(ISequenceOperationsProducer producer, string pass = "1234") { EventKey r1 = await producer.RegisterAsync(USER with { Comment = null }); @@ -1780,17 +1404,6 @@ private static async Task SendSequenceAsync(IProducerSequenceOperatio #endregion // SendSequenceAsync - #region SendLongSequenceAsync - - private static async Task SendLongSequenceAsync(ISequenceOperationsProducer producer, string pass = "1234") - { - var tasks = Enumerable.Range(1, 1500).Select(async m => await producer.SuspendAsync(m)); - EventKeys[] ids = await Task.WhenAll(tasks); - return ids[ids.Length - 1].First(); - } - - #endregion // SendLongSequenceAsync - #region GetCancellationToken /// @@ -1805,75 +1418,5 @@ private static CancellationToken GetCancellationToken() } #endregion // GetCancellationToken - - #region Dispose pattern - - - ~EndToEndTests() - { - Dispose(); - } - - public void Dispose() - { - GC.SuppressFinalize(this); - try - { - IConnectionMultiplexer conn = RedisClientFactory.CreateProviderBlocking( - cfg => cfg.AllowAdmin = true); - string serverName = Environment.GetEnvironmentVariable(END_POINT_KEY) ?? "localhost:6379"; - var server = conn.GetServer(serverName); - IEnumerable keys = server.Keys(pattern: $"*{PARTITION}*"); - IDatabaseAsync db = conn.GetDatabase(); - - var ab = new ActionBlock(k => LocalAsync(k), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 30 }); -#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. -#pragma warning disable CS8604 // Possible null reference argument. - foreach (string key in keys) - { - ab.Post(key); - } -#pragma warning restore CS8604 // Possible null reference argument. -#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. - - ab.Complete(); - ab.Completion.Wait(); - - async Task LocalAsync(string k) - { - try - { - await db.KeyDeleteAsync(k, CommandFlags.DemandMaster); - _outputHelper.WriteLine($"Cleanup: delete key [{k}]"); - } - #region Exception Handling - - catch (RedisTimeoutException ex) - { - _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); - } - catch (Exception ex) - { - _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); - } - - #endregion // Exception Handling - } - } - #region Exception Handling - - catch (RedisTimeoutException ex) - { - _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); - } - catch (Exception ex) - { - _outputHelper.WriteLine($"Test dispose error (delete keys) {ex.FormatLazy()}"); - } - - #endregion // Exception Handling - } - - #endregion // Dispose pattern } } diff --git a/Tests/EventSourcing.Backbone.IntegrationTests/Entities/Person.cs b/Tests/EventSourcing.Backbone.IntegrationTests/Entities/Person.cs new file mode 100644 index 00000000..94d9c95c --- /dev/null +++ b/Tests/EventSourcing.Backbone.IntegrationTests/Entities/Person.cs @@ -0,0 +1,5 @@ +namespace EventSourcing.Backbone.UnitTests.Entities +{ + public record Person(int Id, string Name); + +} diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Entities/User.cs b/Tests/EventSourcing.Backbone.IntegrationTests/Entities/User.cs similarity index 93% rename from Tests/Weknow.EventSource.Backbone.IntegrationTests/Entities/User.cs rename to Tests/EventSourcing.Backbone.IntegrationTests/Entities/User.cs index 3f7c31ff..bc383200 100644 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Entities/User.cs +++ b/Tests/EventSourcing.Backbone.IntegrationTests/Entities/User.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.UnitTests.Entities +namespace EventSourcing.Backbone.UnitTests.Entities { public record User { diff --git a/Tests/EventSourcing.Backbone.IntegrationTests/EventSourcing.Backbone.IntegrationTests.csproj b/Tests/EventSourcing.Backbone.IntegrationTests/EventSourcing.Backbone.IntegrationTests.csproj new file mode 100644 index 00000000..86530467 --- /dev/null +++ b/Tests/EventSourcing.Backbone.IntegrationTests/EventSourcing.Backbone.IntegrationTests.csproj @@ -0,0 +1,74 @@ + + + + Debug;Release;Gen + false + + + + + + + + + + + + PreserveNewest + true + PreserveNewest + + + PreserveNewest + true + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/EventSourcing.Backbone.IntegrationTests/HelloWorld/HelloWorldTests.cs b/Tests/EventSourcing.Backbone.IntegrationTests/HelloWorld/HelloWorldTests.cs new file mode 100644 index 00000000..d40d8a37 --- /dev/null +++ b/Tests/EventSourcing.Backbone.IntegrationTests/HelloWorld/HelloWorldTests.cs @@ -0,0 +1,157 @@ +using EventSourcing.Backbone.Tests; +using EventSourcing.Backbone.UnitTests.Entities; + +using FakeItEasy; + +using Xunit; +using Xunit.Abstractions; + +// docker run -p 6379:6379 -it --rm --name redis-evt-src redislabs/rejson:latest + +namespace EventSourcing.Backbone.IntegrationTests.HelloWorld +{ + /// + /// The end to end tests. + /// + public class HelloWorldTests : TestsBase + { + protected override string URI { get; } = $"demo:{DateTimeOffset.Now.ToUnixTimeMilliseconds}"; + private readonly IHelloConsumer _subscriber = A.Fake(); + + #region Ctor + + /// + /// Initializes a new instance of the class. + /// + /// The output helper. + public HelloWorldTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + } + + #endregion // Ctor + + #region HelloWorld_Minimal_Test + + [Fact(Timeout = TIMEOUT)] + public async Task HelloWorld_Minimal_Test() + { + IHelloProducer producer = RedisProducerBuilder.Create() + .Environment("testing") + .Uri(URI) + .BuildHelloProducer(); + + IConsumerLifetime subscription = RedisConsumerBuilder.Create() + .Environment("testing") + .Uri(URI) + .SubscribeHelloConsumer(_subscriber); + + A.CallTo(() => _subscriber.WorldAsync(A.Ignored, 5)) + .Invokes(() => subscription.DisposeAsync()); + + await producer.HelloAsync("Hi"); + await producer.WorldAsync(5); + + + await subscription.Completion; + + #region Validation + + A.CallTo(() => _subscriber.HelloAsync(A.Ignored, "Hi")) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _subscriber.WorldAsync(A.Ignored, 5)) + .MustHaveHappenedOnceExactly(); + + #endregion // Validation + } + + #endregion // HelloWorld_Minimal_Test + + #region HelloWorld_Test + + [Fact(Timeout = TIMEOUT)] + public async Task HelloWorld_Test() + { + IHelloProducer producer = RedisProducerBuilder.Create() + .Environment("testing") + .Uri(URI) + .WithLogger(_fakeLogger) + .BuildHelloProducer(); + + var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + IConsumerLifetime subscription = RedisConsumerBuilder.Create() + .WithOptions(o => new ConsumerOptions + { + MaxMessages = 2, // disconnect after consuming 2 messages + AckBehavior = AckBehavior.OnSucceed + }) + .WithCancellation(cancellation.Token) + .Environment("testing") + .Uri(URI) + .WithLogger(_fakeLogger) + .Group("CONSUMER_GROUP_1") // the consumer group + .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") // the name of the specific consumer + .SubscribeHelloConsumer(_subscriber); + + await producer.HelloAsync("Hi"); + await producer.WorldAsync(5); + + + await subscription.Completion; + + #region Validation + + A.CallTo(() => _subscriber.HelloAsync(A.Ignored, "Hi")) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _subscriber.WorldAsync(A.Ignored, 5)) + .MustHaveHappenedOnceExactly(); + + #endregion // Validation + } + + #endregion // HelloWorld_Test + + #region HelloWorld_Direct_Endpoint_Test + + [Fact(Timeout = TIMEOUT)] + public async Task HelloWorld_Direct_Endpoint_Test() + { + IHelloProducer producer = RedisProducerBuilder.Create("localhost:6379,localhost:6380") + .Environment("testing") + .Uri(URI) + .WithLogger(_fakeLogger) + .BuildHelloProducer(); + + var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + IConsumerLifetime subscription = RedisConsumerBuilder.Create("localhost:6379,localhost:6380") + .WithOptions(o => new ConsumerOptions + { + MaxMessages = 2, // disconnect after consuming 2 messages + AckBehavior = AckBehavior.OnSucceed + }) + .WithCancellation(cancellation.Token) + .Environment("testing") + .Uri(URI) + .WithLogger(_fakeLogger) + .Group("CONSUMER_GROUP_1") // the consumer group + .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") // the name of the specific consumer + .SubscribeHelloConsumer(_subscriber); + + await producer.HelloAsync("Hi"); + await producer.WorldAsync(5); + + + await subscription.Completion; + + #region Validation + + A.CallTo(() => _subscriber.HelloAsync(A.Ignored, "Hi")) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _subscriber.WorldAsync(A.Ignored, 5)) + .MustHaveHappenedOnceExactly(); + + #endregion // Validation + } + + #endregion // HelloWorld_Direct_Endpoint_Test + } +} diff --git a/Tests/EventSourcing.Backbone.IntegrationTests/HelloWorld/IHello.cs b/Tests/EventSourcing.Backbone.IntegrationTests/HelloWorld/IHello.cs new file mode 100644 index 00000000..7ddebe28 --- /dev/null +++ b/Tests/EventSourcing.Backbone.IntegrationTests/HelloWorld/IHello.cs @@ -0,0 +1,13 @@ +namespace EventSourcing.Backbone.UnitTests.Entities +{ + /// + /// The sequence operations. + /// + [EventsContract(EventsContractType.Producer)] + [EventsContract(EventsContractType.Consumer)] + public interface IHello + { + ValueTask HelloAsync(string message); + ValueTask WorldAsync(int value); + } +} diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/InheritanceS3StoreStrategyTests.cs b/Tests/EventSourcing.Backbone.IntegrationTests/InheritanceS3StoreStrategyTests.cs similarity index 72% rename from Tests/Weknow.EventSource.Backbone.IntegrationTests/InheritanceS3StoreStrategyTests.cs rename to Tests/EventSourcing.Backbone.IntegrationTests/InheritanceS3StoreStrategyTests.cs index 22a8c659..fa6c4708 100644 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/InheritanceS3StoreStrategyTests.cs +++ b/Tests/EventSourcing.Backbone.IntegrationTests/InheritanceS3StoreStrategyTests.cs @@ -1,6 +1,6 @@ using Xunit.Abstractions; -namespace Weknow.EventSource.Backbone.Tests +namespace EventSourcing.Backbone.Tests { public class InheritanceS3StoreStrategyTests : InheritanceTests { @@ -12,8 +12,8 @@ public class InheritanceS3StoreStrategyTests : InheritanceTests public InheritanceS3StoreStrategyTests(ITestOutputHelper outputHelper) : base(outputHelper, - (b, logger) => b.AddS3Strategy(OPTIONS), - (b, logger) => b.AddS3Strategy(OPTIONS)) + (b, logger) => b.AddS3Storage(OPTIONS), + (b, logger) => b.AddS3Storage(OPTIONS)) { } diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/InheritanceTests.cs b/Tests/EventSourcing.Backbone.IntegrationTests/InheritanceTests.cs similarity index 60% rename from Tests/Weknow.EventSource.Backbone.IntegrationTests/InheritanceTests.cs rename to Tests/EventSourcing.Backbone.IntegrationTests/InheritanceTests.cs index a9935719..1bb657a1 100644 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/InheritanceTests.cs +++ b/Tests/EventSourcing.Backbone.IntegrationTests/InheritanceTests.cs @@ -2,32 +2,28 @@ using System.Collections.Concurrent; using System.Diagnostics; -using System.Threading.Tasks.Dataflow; + +using EventSourcing.Backbone.Building; +using EventSourcing.Backbone.Channels.RedisProvider; +using EventSourcing.Backbone.Enums; +using EventSourcing.Backbone.UnitTests.Entities; using FakeItEasy; using Microsoft.Extensions.Logging; -using StackExchange.Redis; - -using Weknow.EventSource.Backbone.Building; -using Weknow.EventSource.Backbone.Enums; - using Xunit; using Xunit.Abstractions; -using static Weknow.EventSource.Backbone.Channels.RedisProvider.Common.RedisChannelConstants; +// docker run -p 6379:6379 -it --rm --name redis-evt-src redislabs/rejson:latest -// docker run -p 6379:6379 -it --rm --name redis-event-source redislabs/rejson:latest - -namespace Weknow.EventSource.Backbone.Tests +namespace EventSourcing.Backbone.Tests { /// /// The end to end tests. /// - public class InheritanceTests : IDisposable + public class InheritanceTests : TestsBase { - private readonly ITestOutputHelper _outputHelper; private readonly IFlowAConsumer _subscriberA = A.Fake(); private readonly IFlowBConsumer _subscriberB = A.Fake(); private readonly IFlowABConsumer _subscriberAB = A.Fake(); @@ -35,12 +31,9 @@ public class InheritanceTests : IDisposable private readonly IProducerStoreStrategyBuilder _producerBuilder; private readonly IConsumerStoreStrategyBuilder _consumerBuilder; - private readonly string ENV = $"Development"; - private readonly string PARTITION = $"{DateTime.UtcNow:yyyy-MM-dd HH_mm_ss}:{Guid.NewGuid():N}"; - private readonly string SHARD = $"some-shard-{DateTime.UtcNow.Second}"; + private readonly string ENV = $"test"; + protected override string URI { get; } = $"{DateTime.UtcNow:yyyy-MM-dd HH_mm_ss}:{Guid.NewGuid():N}"; - private readonly ILogger _fakeLogger = A.Fake(); - private const int TIMEOUT = 1_000 * 50; #region Ctor @@ -54,20 +47,19 @@ public InheritanceTests( ITestOutputHelper outputHelper, Func? producerChannelBuilder = null, Func? consumerChannelBuilder = null) + : base(outputHelper) { - _outputHelper = outputHelper; _producerBuilder = ProducerBuilder.Empty.UseRedisChannel( /*, configuration: (cfg) => cfg.ServiceName = "mymaster" */); _producerBuilder = producerChannelBuilder?.Invoke(_producerBuilder, _fakeLogger) ?? _producerBuilder; - var consumerBuilder = ConsumerBuilder.Empty.UseRedisChannel( - stg => stg with - { - DelayWhenEmptyBehavior = - stg.DelayWhenEmptyBehavior with - { - CalcNextDelay = (d => TimeSpan.FromMilliseconds(2)) - } - }); + RedisConsumerChannelSetting stg = new RedisConsumerChannelSetting + { + DelayWhenEmptyBehavior = new DelayWhenEmptyBehavior + { + CalcNextDelay = ((d, _) => TimeSpan.FromMilliseconds(2)) + } + }; + var consumerBuilder = stg.CreateRedisConsumerBuilder(); _consumerBuilder = consumerChannelBuilder?.Invoke(consumerBuilder, _fakeLogger) ?? consumerBuilder; } @@ -104,8 +96,7 @@ public async Task Inheritance_PartialConsumer_Strict_Succeed_Test(MultiConsumerB IFlowABProducer producer = _producerBuilder .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .BuildFlowABProducer(); #endregion // ISequenceOperations producer = ... @@ -120,15 +111,15 @@ public async Task Inheritance_PartialConsumer_Strict_Succeed_Test(MultiConsumerB #region Prepare var hash = new ConcurrentDictionary(); - A.CallTo(() => _subscriberA.AAsync(1)) + A.CallTo(() => _subscriberA.AAsync(A.Ignored, 1)) .Invokes(c => { hash.AddOrUpdate(c.Method.Name, 1, (k, v) => v + 1); }); - A.CallTo(() => _subscriberB.BAsync(A.Ignored)) + A.CallTo(() => _subscriberB.BAsync(A.Ignored, A.Ignored)) .Invokes(c => { hash.AddOrUpdate(c.Method.Name, 1, (k, v) => v + 1); }); - A.CallTo(() => _subscriberAB.DerivedAsync("Hi")) + A.CallTo(() => _subscriberAB.DerivedAsync(A.Ignored, "Hi")) .Invokes(c => { hash.AddOrUpdate(c.Method.Name, 1, (k, v) => v + 1); }); - A.CallTo(() => _subscriberAB.AAsync(1)) + A.CallTo(() => _subscriberAB.AAsync(A.Ignored, 1)) .Invokes(c => { hash.AddOrUpdate(c.Method.Name, 1, (k, v) => v + 1); }); - A.CallTo(() => _subscriberAB.BAsync(A.Ignored)) + A.CallTo(() => _subscriberAB.BAsync(A.Ignored, A.Ignored)) .Invokes(c => { hash.AddOrUpdate(c.Method.Name, 1, (k, v) => v + 1); }); #endregion // Prepare @@ -142,8 +133,7 @@ public async Task Inheritance_PartialConsumer_Strict_Succeed_Test(MultiConsumerB }) .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .Group("CONSUMER_GROUP_X_1") .Name($"TEST {DateTime.UtcNow:HH:mm:ss}") .SubscribeFlowAConsumer(_subscriberA) @@ -161,15 +151,15 @@ public async Task Inheritance_PartialConsumer_Strict_Succeed_Test(MultiConsumerB if (multiConsumerBehavior == MultiConsumerBehavior.All) { Assert.True(hash.All(m => m.Value >= 1)); - A.CallTo(() => _subscriberA.AAsync(1)) + A.CallTo(() => _subscriberA.AAsync(A.Ignored, 1)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriberB.BAsync(A.Ignored)) + A.CallTo(() => _subscriberB.BAsync(A.Ignored, A.Ignored)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriberAB.DerivedAsync("Hi")) + A.CallTo(() => _subscriberAB.DerivedAsync(A.Ignored, "Hi")) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriberAB.AAsync(1)) + A.CallTo(() => _subscriberAB.AAsync(A.Ignored, 1)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriberAB.BAsync(A.Ignored)) + A.CallTo(() => _subscriberAB.BAsync(A.Ignored, A.Ignored)) .MustHaveHappenedOnceExactly(); } else @@ -191,8 +181,7 @@ public async Task Inheritance_ConsumerCooperation_Succeed_Test() IFlowABProducer producer = _producerBuilder .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .BuildFlowABProducer(); #endregion // ISequenceOperations producer = ... @@ -202,21 +191,47 @@ public async Task Inheritance_ConsumerCooperation_Succeed_Test() await producer.BAsync(now); await producer.DerivedAsync("Hi"); - CancellationToken cancellation = GetCancellationToken(20); - + CancellationToken cancellation = GetCancellationToken(10); + int i = 0; + var tcs = new TaskCompletionSource(); #region Prepare var hash = new ConcurrentDictionary(); - A.CallTo(() => _subscriberA.AAsync(1)) - .Invokes(c => { hash.AddOrUpdate(c.Method.Name, 1, (k, v) => v + 1); }); - A.CallTo(() => _subscriberB.BAsync(A.Ignored)) - .Invokes(c => { hash.AddOrUpdate(c.Method.Name, 1, (k, v) => v + 1); }); - A.CallTo(() => _subscriberAB.DerivedAsync("Hi")) - .Invokes(c => { hash.AddOrUpdate(c.Method.Name, 1, (k, v) => v + 1); }); - A.CallTo(() => _subscriberAB.AAsync(1)) - .Invokes(c => { hash.AddOrUpdate(c.Method.Name, 1, (k, v) => v + 1); }); - A.CallTo(() => _subscriberAB.BAsync(A.Ignored)) - .Invokes(c => { hash.AddOrUpdate(c.Method.Name, 1, (k, v) => v + 1); }); + A.CallTo(() => _subscriberA.AAsync(A.Ignored, 1)) + .Invokes(c => + { + hash.AddOrUpdate(c.Method.Name, 1, (k, v) => v + 1); + if (Interlocked.Increment(ref i) == 3) + tcs.SetResult(i); + }); + A.CallTo(() => _subscriberB.BAsync(A.Ignored, A.Ignored)) + .Invokes(c => + { + hash.AddOrUpdate(c.Method.Name, 1, (k, v) => v + 1); + if (Interlocked.Increment(ref i) == 3) + tcs.SetResult(i); + }); + A.CallTo(() => _subscriberAB.DerivedAsync(A.Ignored, "Hi")) + .Invokes(c => + { + hash.AddOrUpdate(c.Method.Name, 1, (k, v) => v + 1); + if (Interlocked.Increment(ref i) == 3) + tcs.SetResult(i); + }); + A.CallTo(() => _subscriberAB.AAsync(A.Ignored, 1)) + .Invokes(c => + { + hash.AddOrUpdate(c.Method.Name, 1, (k, v) => v + 1); + if (Interlocked.Increment(ref i) == 3) + tcs.SetResult(i); + }); + A.CallTo(() => _subscriberAB.BAsync(A.Ignored, A.Ignored)) + .Invokes(c => + { + hash.AddOrUpdate(c.Method.Name, 1, (k, v) => v + 1); + if (Interlocked.Increment(ref i) == 3) + tcs.SetResult(i); + }); #endregion // Prepare @@ -229,8 +244,7 @@ public async Task Inheritance_ConsumerCooperation_Succeed_Test() }) .WithCancellation(cancellation) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .Group("CONSUMER_GROUP_X_1"); await using IConsumerLifetime subscriptionA = @@ -252,9 +266,7 @@ public async Task Inheritance_ConsumerCooperation_Succeed_Test() #endregion // await using IConsumerLifetime subscription = ...Subscribe(...) - await subscriptionA.Completion; - await subscriptionB.Completion; - await subscriptionAB.Completion; + await tcs.Task.WithCancellation(new CancellationTokenSource(TimeSpan.FromSeconds(20)).Token); #region Validation @@ -263,7 +275,6 @@ public async Task Inheritance_ConsumerCooperation_Succeed_Test() Assert.True(hash.All(m => m.Value == 1)); #endregion // Validation - } #endregion // Inheritance_ConsumerCooperation_Succeed_Test @@ -282,75 +293,5 @@ private static CancellationToken GetCancellationToken(int duration = 10) } #endregion // GetCancellationToken - - #region Dispose pattern - - - ~InheritanceTests() - { - Dispose(); - } - - public void Dispose() - { - GC.SuppressFinalize(this); - try - { - IConnectionMultiplexer conn = RedisClientFactory.CreateProviderBlocking( - cfg => cfg.AllowAdmin = true); - string serverName = Environment.GetEnvironmentVariable(END_POINT_KEY) ?? "localhost:6379"; - var server = conn.GetServer(serverName); - IEnumerable keys = server.Keys(pattern: $"*{PARTITION}*"); - IDatabaseAsync db = conn.GetDatabase(); - - var ab = new ActionBlock(k => LocalAsync(k), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 30 }); -#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. -#pragma warning disable CS8604 // Possible null reference argument. - foreach (string key in keys) - { - ab.Post(key); - } -#pragma warning restore CS8604 // Possible null reference argument. -#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. - - ab.Complete(); - ab.Completion.Wait(); - - async Task LocalAsync(string k) - { - try - { - await db.KeyDeleteAsync(k, CommandFlags.DemandMaster); - _outputHelper.WriteLine($"Cleanup: delete key [{k}]"); - } - #region Exception Handling - - catch (RedisTimeoutException ex) - { - _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); - } - catch (Exception ex) - { - _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); - } - - #endregion // Exception Handling - } - } - #region Exception Handling - - catch (RedisTimeoutException ex) - { - _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); - } - catch (Exception ex) - { - _outputHelper.WriteLine($"Test dispose error (delete keys) {ex.FormatLazy()}"); - } - - #endregion // Exception Handling - } - - #endregion // Dispose pattern } } diff --git a/Tests/EventSourcing.Backbone.IntegrationTests/MigrationReceiverTest.cs b/Tests/EventSourcing.Backbone.IntegrationTests/MigrationReceiverTest.cs new file mode 100644 index 00000000..e49b2fe3 --- /dev/null +++ b/Tests/EventSourcing.Backbone.IntegrationTests/MigrationReceiverTest.cs @@ -0,0 +1,107 @@ +using System.Diagnostics; + +using EventSourcing.Backbone.Building; + +using FakeItEasy; + +using Microsoft.Extensions.Logging; + +using Xunit; +using Xunit.Abstractions; + +// docker run -p 6379:6379 -it --rm --name redis-evt-src redislabs/rejson:latest + +namespace EventSourcing.Backbone.Tests +{ + /// + /// The end to end tests. + /// + public class MigrationReceiverTest // : IDisposable + { + private const string TARGET_KEY = "REDIS_MIGRATION_TARGET_ENDPOINT"; + private const string SOURCE_KEY = "REDIS_EVENT_SOURCE_ENDPOINT"; + + private readonly ITestOutputHelper _outputHelper; + private readonly IProducerStoreStrategyBuilder _targetProducerBuilder; + private readonly IConsumerStoreStrategyBuilder _sourceConsumerBuilder; + private readonly string ENV = "Production"; + private readonly string URI = "analysts"; + private readonly ILogger _fakeLogger = A.Fake(); + private const int TIMEOUT = 1_000 * 300; + + #region Ctor + + /// + /// Initializes a new instance of the class. + /// + /// The output helper. + public MigrationReceiverTest(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + var credentials = new RedisCredentialsEnvKeys { Endpoint = TARGET_KEY }; + _targetProducerBuilder = credentials.CreateRedisProducerBuilder() + .AddVoidStrategy(); + + _sourceConsumerBuilder = ConsumerBuilder.Empty.UseRedisChannel(credentialsKeys: new RedisCredentialsEnvKeys { Endpoint = SOURCE_KEY }) + .AddS3Storage(new S3Options { Bucket = "evt-src-storage", EnvironmentConvension = S3EnvironmentConvention.BucketPrefix }); + } + + #endregion // Ctor + + + #region Migration_By_Receiver_Test + + [Fact(Timeout = TIMEOUT, Skip = "Use to migrate data between 2 different sources")] + //[Fact(Timeout = TIMEOUT)] + public async Task Migration_By_Receiver_Test() + { + #region IRawProducer rawProducer = ... + + IRawProducer rawProducer = _targetProducerBuilder + .Environment(ENV) + .Uri(URI) + .WithLogger(_fakeLogger) + .BuildRaw(new RawProducerOptions { KeepOriginalMeta = true }); + + #endregion // IRawProducer rawProducer = ... + + + CancellationToken cancellation = GetCancellationToken(); + + IAsyncEnumerable announcements = _sourceConsumerBuilder + .WithCancellation(cancellation) + .Environment(ENV) + .Uri(URI) + .WithLogger(_fakeLogger) + .BuildIterator() + .GetAsyncEnumerable(new ConsumerAsyncEnumerableOptions { ExitWhenEmpty = true }); + int count = 0; + await foreach (var announcement in announcements.WithCancellation(cancellation)) + { + _fakeLogger.LogInformation(announcement.ToString()); + await rawProducer.Produce(announcement); + count++; + _outputHelper.WriteLine($"{count} events processed"); + } + _outputHelper.WriteLine($"Total events = {count}"); + } + + #endregion // Migration_By_Receiver_Test + + + #region GetCancellationToken + + /// + /// Gets the cancellation token. + /// + /// + private static CancellationToken GetCancellationToken() + { + return new CancellationTokenSource(Debugger.IsAttached + ? TimeSpan.FromMinutes(10) + : TimeSpan.FromMinutes(5)).Token; + } + + #endregion // GetCancellationToken + } +} diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/MigrationTest.cs b/Tests/EventSourcing.Backbone.IntegrationTests/MigrationTest.cs similarity index 62% rename from Tests/Weknow.EventSource.Backbone.IntegrationTests/MigrationTest.cs rename to Tests/EventSourcing.Backbone.IntegrationTests/MigrationTest.cs index ad647a07..5549ed64 100644 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/MigrationTest.cs +++ b/Tests/EventSourcing.Backbone.IntegrationTests/MigrationTest.cs @@ -1,42 +1,34 @@ using System.Diagnostics; -using System.Threading.Tasks.Dataflow; + +using EventSourcing.Backbone.Building; +using EventSourcing.Backbone.Channels.RedisProvider; +using EventSourcing.Backbone.Enums; +using EventSourcing.Backbone.UnitTests.Entities; using FakeItEasy; using Microsoft.Extensions.Logging; -using StackExchange.Redis; - -using Weknow.EventSource.Backbone.Building; -using Weknow.EventSource.Backbone.Enums; -using Weknow.EventSource.Backbone.UnitTests.Entities; - using Xunit; using Xunit.Abstractions; -using static Weknow.EventSource.Backbone.Channels.RedisProvider.Common.RedisChannelConstants; - -// docker run -p 6379:6379 -it --rm --name redis-event-source redislabs/rejson:latest +// docker run -p 6379:6379 -it --rm --name redis-evt-src redislabs/rejson:latest -namespace Weknow.EventSource.Backbone.Tests +namespace EventSourcing.Backbone.Tests { /// /// The end to end tests. /// - public class MigrationTest : IDisposable + public class MigrationTest : TestsBase { - private readonly ITestOutputHelper _outputHelper; private readonly ISequenceOperationsConsumer _subscriber = A.Fake(); private readonly SequenceOperationsConsumerBridge _subscriberBridge; private readonly IProducerStoreStrategyBuilder _producerBuilder; private readonly IConsumerStoreStrategyBuilder _consumerBuilder; private readonly string ENV = $"Development"; - private readonly string PARTITION = $"{DateTime.UtcNow:yyyy-MM-dd HH_mm_ss}:{Guid.NewGuid():N}"; - private readonly string SHARD = $"some-shard-{DateTime.UtcNow.Second}"; + protected override string URI { get; } = $"{DateTime.UtcNow:yyyy-MM-dd HH_mm_ss}:{Guid.NewGuid():N}"; - private readonly ILogger _fakeLogger = A.Fake(); private static readonly User USER = new User { Eracure = new Personal { Name = "mike", GovernmentId = "A25" }, Comment = "Do it" }; - private const int TIMEOUT = 1_000 * 30; #region Ctor @@ -45,32 +37,30 @@ public class MigrationTest : IDisposable /// /// The output helper. public MigrationTest( - ITestOutputHelper outputHelper) + ITestOutputHelper outputHelper) : base(outputHelper) { - _outputHelper = outputHelper; _producerBuilder = ProducerBuilder.Empty.UseRedisChannel( /*, configuration: (cfg) => cfg.ServiceName = "mymaster" */); - _consumerBuilder = ConsumerBuilder.Empty.UseRedisChannel( - stg => stg with - { - DelayWhenEmptyBehavior = - stg.DelayWhenEmptyBehavior with - { - CalcNextDelay = (d => TimeSpan.FromMilliseconds(2)) - } - }); - - A.CallTo(() => _subscriber.RegisterAsync(A.Ignored)) + var stg = new RedisConsumerChannelSetting + { + DelayWhenEmptyBehavior = new DelayWhenEmptyBehavior + { + CalcNextDelay = ((d, _) => TimeSpan.FromMilliseconds(2)) + } + }; + _consumerBuilder = stg.CreateRedisConsumerBuilder(); + + A.CallTo(() => _subscriber.RegisterAsync(A.Ignored, A.Ignored)) .ReturnsLazily(() => { Metadata meta = ConsumerMetadata.Context; if (string.IsNullOrEmpty(meta.EventKey)) - return ValueTask.FromException(new Exception("Event Key is missing")); + return ValueTask.FromException(new EventSourcingException("Event Key is missing")); return ValueTask.CompletedTask; }); - A.CallTo(() => _subscriber.LoginAsync(A.Ignored, A.Ignored)) + A.CallTo(() => _subscriber.LoginAsync(A.Ignored, A.Ignored, A.Ignored)) .ReturnsLazily(() => Delay()); - A.CallTo(() => _subscriber.EarseAsync(A.Ignored)) + A.CallTo(() => _subscriber.EarseAsync(A.Ignored, A.Ignored)) .ReturnsLazily(() => Delay()); #region A.CallTo(() => _fakeLogger...) @@ -127,8 +117,7 @@ public async Task Migration_Test() ISequenceOperationsProducer producer = _producerBuilder //.WithOptions(producerOption) .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger) .BuildSequenceOperationsProducer(); @@ -147,8 +136,7 @@ public async Task Migration_Test() var consumerBuilder = _consumerBuilder .WithOptions(o => DefaultOptions(o, 3, AckBehavior.OnSucceed)) .WithCancellation(cancellation) - .Partition(PARTITION) - .Shard(SHARD) + .Uri(URI) .WithLogger(_fakeLogger); #endregion // var consumerBuilder = _consumerBuilder... @@ -181,8 +169,9 @@ public async Task Migration_Test() IRawProducer fwProducer = _producerBuilder .Environment(MIGRATE_TO) .BuildRaw(); + // attach the producer into a subscription bridge - SubscriptionBridge fwSubscriberBridge = new(fwProducer); + ISubscriptionBridge fwSubscriberBridge = fwProducer.ToSubscriptionBridge(); // attach the forward subscription into a concrete stream await using IConsumerLifetime fwSubscription = consumerBuilder @@ -203,14 +192,14 @@ public async Task Migration_Test() #region Validation - A.CallTo(() => _subscriber.RegisterAsync(A.Ignored)) + A.CallTo(() => _subscriber.RegisterAsync(A.Ignored, A.Ignored)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.LoginAsync("admin", "1234")) + A.CallTo(() => _subscriber.LoginAsync(A.Ignored, "admin", "1234")) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.EarseAsync(4335)) + A.CallTo(() => _subscriber.EarseAsync(A.Ignored, 4335)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => emptySubscriber.RegisterAsync(A.Ignored)) + A.CallTo(() => emptySubscriber.RegisterAsync(A.Ignored, A.Ignored)) .MustNotHaveHappened(); #endregion // Validation @@ -258,96 +247,5 @@ private static CancellationToken GetCancellationToken() } #endregion // GetCancellationToken - - #region Dispose pattern - - - ~MigrationTest() - { - Dispose(); - } - - public void Dispose() - { - GC.SuppressFinalize(this); - try - { - IConnectionMultiplexer conn = RedisClientFactory.CreateProviderBlocking( - cfg => cfg.AllowAdmin = true); - string serverName = Environment.GetEnvironmentVariable(END_POINT_KEY) ?? "localhost:6379"; - var server = conn.GetServer(serverName); - IEnumerable keys = server.Keys(pattern: $"*{PARTITION}*"); - IDatabaseAsync db = conn.GetDatabase(); - - var ab = new ActionBlock(k => LocalAsync(k), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 30 }); -#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. -#pragma warning disable CS8604 // Possible null reference argument. - foreach (string key in keys) - { - ab.Post(key); - } -#pragma warning restore CS8604 // Possible null reference argument. -#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. - - ab.Complete(); - ab.Completion.Wait(); - - async Task LocalAsync(string k) - { - try - { - await db.KeyDeleteAsync(k, CommandFlags.DemandMaster); - _outputHelper.WriteLine($"Cleanup: delete key [{k}]"); - } - #region Exception Handling - - catch (RedisTimeoutException ex) - { - _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); - } - catch (Exception ex) - { - _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); - } - - #endregion // Exception Handling - } - } - #region Exception Handling - - catch (RedisTimeoutException ex) - { - _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); - } - catch (Exception ex) - { - _outputHelper.WriteLine($"Test dispose error (delete keys) {ex.FormatLazy()}"); - } - - #endregion // Exception Handling - } - - #endregion // Dispose pattern - - #region SubscriptionBridge - - private class SubscriptionBridge : ISubscriptionBridge - { - private readonly IRawProducer _fw; - - public SubscriptionBridge(IRawProducer fw) - { - _fw = fw; - } - - public async Task BridgeAsync(Announcement announcement, IConsumerBridge consumerBridge) - { - await _fw.Produce(announcement); - return true; - - } - } - - #endregion // SubscriptionBridge } } diff --git a/Tests/EventSourcing.Backbone.IntegrationTests/S3StoreCredentialsTests.cs b/Tests/EventSourcing.Backbone.IntegrationTests/S3StoreCredentialsTests.cs new file mode 100644 index 00000000..93fb3921 --- /dev/null +++ b/Tests/EventSourcing.Backbone.IntegrationTests/S3StoreCredentialsTests.cs @@ -0,0 +1,148 @@ +using Amazon.S3; + +using EventSourcing.Backbone.Building; +using EventSourcing.Backbone.UnitTests.Entities; + +using FakeItEasy; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +using Xunit; +using Xunit.Abstractions; + +namespace EventSourcing.Backbone.Tests +{ + public sealed class S3StoreCredentialsTests : IDisposable + { + private const string TEST_URI = "testing"; + private readonly ITestOutputHelper _outputHelper; + private readonly IHost _host; + private const int TIMEOUT = 1000 * 20; + private readonly ISimpleConsumer _subscriber = A.Fake(); + + #region Ctor + + public S3StoreCredentialsTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "test"); + _host = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((context, builder) => + { + builder.SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); + }) + //.AddEnvironmentVariables() + .ConfigureServices((context, services) => + { + var s3Options = new S3Options { Bucket = "event-sourcing-validation" }; + + services.AddDefaultAWSOptions(context.Configuration.GetAWSOptions()); + services.AddAWSService(); + //IHostEnvironment environment = context.HostingEnvironment; + services.AddEventSourceRedisConnection(); + + services.AddSingleton(ioc => + { + var logger = ioc.GetService>(); + IProducerHooksBuilder producer = ioc.ResolveRedisProducerChannel() + .ResolveS3Storage(s3Options) + .WithLogger(logger!) + .Uri(TEST_URI); + + return producer; + }); + services.AddSingleton(ioc => + { + //var logger = ioc.GetService>(); + IConsumerReadyBuilder consumer = ioc.ResolveRedisConsumerChannel() + .ResolveS3Storage(s3Options) + .Uri(TEST_URI); + + return consumer; + }); + }) + .Build(); + } + + #endregion // Ctor + + #region S3_Cred + + [Fact(Timeout = TIMEOUT)] + //[Fact] + public async Task S3_Cred() + { + var producerBuilder = _host.Services.GetService() ?? throw new EventSourcingException("Producer is null"); + + ISimpleProducer producer = producerBuilder + .EnvironmentFromVariable() + .BuildSimpleProducer(); + + var id1 = producer.Step1Async(1); + var id2 = producer.Step2Async(2); + + IConsumerReadyBuilder consumerBuilder = _host.Services.GetService() ?? throw new EventSourcingException("Producer is null"); + IConsumerLifetime consumer = consumerBuilder + .EnvironmentFromVariable() + .WithOptions(opt => opt with { MaxMessages = 2 }) + .SubscribeSimpleConsumer(_subscriber); + + await consumer.Completion; + + A.CallTo(() => _subscriber.Step1Async(A.Ignored, 1)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _subscriber.Step2Async(A.Ignored, 2)) + .MustHaveHappenedOnceExactly(); + } + + #endregion // S3_Cred + + #region S3_Cred_No_Env_Test + + //[Fact(Timeout = TIMEOUT)] + [Fact] + public async Task S3_Cred_No_Env_Test() + { + var producerBuilder = _host.Services.GetService() ?? throw new EventSourcingException("Producer is null"); + + ISimpleProducer producer = producerBuilder + .BuildSimpleProducer(); + + var id1 = producer.Step1Async(1); + var id2 = producer.Step2Async(2); + + IConsumerReadyBuilder consumerBuilder = _host.Services.GetService() ?? throw new EventSourcingException("Producer is null"); + IConsumerLifetime consumer = consumerBuilder + .WithOptions(opt => opt with { MaxMessages = 2 }) + .SubscribeSimpleConsumer(_subscriber); + + await consumer.Completion; + + A.CallTo(() => _subscriber.Step1Async(A.Ignored, 1)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _subscriber.Step2Async(A.Ignored, 2)) + .MustHaveHappenedOnceExactly(); + } + + #endregion // S3_Cred_No_Env_Test + + + #region Dispose Pattern + + public void Dispose() + { + _host.Dispose(); + GC.SuppressFinalize(this); + } + + ~S3StoreCredentialsTests() + { + Dispose(); + } + + #endregion // Dispose Pattern + } +} diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/S3StoreStrategyExplicitTests.cs b/Tests/EventSourcing.Backbone.IntegrationTests/S3StoreStrategyExplicitTests.cs similarity index 62% rename from Tests/Weknow.EventSource.Backbone.IntegrationTests/S3StoreStrategyExplicitTests.cs rename to Tests/EventSourcing.Backbone.IntegrationTests/S3StoreStrategyExplicitTests.cs index 26e52cb4..b8ec7a04 100644 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/S3StoreStrategyExplicitTests.cs +++ b/Tests/EventSourcing.Backbone.IntegrationTests/S3StoreStrategyExplicitTests.cs @@ -1,14 +1,14 @@ using Xunit.Abstractions; -namespace Weknow.EventSource.Backbone.Tests +namespace EventSourcing.Backbone.Tests { public class S3StoreStrategyExplicitTests : EndToEndExplicitTests { public S3StoreStrategyExplicitTests(ITestOutputHelper outputHelper) : base(outputHelper, - (b, logger) => b.AddS3Strategy(), - (b, logger) => b.AddS3Strategy()) + (b, logger) => b.AddS3Storage(), + (b, logger) => b.AddS3Storage()) { } diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/S3StoreStrategyStressTests .cs b/Tests/EventSourcing.Backbone.IntegrationTests/S3StoreStrategyStressTests .cs similarity index 72% rename from Tests/Weknow.EventSource.Backbone.IntegrationTests/S3StoreStrategyStressTests .cs rename to Tests/EventSourcing.Backbone.IntegrationTests/S3StoreStrategyStressTests .cs index db27a135..952c9f4e 100644 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/S3StoreStrategyStressTests .cs +++ b/Tests/EventSourcing.Backbone.IntegrationTests/S3StoreStrategyStressTests .cs @@ -1,6 +1,6 @@ using Xunit.Abstractions; -namespace Weknow.EventSource.Backbone.Tests +namespace EventSourcing.Backbone.Tests { public class S3StoreStrategyStressTests : EndToEndStressTests { @@ -12,10 +12,9 @@ public class S3StoreStrategyStressTests : EndToEndStressTests public S3StoreStrategyStressTests(ITestOutputHelper outputHelper) : base(outputHelper, - (b, logger) => b.AddS3Strategy(OPTIONS), - (b, logger) => b.AddS3Strategy(OPTIONS)) + (b, logger) => b.AddS3Storage(OPTIONS), + (b, logger) => b.AddS3Storage(OPTIONS)) { } - } } diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/S3StoreStrategyTests.cs b/Tests/EventSourcing.Backbone.IntegrationTests/S3StoreStrategyTests.cs similarity index 71% rename from Tests/Weknow.EventSource.Backbone.IntegrationTests/S3StoreStrategyTests.cs rename to Tests/EventSourcing.Backbone.IntegrationTests/S3StoreStrategyTests.cs index d173ba11..05294c89 100644 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/S3StoreStrategyTests.cs +++ b/Tests/EventSourcing.Backbone.IntegrationTests/S3StoreStrategyTests.cs @@ -1,6 +1,6 @@ using Xunit.Abstractions; -namespace Weknow.EventSource.Backbone.Tests +namespace EventSourcing.Backbone.Tests { public class S3StoreStrategyTests : EndToEndTests { @@ -12,8 +12,8 @@ public class S3StoreStrategyTests : EndToEndTests public S3StoreStrategyTests(ITestOutputHelper outputHelper) : base(outputHelper, - (b, logger) => b.AddS3Strategy(OPTIONS), - (b, logger) => b.AddS3Strategy(OPTIONS)) + (b, logger) => b.AddS3Storage(OPTIONS), + (b, logger) => b.AddS3Storage(OPTIONS)) { } diff --git a/Tests/EventSourcing.Backbone.IntegrationTests/TestsBase.cs b/Tests/EventSourcing.Backbone.IntegrationTests/TestsBase.cs new file mode 100644 index 00000000..ea261fa5 --- /dev/null +++ b/Tests/EventSourcing.Backbone.IntegrationTests/TestsBase.cs @@ -0,0 +1,124 @@ +using System.Threading.Tasks.Dataflow; + +using EventSourcing.Backbone.IntegrationTests.HelloWorld; + +using FakeItEasy; + +using Microsoft.Extensions.Logging; + +using StackExchange.Redis; + +using Xunit.Abstractions; + +using static EventSourcing.Backbone.Channels.RedisProvider.Common.RedisChannelConstants; + +#pragma warning disable S3881 // "IDisposable" should be implemented correctly + +// docker run -p 6379:6379 -it --rm --name redis-evt-src redislabs/rejson:latest + +namespace EventSourcing.Backbone.Tests +{ + /// + /// The end to end tests. + /// + public abstract class TestsBase : IDisposable + { + protected readonly ITestOutputHelper _outputHelper; + protected readonly ILogger _fakeLogger = A.Fake(); + protected const int TIMEOUT = 1_000 * 50; + + protected abstract string URI { get; } + + #region Ctor + + /// + /// Initializes a new instance of the class. + /// + /// The output helper. + protected TestsBase(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + } + + #endregion // Ctor + + #region Dispose pattern + + + ~TestsBase() + { + Dispose(); + } + + public void Dispose() + { + try + { + IConnectionMultiplexer conn = RedisClientFactory.CreateProviderAsync( + logger: _fakeLogger, + configurationHook: cfg => cfg.AllowAdmin = true).Result; + string serverNames = Environment.GetEnvironmentVariable(END_POINT_KEY) ?? "localhost:6379"; + foreach (var serverName in serverNames.Split(',')) + { + var server = conn.GetServer(serverName); + if (!server.IsConnected) + continue; + IEnumerable keys = server.Keys(pattern: $"*{URI}*"); + IDatabaseAsync db = conn.GetDatabase(); + + var ab = new ActionBlock(k => LocalAsync(k), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 30 }); +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. +#pragma warning disable CS8604 // Possible null reference argument. + foreach (string key in keys) + { + ab.Post(key); + } +#pragma warning restore CS8604 // Possible null reference argument. +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. + + ab.Complete(); + ab.Completion.Wait(); + + async Task LocalAsync(string k) + { + try + { + await db.KeyDeleteAsync(k, CommandFlags.DemandMaster); + _outputHelper.WriteLine($"Cleanup: delete key [{k}]"); + } + #region Exception Handling + + catch (RedisTimeoutException ex) + { + _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); + } + catch (Exception ex) + { + _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); + } + + #endregion // Exception Handling + } + } + } + #region Exception Handling + + catch (RedisTimeoutException ex) + { + _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); + } + catch (Exception ex) + { + _outputHelper.WriteLine($"Test dispose error (delete keys) {ex.FormatLazy()}"); + } + + #endregion // Exception Handling + finally + { + GC.SuppressFinalize(this); + } + } + + #endregion // Dispose pattern + } +} diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/appsettings.json b/Tests/EventSourcing.Backbone.IntegrationTests/appsettings.json similarity index 63% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/appsettings.json rename to Tests/EventSourcing.Backbone.IntegrationTests/appsettings.json index d9d9a9bf..b689c6e4 100644 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/appsettings.json +++ b/Tests/EventSourcing.Backbone.IntegrationTests/appsettings.json @@ -6,5 +6,9 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "AWS": { + "Region": "us-east-1", + "Profile": "playground" + } } diff --git a/Tests/EventSourcing.Backbone.IntegrationTests/icon.png b/Tests/EventSourcing.Backbone.IntegrationTests/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/Tests/EventSourcing.Backbone.IntegrationTests/icon.png differ diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/payload.json b/Tests/EventSourcing.Backbone.IntegrationTests/payload.json similarity index 100% rename from Tests/Weknow.EventSource.Backbone.IntegrationTests/payload.json rename to Tests/EventSourcing.Backbone.IntegrationTests/payload.json diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/person.json b/Tests/EventSourcing.Backbone.IntegrationTests/person.json similarity index 100% rename from Tests/Weknow.EventSource.Backbone.IntegrationTests/person.json rename to Tests/EventSourcing.Backbone.IntegrationTests/person.json diff --git a/Tests/EventSourcing.Backbone.IntegrationTests/xunit.runner.json b/Tests/EventSourcing.Backbone.IntegrationTests/xunit.runner.json new file mode 100644 index 00000000..f5ac9bed --- /dev/null +++ b/Tests/EventSourcing.Backbone.IntegrationTests/xunit.runner.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "maxParallelThreads": 20, + "diagnosticMessages": true, + "longRunningTestSeconds": 30 +} diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/.editorconfig b/Tests/EventSourcing.Backbone.UnitTests/.editorconfig similarity index 100% rename from Tests/Weknow.EventSource.Backbone.UnitTests/.editorconfig rename to Tests/EventSourcing.Backbone.UnitTests/.editorconfig diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/ApiDesign/EventSourceApiConsumerDesignTests.cs b/Tests/EventSourcing.Backbone.UnitTests/ApiDesign/EventSourceApiConsumerDesignTests.cs similarity index 93% rename from Tests/Weknow.EventSource.Backbone.UnitTests/ApiDesign/EventSourceApiConsumerDesignTests.cs rename to Tests/EventSourcing.Backbone.UnitTests/ApiDesign/EventSourceApiConsumerDesignTests.cs index 5fc6ba5a..58e37df2 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/ApiDesign/EventSourceApiConsumerDesignTests.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/ApiDesign/EventSourceApiConsumerDesignTests.cs @@ -1,13 +1,13 @@ +using EventSourcing.Backbone.UnitTests.Entities; + using FakeItEasy; using Microsoft.Extensions.Logging; -using Weknow.EventSource.Backbone.UnitTests.Entities; - using Xunit; using Xunit.Abstractions; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public class EventSourceConsumerApiDesignTests { @@ -37,7 +37,7 @@ public void Build_Raw_Consumer_Direct_Test() .RegisterInterceptor(_rawAsyncInterceptor) .RegisterInterceptor(_rawInterceptor) .RegisterSegmentationStrategy(_segmentation) - .Partition("ORDERS") + .Uri("ORDERS") // .Shard("ORDER-AHS7821X") .WithLogger(_logger) .SubscribeSequenceOperationsConsumer(_subscriber); diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/ApiDesign/EventSourceApiProducerDesignTests.cs b/Tests/EventSourcing.Backbone.UnitTests/ApiDesign/EventSourceApiProducerDesignTests.cs similarity index 86% rename from Tests/Weknow.EventSource.Backbone.UnitTests/ApiDesign/EventSourceApiProducerDesignTests.cs rename to Tests/EventSourcing.Backbone.UnitTests/ApiDesign/EventSourceApiProducerDesignTests.cs index 15b100c3..0df0e206 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/ApiDesign/EventSourceApiProducerDesignTests.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/ApiDesign/EventSourceApiProducerDesignTests.cs @@ -1,16 +1,16 @@ +using EventSourcing.Backbone.Building; +using EventSourcing.Backbone.UnitTests.Entities; + using FakeItEasy; using Microsoft.Extensions.Logging; -using Weknow.EventSource.Backbone.Building; -using Weknow.EventSource.Backbone.UnitTests.Entities; - using Xunit; using Xunit.Abstractions; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public class EventSourceApiProducerDesignTests { @@ -41,20 +41,17 @@ public async Task Build_API_Merge_Producer_Test() { var producerA = _builder.UseChannel(_channel) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids:HappySocks") .AddInterceptor(_rawAsyncInterceptor); var producerB = _builder.UseTestProducerChannel1() - .Partition("NGOs") - .Shard("NGO #2782228") + .Uri("NGOs:NGO #2782228") .UseSegmentation(_segmentationStrategy); var producerC = _builder.UseChannel(_channel) - .Partition("Fans") - .Shard("Geek: @someone") + .Uri("Fans:Geek: @someone") .UseSegmentation(_otherSegmentationStrategy); @@ -82,8 +79,7 @@ public async Task Build_API_Serializer_Producer_Test() ISequenceOperationsProducer producer = _builder.UseChannel(_channel) .WithOptions(option) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids:HappySocks") .WithLogger(_logger) .BuildSequenceOperationsProducer(); @@ -101,8 +97,7 @@ public async Task Build_API_Interceptor_Producer_Test() { ISequenceOperationsProducer producer = _builder.UseChannel(_channel) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids:HappySocks") .AddInterceptor(_rawInterceptor) .AddInterceptor(_rawAsyncInterceptor) .UseSegmentation(_segmentationStrategy) diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/ApiDesign/TestApiExtensions.cs b/Tests/EventSourcing.Backbone.UnitTests/ApiDesign/TestApiExtensions.cs similarity index 96% rename from Tests/Weknow.EventSource.Backbone.UnitTests/ApiDesign/TestApiExtensions.cs rename to Tests/EventSourcing.Backbone.UnitTests/ApiDesign/TestApiExtensions.cs index 2b2d55aa..3778a245 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/ApiDesign/TestApiExtensions.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/ApiDesign/TestApiExtensions.cs @@ -1,12 +1,12 @@ using System.Collections.Immutable; +using EventSourcing.Backbone.Building; + using FakeItEasy; using Microsoft.Extensions.Logging; -using Weknow.EventSource.Backbone.Building; - -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public static class TestApiExtensions { diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/Channels/ConsumerTestChannel.cs b/Tests/EventSourcing.Backbone.UnitTests/Channels/ConsumerTestChannel.cs similarity index 96% rename from Tests/Weknow.EventSource.Backbone.UnitTests/Channels/ConsumerTestChannel.cs rename to Tests/EventSourcing.Backbone.UnitTests/Channels/ConsumerTestChannel.cs index 203a0b40..24b27af2 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/Channels/ConsumerTestChannel.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/Channels/ConsumerTestChannel.cs @@ -1,6 +1,6 @@ using System.Threading.Channels; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public class ConsumerTestChannel : IConsumerChannelProvider @@ -31,7 +31,7 @@ public ConsumerTestChannel(Channel channel) /// /// When completed /// - public async ValueTask SubsribeAsync( + public async ValueTask SubscribeAsync( IConsumerPlan plan, Func> func, CancellationToken cancellationToken) diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/Channels/ProducerTestChannel.cs b/Tests/EventSourcing.Backbone.UnitTests/Channels/ProducerTestChannel.cs similarity index 92% rename from Tests/Weknow.EventSource.Backbone.UnitTests/Channels/ProducerTestChannel.cs rename to Tests/EventSourcing.Backbone.UnitTests/Channels/ProducerTestChannel.cs index a1c37401..fdcb4230 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/Channels/ProducerTestChannel.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/Channels/ProducerTestChannel.cs @@ -1,13 +1,13 @@ using System.Collections.Immutable; using System.Threading.Channels; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// In-Memory Channel (excellent for testing) /// - /// + /// public class ProducerTestChannel : IProducerChannelProvider { diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/ConsumerBuilderTests.cs b/Tests/EventSourcing.Backbone.UnitTests/ConsumerBuilderTests.cs similarity index 93% rename from Tests/Weknow.EventSource.Backbone.UnitTests/ConsumerBuilderTests.cs rename to Tests/EventSourcing.Backbone.UnitTests/ConsumerBuilderTests.cs index 4ca99a9b..fba7afea 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/ConsumerBuilderTests.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/ConsumerBuilderTests.cs @@ -1,13 +1,13 @@ +using EventSourcing.Backbone.UnitTests.Entities; + using FakeItEasy; using Microsoft.Extensions.Logging; -using Weknow.EventSource.Backbone.UnitTests.Entities; - using Xunit; using Xunit.Abstractions; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public class ConsumerBuilderTests { @@ -39,7 +39,7 @@ public void Build_Raw_Consumer_Direct_Test() .RegisterInterceptor(_rawAsyncInterceptor) .RegisterInterceptor(_rawInterceptor) .RegisterSegmentationStrategy(_segmentation) - .Partition("ORDERS") + .Uri("ORDERS") // .Shard("ORDER-AHS7821X") .SubscribeSequenceOperationsConsumer(_subscriber); diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/Contracts/ISequenceOperations.cs b/Tests/EventSourcing.Backbone.UnitTests/Contracts/ISequenceOperations.cs similarity index 52% rename from Tests/Weknow.EventSource.Backbone.UnitTests/Contracts/ISequenceOperations.cs rename to Tests/EventSourcing.Backbone.UnitTests/Contracts/ISequenceOperations.cs index 25f8c915..d20fdb39 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/Contracts/ISequenceOperations.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/Contracts/ISequenceOperations.cs @@ -1,10 +1,10 @@ -namespace Weknow.EventSource.Backbone.UnitTests.Entities +namespace EventSourcing.Backbone.UnitTests.Entities { - [GenerateEventSource(EventSourceGenType.Producer, Name = "ISequenceOfProducer")] - [GenerateEventSource(EventSourceGenType.Consumer, Name = "ISequenceOfConsumer")] - [GenerateEventSource(EventSourceGenType.Producer)] - [GenerateEventSource(EventSourceGenType.Consumer)] - //[GenerateEventSourceBridge(EventSourceGenType.Producer, Name = "ISequenceOfProducer")] + [EventsContract(EventsContractType.Producer, Name = "ISequenceOfProducer")] + [EventsContract(EventsContractType.Consumer, Name = "ISequenceOfConsumer")] + [EventsContract(EventsContractType.Producer)] + [EventsContract(EventsContractType.Consumer)] + //[EventsContractBridge(EventSourceGenType.Producer, Name = "ISequenceOfProducer")] [Obsolete("Use ISequenceOfProducer or ISequenceOfConsumer", true)] public interface ISequenceOperations { diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/Contracts/ISimpleEvent.cs b/Tests/EventSourcing.Backbone.UnitTests/Contracts/ISimpleEvent.cs similarity index 75% rename from Tests/Weknow.EventSource.Backbone.UnitTests/Contracts/ISimpleEvent.cs rename to Tests/EventSourcing.Backbone.UnitTests/Contracts/ISimpleEvent.cs index 2b3b0244..37bd3271 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/Contracts/ISimpleEvent.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/Contracts/ISimpleEvent.cs @@ -1,15 +1,12 @@ -using System.ComponentModel; - -namespace Weknow.EventSource.Backbone.UnitTests.Entities +namespace EventSourcing.Backbone.UnitTests.Entities { /// /// Test contract /// - [GenerateEventSource(EventSourceGenType.Producer)] - [GenerateEventSource(EventSourceGenType.Consumer)] + [EventsContract(EventsContractType.Producer)] + [EventsContract(EventsContractType.Consumer)] [Obsolete("This interface is base for code generation, please use ISimpleEventProducer or ISimpleEventConsumer", true)] - [EditorBrowsable(EditorBrowsableState.Never)] public interface ISimpleEvent { /// diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/Contracts/ISimpleEventTag.cs b/Tests/EventSourcing.Backbone.UnitTests/Contracts/ISimpleEventTag.cs similarity index 66% rename from Tests/Weknow.EventSource.Backbone.UnitTests/Contracts/ISimpleEventTag.cs rename to Tests/EventSourcing.Backbone.UnitTests/Contracts/ISimpleEventTag.cs index ea5a9851..0738c01e 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/Contracts/ISimpleEventTag.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/Contracts/ISimpleEventTag.cs @@ -1,12 +1,12 @@ using System.ComponentModel; -namespace Weknow.EventSource.Backbone.UnitTests.Entities +namespace EventSourcing.Backbone.UnitTests.Entities { /// /// Test contract /// - [GenerateEventSource(EventSourceGenType.Producer)] - [GenerateEventSource(EventSourceGenType.Consumer)] + [EventsContract(EventsContractType.Producer)] + [EventsContract(EventsContractType.Consumer)] [Obsolete("This interface is base for code generation, please use ISimpleEventProducer or ISimpleEventConsumer", true)] [EditorBrowsable(EditorBrowsableState.Never)] public interface ISimpleEventTag : ISimpleEvent diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/EndToEndTests.cs b/Tests/EventSourcing.Backbone.UnitTests/EndToEndTests.cs similarity index 80% rename from Tests/Weknow.EventSource.Backbone.UnitTests/EndToEndTests.cs rename to Tests/EventSourcing.Backbone.UnitTests/EndToEndTests.cs index 898ca3e8..2d3a37b2 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/EndToEndTests.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/EndToEndTests.cs @@ -1,18 +1,18 @@ using System.Threading.Channels; +using EventSourcing.Backbone.Building; +using EventSourcing.Backbone.UnitTests.Entities; + using FakeItEasy; using Microsoft.Extensions.Logging; -using Weknow.EventSource.Backbone.Building; -using Weknow.EventSource.Backbone.UnitTests.Entities; - using Xunit; using Xunit.Abstractions; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public class EndToEndTests { @@ -63,8 +63,7 @@ public async Task End2End_CustomBaseSubscription_Test() ISimpleEventProducer producer = _producerBuilder.UseChannel(_producerChannel) //.WithOptions(producerOption) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids#HappySocks") .BuildSimpleEventProducer(); await producer.ExecuteAsync("Id", 1); @@ -78,19 +77,18 @@ public async Task End2End_CustomBaseSubscription_Test() _consumerBuilder.UseChannel(_consumerChannel) //.WithOptions(consumerOptions) .WithCancellation(cts.Token) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids#HappySocks") .SubscribeSimpleEvent(_simpleEventConsumer); ch.Writer.Complete(); await subscription.DisposeAsync(); await ch.Reader.Completion; - A.CallTo(() => _simpleEventConsumer.ExecuteAsync("Id", 1)) + A.CallTo(() => _simpleEventConsumer.ExecuteAsync(A.Ignored, "Id", 1)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _simpleEventConsumer.RunAsync(1, A.Ignored)) + A.CallTo(() => _simpleEventConsumer.RunAsync(A.Ignored, 1, A.Ignored)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _simpleEventConsumer.RunAsync(2, A.Ignored)) + A.CallTo(() => _simpleEventConsumer.RunAsync(A.Ignored, 2, A.Ignored)) .MustHaveHappenedOnceExactly(); } @@ -106,8 +104,7 @@ public async Task End2End_CustomSubscriptionBridge_Test() ISimpleEventProducer producer = _producerBuilder.UseChannel(_producerChannel) //.WithOptions(producerOption) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids#HappySocks") .BuildSimpleEventProducer(); await producer.ExecuteAsync("Id", 1); @@ -121,19 +118,18 @@ public async Task End2End_CustomSubscriptionBridge_Test() _consumerBuilder.UseChannel(_consumerChannel) //.WithOptions(consumerOptions) .WithCancellation(cts.Token) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids#HappySocks") .Subscribe(_simpleBridgeSubscription.BridgeAsync); ch.Writer.Complete(); await subscription.DisposeAsync(); await ch.Reader.Completion; - A.CallTo(() => _simpleEventConsumer.ExecuteAsync("Id", 1)) + A.CallTo(() => _simpleEventConsumer.ExecuteAsync(A.Ignored, "Id", 1)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _simpleEventConsumer.RunAsync(1, A.Ignored)) + A.CallTo(() => _simpleEventConsumer.RunAsync(A.Ignored, 1, A.Ignored)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _simpleEventConsumer.RunAsync(2, A.Ignored)) + A.CallTo(() => _simpleEventConsumer.RunAsync(A.Ignored, 2, A.Ignored)) .MustHaveHappenedOnceExactly(); } @@ -149,8 +145,7 @@ public async Task End2End_GenBaseSubscription_Test() ISimpleEventProducer producer = _producerBuilder.UseChannel(_producerChannel) //.WithOptions(producerOption) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids#HappySocks") .BuildSimpleEventProducer(); await producer.ExecuteAsync("Id", 1); @@ -164,19 +159,18 @@ public async Task End2End_GenBaseSubscription_Test() _consumerBuilder.UseChannel(_consumerChannel) //.WithOptions(consumerOptions) .WithCancellation(cts.Token) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids#HappySocks") .Subscribe(_simpleGenSubscription); ch.Writer.Complete(); await subscription.DisposeAsync(); await ch.Reader.Completion; - A.CallTo(() => _simpleEventConsumer.ExecuteAsync("Id", 1)) + A.CallTo(() => _simpleEventConsumer.ExecuteAsync(A.Ignored, "Id", 1)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _simpleEventConsumer.RunAsync(1, A.Ignored)) + A.CallTo(() => _simpleEventConsumer.RunAsync(A.Ignored, 1, A.Ignored)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _simpleEventConsumer.RunAsync(2, A.Ignored)) + A.CallTo(() => _simpleEventConsumer.RunAsync(A.Ignored, 2, A.Ignored)) .MustHaveHappenedOnceExactly(); } @@ -192,8 +186,7 @@ public async Task End2End_GenSubscriptionBridge_Test() ISimpleEventProducer producer = _producerBuilder.UseChannel(_producerChannel) //.WithOptions(producerOption) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids#HappySocks") .BuildSimpleEventProducer(); await producer.ExecuteAsync("Id", 1); @@ -207,19 +200,18 @@ public async Task End2End_GenSubscriptionBridge_Test() _consumerBuilder.UseChannel(_consumerChannel) //.WithOptions(consumerOptions) .WithCancellation(cts.Token) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids#HappySocks") .Subscribe(_simpleGenBridgeSubscription.BridgeAsync); ch.Writer.Complete(); await subscription.DisposeAsync(); await ch.Reader.Completion; - A.CallTo(() => _simpleEventConsumer.ExecuteAsync("Id", 1)) + A.CallTo(() => _simpleEventConsumer.ExecuteAsync(A.Ignored, "Id", 1)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _simpleEventConsumer.RunAsync(1, A.Ignored)) + A.CallTo(() => _simpleEventConsumer.RunAsync(A.Ignored, 1, A.Ignored)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _simpleEventConsumer.RunAsync(2, A.Ignored)) + A.CallTo(() => _simpleEventConsumer.RunAsync(A.Ignored, 2, A.Ignored)) .MustHaveHappenedOnceExactly(); } @@ -235,8 +227,7 @@ public async Task End2End_Test() ISequenceOperationsProducer producer = _producerBuilder.UseChannel(_producerChannel) //.WithOptions(producerOption) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids#HappySocks") .BuildSequenceOperationsProducer(); await producer.RegisterAsync(new User()); @@ -250,19 +241,18 @@ public async Task End2End_Test() _consumerBuilder.UseChannel(_consumerChannel) //.WithOptions(consumerOptions) .WithCancellation(cts.Token) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids#HappySocks") .Subscribe(new SequenceOfConsumerBridge(_subscriber)); ch.Writer.Complete(); await subscription.DisposeAsync(); await ch.Reader.Completion; - A.CallTo(() => _subscriber.RegisterAsync(A.Ignored)) + A.CallTo(() => _subscriber.RegisterAsync(A.Ignored, A.Ignored)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.LoginAsync("admin", "1234")) + A.CallTo(() => _subscriber.LoginAsync(A.Ignored, "admin", "1234")) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.EarseAsync(4335)) + A.CallTo(() => _subscriber.EarseAsync(A.Ignored, 4335)) .MustHaveHappenedOnceExactly(); } diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/Entities/User.cs b/Tests/EventSourcing.Backbone.UnitTests/Entities/User.cs similarity index 95% rename from Tests/Weknow.EventSource.Backbone.UnitTests/Entities/User.cs rename to Tests/EventSourcing.Backbone.UnitTests/Entities/User.cs index 3c991a06..9350ddd9 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/Entities/User.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/Entities/User.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.UnitTests.Entities +namespace EventSourcing.Backbone.UnitTests.Entities { /// /// The user. diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/EventIdTests.cs b/Tests/EventSourcing.Backbone.UnitTests/EventIdTests.cs similarity index 97% rename from Tests/Weknow.EventSource.Backbone.UnitTests/EventIdTests.cs rename to Tests/EventSourcing.Backbone.UnitTests/EventIdTests.cs index e5ecfe62..85b6e6a0 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/EventIdTests.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/EventIdTests.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public class EventKeyTests { diff --git a/Tests/EventSourcing.Backbone.UnitTests/EventSourcing.Backbone.UnitTests.csproj b/Tests/EventSourcing.Backbone.UnitTests/EventSourcing.Backbone.UnitTests.csproj new file mode 100644 index 00000000..a99f1026 --- /dev/null +++ b/Tests/EventSourcing.Backbone.UnitTests/EventSourcing.Backbone.UnitTests.csproj @@ -0,0 +1,43 @@ + + + + Debug;Release;Gen + false + disable + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/Factory/ISequenceOfProducer/SequenceOfProducerFactory.cs b/Tests/EventSourcing.Backbone.UnitTests/Factory/ISequenceOfProducer/SequenceOfProducerFactory.cs similarity index 97% rename from Tests/Weknow.EventSource.Backbone.UnitTests/Factory/ISequenceOfProducer/SequenceOfProducerFactory.cs rename to Tests/EventSourcing.Backbone.UnitTests/Factory/ISequenceOfProducer/SequenceOfProducerFactory.cs index 39a0296c..32c8a97a 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/Factory/ISequenceOfProducer/SequenceOfProducerFactory.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/Factory/ISequenceOfProducer/SequenceOfProducerFactory.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.UnitTests.Entities +namespace EventSourcing.Backbone.UnitTests.Entities { public class SequenceOfProducerFactory : ProducerPipeline, ISequenceOfProducer diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/Factory/ISequenceOfProducer/SequenceOfProducerFactoryExtensions.cs b/Tests/EventSourcing.Backbone.UnitTests/Factory/ISequenceOfProducer/SequenceOfProducerFactoryExtensions.cs similarity index 84% rename from Tests/Weknow.EventSource.Backbone.UnitTests/Factory/ISequenceOfProducer/SequenceOfProducerFactoryExtensions.cs rename to Tests/EventSourcing.Backbone.UnitTests/Factory/ISequenceOfProducer/SequenceOfProducerFactoryExtensions.cs index 23694a01..30fb840e 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/Factory/ISequenceOfProducer/SequenceOfProducerFactoryExtensions.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/Factory/ISequenceOfProducer/SequenceOfProducerFactoryExtensions.cs @@ -1,6 +1,6 @@ -using Weknow.EventSource.Backbone.Building; +using EventSourcing.Backbone.Building; -namespace Weknow.EventSource.Backbone.UnitTests.Entities +namespace EventSourcing.Backbone.UnitTests.Entities { public static class SequenceOfProducerFactoryExtensions { diff --git a/Tests/EventSourcing.Backbone.UnitTests/Factory/SequenceOperations/SequenceOperationsConsumer.cs b/Tests/EventSourcing.Backbone.UnitTests/Factory/SequenceOperations/SequenceOperationsConsumer.cs new file mode 100644 index 00000000..4eeb5368 --- /dev/null +++ b/Tests/EventSourcing.Backbone.UnitTests/Factory/SequenceOperations/SequenceOperationsConsumer.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks.Dataflow; + +namespace EventSourcing.Backbone.UnitTests.Entities +{ + public class SequenceOperationsConsumer : ISequenceOperationsConsumer + { + private readonly ActionBlock _block = new ActionBlock( + u => Console.WriteLine(u.Details.Id), + new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 10 }); + public ValueTask RegisterAsync(ConsumerMetadata meta, User user) + { + //var msg = new Ackable(user, ack); + //_block.Post(msg); + return ValueTask.CompletedTask; + } + + public ValueTask UpdateAsync(ConsumerMetadata meta, User user) => throw new NotImplementedException(); + public ValueTask LoginAsync(ConsumerMetadata meta, string email, string password) => throw new NotImplementedException(); + public ValueTask LogoffAsync(ConsumerMetadata meta, int id) => throw new NotImplementedException(); + public ValueTask ApproveAsync(ConsumerMetadata meta, int id) => throw new NotImplementedException(); + public ValueTask SuspendAsync(ConsumerMetadata meta, int id) => throw new NotImplementedException(); + public ValueTask ActivateAsync(ConsumerMetadata meta, int id) => throw new NotImplementedException(); + public ValueTask EarseAsync(ConsumerMetadata meta, int id) => throw new NotImplementedException(); + } +} diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/Factory/SequenceOperations/SequenceOperationsProducerFactory.cs b/Tests/EventSourcing.Backbone.UnitTests/Factory/SequenceOperations/SequenceOperationsProducerFactory.cs similarity index 98% rename from Tests/Weknow.EventSource.Backbone.UnitTests/Factory/SequenceOperations/SequenceOperationsProducerFactory.cs rename to Tests/EventSourcing.Backbone.UnitTests/Factory/SequenceOperations/SequenceOperationsProducerFactory.cs index 1dda2835..02c738d9 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/Factory/SequenceOperations/SequenceOperationsProducerFactory.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/Factory/SequenceOperations/SequenceOperationsProducerFactory.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.UnitTests.Entities +namespace EventSourcing.Backbone.UnitTests.Entities { public class SequenceOperationsProducerFactory : ProducerPipeline, ISequenceOperationsProducer diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/Factory/SequenceOperations/SequenceOperationsProducerFactoryExtensions.cs b/Tests/EventSourcing.Backbone.UnitTests/Factory/SequenceOperations/SequenceOperationsProducerFactoryExtensions.cs similarity index 77% rename from Tests/Weknow.EventSource.Backbone.UnitTests/Factory/SequenceOperations/SequenceOperationsProducerFactoryExtensions.cs rename to Tests/EventSourcing.Backbone.UnitTests/Factory/SequenceOperations/SequenceOperationsProducerFactoryExtensions.cs index f8ea7315..47421154 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/Factory/SequenceOperations/SequenceOperationsProducerFactoryExtensions.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/Factory/SequenceOperations/SequenceOperationsProducerFactoryExtensions.cs @@ -1,6 +1,6 @@ -using Weknow.EventSource.Backbone.Building; +using EventSourcing.Backbone.Building; -namespace Weknow.EventSource.Backbone.UnitTests.Entities +namespace EventSourcing.Backbone.UnitTests.Entities { public static class SequenceOperationsProducerFactoryExtensions { diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/InheritGenerationTest.cs b/Tests/EventSourcing.Backbone.UnitTests/InheritGenerationTest.cs similarity index 87% rename from Tests/Weknow.EventSource.Backbone.UnitTests/InheritGenerationTest.cs rename to Tests/EventSourcing.Backbone.UnitTests/InheritGenerationTest.cs index 4635d9d9..424ed1de 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/InheritGenerationTest.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/InheritGenerationTest.cs @@ -1,11 +1,11 @@ -using Weknow.EventSource.Backbone.UnitTests.Entities; +using EventSourcing.Backbone.UnitTests.Entities; using Xunit; using Xunit.Abstractions; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public class InheritGenerationTest { diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/ProducerBuilderTests.cs b/Tests/EventSourcing.Backbone.UnitTests/ProducerBuilderTests.cs similarity index 80% rename from Tests/Weknow.EventSource.Backbone.UnitTests/ProducerBuilderTests.cs rename to Tests/EventSourcing.Backbone.UnitTests/ProducerBuilderTests.cs index 3c80cee2..285f2aa2 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/ProducerBuilderTests.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/ProducerBuilderTests.cs @@ -1,25 +1,27 @@ using System.Threading.Channels; +using EventSourcing.Backbone.Building; +using EventSourcing.Backbone.UnitTests.Entities; + using FakeItEasy; using Microsoft.Extensions.Logging; -using Weknow.EventSource.Backbone.Building; -using Weknow.EventSource.Backbone.UnitTests.Entities; - using Xunit; using Xunit.Abstractions; +#pragma warning disable S2699 // Tests should include assertions +#pragma warning disable S1481 // Unused local variables should be removed +#pragma warning disable S125 // Sections of code should not be commented out -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public class ProducerBuilderTests { private readonly ITestOutputHelper _outputHelper; private readonly IProducerBuilder _builder = ProducerBuilder.Empty; private readonly Func _channel; - //private readonly IDataSerializer _serializer; private readonly IProducerInterceptor _rawInterceptor = A.Fake(); private readonly IProducerAsyncInterceptor _rawAsyncInterceptor = A.Fake(); private readonly IProducerAsyncSegmentationStrategy _segmentationStrategy = A.Fake(); @@ -50,20 +52,17 @@ public async Task Build_Merge_Producer_Test() { var producerA = _builder.UseChannel(_channel) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids:HappySocks") .AddInterceptor(_rawAsyncInterceptor); var producerB = _builder.UseTestProducerChannel1() - .Partition("NGOs") - .Shard("NGO #2782228") + .Uri("NGOs:NGO #2782228") .UseSegmentation(_segmentationStrategy); var producerC = _builder.UseTestProducerChannel2() - .Partition("Fans") - .Shard("Geek: @someone") + .Uri("Fans:Geek: @someone") .UseSegmentation(_otherSegmentationStrategy); @@ -73,6 +72,8 @@ public async Task Build_Merge_Producer_Test() .UseSegmentation(_postSegmentationStrategy) .CustomBuildSequenceOfProducer(); + _outputHelper.WriteLine("sending"); + string[] ids1 = await producer.RegisterAsync(new User()); string[] ids2 = await producer.LoginAsync("admin", "1234"); string[] ids3 = await producer.EarseAsync(4335); @@ -94,8 +95,7 @@ public async Task Build_Serializer_Producer_Test() ISequenceOperationsProducer producer = _builder.UseChannel(_channel) //.WithOptions(option) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids:HappySocks") .BuildSequenceOperationsProducer(); await producer.RegisterAsync(new User()); @@ -117,8 +117,7 @@ public async Task Build_GeneratedFactory_Producer_Test() ISequenceOfProducer producer = _builder.UseChannel(_channel) //.WithOptions(option) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids:HappySocks") .BuildSequenceOfProducer(); await producer.RegisterAsync(new User()); @@ -140,8 +139,7 @@ public async Task Build_GeneratedFactory_Specialize_Producer_Test() ISequenceOfProducer producer = _builder.UseChannel(_channel) //.WithOptions(option) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids:HappySocks") .Specialize() .Environment("QA") .BuildSequenceOfProducer(); @@ -155,29 +153,6 @@ public async Task Build_GeneratedFactory_Specialize_Producer_Test() #endregion // Build_GeneratedFactory_Specialize_Producer_Test - #region Build_Factory_Producer_Test - - [Fact] - public async Task Build_Factory_Producer_Test() - { - //var option = new EventSourceOptions(_serializer); - - ISequenceOperationsProducer producer = - _builder.UseChannel(_channel) - //.WithOptions(option) - .Partition("Organizations") - .Shard("Org: #RedSocks") - .BuildSequenceOperationsProducer(); - - await producer.RegisterAsync(new User()); - await producer.LoginAsync("admin", "1234"); - await producer.EarseAsync(4335); - - var message = await ch.Reader.ReadAsync(); - } - - #endregion // Build_Factory_Producer_Test - #region Build_Factory_Producer_WithReturn_Test [Fact] @@ -188,8 +163,7 @@ public async Task Build_Factory_Producer_WithReturn_Test() ISequenceOfProducer producer = _builder.UseChannel(_channel) //.WithOptions(option) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids:HappySocks") .CustomBuildSequenceOfProducer(); string id1 = await producer.RegisterAsync(new User()); @@ -214,8 +188,7 @@ public async Task Build_Factory_Specialize_Producer_Test() ISequenceOperationsProducer producer = _builder.UseChannel(_channel) //.WithOptions(option) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids:HappySocks") .Specialize() .Environment("QA") .BuildSequenceOperationsProducer(); @@ -236,8 +209,7 @@ public async Task Build_Interceptor_Producer_Test() { ISequenceOperationsProducer producer = _builder.UseChannel(_channel) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids:HappySocks") .AddInterceptor(_rawInterceptor) .AddInterceptor(_rawAsyncInterceptor) .UseSegmentation(_segmentationStrategy) diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/S3OptionsTests.cs b/Tests/EventSourcing.Backbone.UnitTests/S3OptionsTests.cs similarity index 98% rename from Tests/Weknow.EventSource.Backbone.UnitTests/S3OptionsTests.cs rename to Tests/EventSourcing.Backbone.UnitTests/S3OptionsTests.cs index 6e3353b8..19ddadf3 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/S3OptionsTests.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/S3OptionsTests.cs @@ -2,7 +2,7 @@ -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public class S3OptionsTests diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/SerializationTests.cs b/Tests/EventSourcing.Backbone.UnitTests/SerializationTests.cs similarity index 91% rename from Tests/Weknow.EventSource.Backbone.UnitTests/SerializationTests.cs rename to Tests/EventSourcing.Backbone.UnitTests/SerializationTests.cs index bf0953ee..5fa12e2d 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/SerializationTests.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/SerializationTests.cs @@ -1,6 +1,6 @@ using Xunit; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public class SerializationTests { @@ -13,8 +13,7 @@ public void Metadata_Serialization_Test() { MessageId = "message1", Environment = "env1", - Partition = "partition1", - Shard = "shard1", + Uri = "uri1", Operation = "operation1" }; @@ -56,8 +55,7 @@ public void Announcement_Serialization_Test() { MessageId = "message1", Environment = "env1", - Partition = "partition1", - Shard = "shard1", + Uri = "uri1", Operation = "operation1" }; meta = meta with { Linked = meta, Origin = MessageOrigin.Copy }; @@ -73,7 +71,8 @@ public void Announcement_Serialization_Test() var deserialize = serializer.Deserialize(buffer); Assert.Equal(announcement.Metadata, deserialize.Metadata); - Assert.True(deserialize.Segments.TryGetValue("X", out var arr)); + Assert.True(announcement.Segments.TryGetValue("X", out var arr)); + Assert.True(deserialize.Segments.TryGetValue("X", out arr)); Assert.Equal(1, arr.Span[0]); Assert.Equal(2, arr.Span[1]); } diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/SourceMigrationTests.cs b/Tests/EventSourcing.Backbone.UnitTests/SourceMigrationTests.cs similarity index 75% rename from Tests/Weknow.EventSource.Backbone.UnitTests/SourceMigrationTests.cs rename to Tests/EventSourcing.Backbone.UnitTests/SourceMigrationTests.cs index a4045afb..c1a7a317 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/SourceMigrationTests.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/SourceMigrationTests.cs @@ -1,19 +1,19 @@ using System.Threading.Channels; +using EventSourcing.Backbone.Building; +using EventSourcing.Backbone.UnitTests.Entities; + using FakeItEasy; using Microsoft.Extensions.Logging; -using Weknow.EventSource.Backbone.Building; -using Weknow.EventSource.Backbone.UnitTests.Entities; - using Xunit; using Xunit.Abstractions; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public class SourceMigrationTests { @@ -57,8 +57,7 @@ public async Task Migration_Simple_Test() ISimpleEventProducer producer = _producerBuilder.UseChannel(_producerChannel) //.WithOptions(producerOption) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids:HappySocks") .BuildSimpleEventProducer(); await producer.ExecuteAsync("Id", 1); @@ -70,7 +69,7 @@ public async Task Migration_Simple_Test() _producerBuilder.UseChannel(_rawProducerChannel) .BuildRaw(); - ISubscriptionBridge subscriptionBridge = new SubscriptionBridge(rawProducer); + ISubscriptionBridge subscriptionBridge = rawProducer.ToSubscriptionBridge(); var cts = new CancellationTokenSource(); @@ -78,8 +77,7 @@ public async Task Migration_Simple_Test() _consumerBuilder.UseChannel(_consumerChannel) //.WithOptions(consumerOptions)r .WithCancellation(cts.Token) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids:HappySocks") .Subscribe(subscriptionBridge); // ASSERT @@ -87,10 +85,8 @@ public async Task Migration_Simple_Test() Assert.Equal(MessageOrigin.Copy, a1.Metadata.Origin); Assert.Equal(MessageOrigin.Original, a1.Metadata.Linked.Origin); Assert.Equal("ExecuteAsync", a1.Metadata.Operation); - Assert.Equal("Organizations", a1.Metadata.Partition); - Assert.Equal("Organizations", a1.Metadata.Linked.Partition); - Assert.Equal("Org: #RedSocks", a1.Metadata.Shard); - Assert.Equal("Org: #RedSocks", a1.Metadata.Linked.Shard); + Assert.Equal("Kids:HappySocks", a1.Metadata.Uri); + Assert.Equal("Kids:HappySocks", a1.Metadata.Linked.Uri); Assert.Equal(2, a1.Segments.Count()); Assert.True(a1.Segments.TryGet("key", out string k1)); @@ -131,8 +127,7 @@ public async Task Migration_Change_Target_Test() ISimpleEventProducer producer = _producerBuilder.UseChannel(_producerChannel) //.WithOptions(producerOption) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids:HappySocks") .BuildSimpleEventProducer(); await producer.ExecuteAsync("Id", 1); @@ -143,11 +138,10 @@ public async Task Migration_Change_Target_Test() IRawProducer rawProducer = _producerBuilder.UseChannel(_rawProducerChannel) //.WithOptions(producerOption) - .Partition("New-Organizations") - .Shard("Org: #WhiteSocks") + .Uri("Man:Socks") .BuildRaw(); - ISubscriptionBridge subscriptionBridge = new SubscriptionBridge(rawProducer); + ISubscriptionBridge subscriptionBridge = rawProducer.ToSubscriptionBridge(); var cts = new CancellationTokenSource(); @@ -155,8 +149,7 @@ public async Task Migration_Change_Target_Test() _consumerBuilder.UseChannel(_consumerChannel) //.WithOptions(consumerOptions)r .WithCancellation(cts.Token) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids:HappySocks") .Subscribe(subscriptionBridge); // ASSERT @@ -164,10 +157,8 @@ public async Task Migration_Change_Target_Test() Assert.Equal(MessageOrigin.Copy, a1.Metadata.Origin); Assert.Equal(MessageOrigin.Original, a1.Metadata.Linked.Origin); Assert.Equal("ExecuteAsync", a1.Metadata.Operation); - Assert.Equal("New-Organizations", a1.Metadata.Partition); - Assert.Equal("Organizations", a1.Metadata.Linked.Partition); - Assert.Equal("Org: #WhiteSocks", a1.Metadata.Shard); - Assert.Equal("Org: #RedSocks", a1.Metadata.Linked.Shard); + Assert.Equal("Man:Socks", a1.Metadata.Uri); + Assert.Equal("Kids:HappySocks", a1.Metadata.Linked.Uri); Assert.Equal(2, a1.Segments.Count()); Assert.True(a1.Segments.TryGet("key", out string k1)); Assert.Equal("Id", k1); @@ -178,10 +169,8 @@ public async Task Migration_Change_Target_Test() Assert.Equal(MessageOrigin.Copy, a2.Metadata.Origin); Assert.Equal(MessageOrigin.Original, a2.Metadata.Linked.Origin); Assert.Equal("RunAsync", a2.Metadata.Operation); - Assert.Equal("New-Organizations", a2.Metadata.Partition); - Assert.Equal("Organizations", a2.Metadata.Linked.Partition); - Assert.Equal("Org: #WhiteSocks", a2.Metadata.Shard); - Assert.Equal("Org: #RedSocks", a2.Metadata.Linked.Shard); + Assert.Equal("Man:Socks", a2.Metadata.Uri); + Assert.Equal("Kids:HappySocks", a2.Metadata.Linked.Uri); Assert.Equal(2, a1.Segments.Count()); Assert.True(a2.Segments.TryGet("id", out int i2)); Assert.Equal(1, i2); @@ -191,10 +180,8 @@ public async Task Migration_Change_Target_Test() Assert.Equal(MessageOrigin.Copy, a3.Metadata.Origin); Assert.Equal(MessageOrigin.Original, a3.Metadata.Linked.Origin); Assert.Equal("RunAsync", a3.Metadata.Operation); - Assert.Equal("New-Organizations", a3.Metadata.Partition); - Assert.Equal("Organizations", a3.Metadata.Linked.Partition); - Assert.Equal("Org: #WhiteSocks", a3.Metadata.Shard); - Assert.Equal("Org: #RedSocks", a3.Metadata.Linked.Shard); + Assert.Equal("Man:Socks", a3.Metadata.Uri); + Assert.Equal("Kids:HappySocks", a3.Metadata.Linked.Uri); Assert.Equal(2, a1.Segments.Count()); Assert.True(a3.Segments.TryGet("id", out int i3)); Assert.Equal(2, i3); @@ -207,23 +194,5 @@ public async Task Migration_Change_Target_Test() } #endregion // Migration_Change_Target_Test - - - private class SubscriptionBridge : ISubscriptionBridge - { - private readonly IRawProducer _fw; - - public SubscriptionBridge(IRawProducer fw) - { - _fw = fw; - } - - public async Task BridgeAsync(Announcement announcement, IConsumerBridge consumerBridge) - { - await _fw.Produce(announcement); - return true; - - } - } } } diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/StoreStrategyTests.cs b/Tests/EventSourcing.Backbone.UnitTests/StoreStrategyTests.cs similarity index 94% rename from Tests/Weknow.EventSource.Backbone.UnitTests/StoreStrategyTests.cs rename to Tests/EventSourcing.Backbone.UnitTests/StoreStrategyTests.cs index 0daaad8b..babedd0e 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/StoreStrategyTests.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/StoreStrategyTests.cs @@ -1,18 +1,18 @@ using System.Threading.Channels; +using EventSourcing.Backbone.Building; +using EventSourcing.Backbone.UnitTests.Entities; + using FakeItEasy; using Microsoft.Extensions.Logging; -using Weknow.EventSource.Backbone.Building; -using Weknow.EventSource.Backbone.UnitTests.Entities; - using Xunit; using Xunit.Abstractions; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public class StoreStrategyTests { @@ -61,8 +61,7 @@ public async Task Build_Serializer_Producer_Test() .AddStorageStrategy(l => _producerStorageStrategyA.ToValueTask(), filter: LocalOnlyEmail) .AddStorageStrategy(l => _producerStorageStrategyB.ToValueTask(), filter: LocalAllButEmail) //.WithOptions(producerOption) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids:HappySocks") .BuildSequenceOperationsProducer(); await producer.RegisterAsync(new User()); @@ -79,19 +78,18 @@ public async Task Build_Serializer_Producer_Test() .AddStorageStrategyFactory(l => _consumerStorageStrategyC.ToValueTask(), EventBucketCategories.Interceptions) //.WithOptions(consumerOptions) .WithCancellation(cts.Token) - .Partition("Organizations") - .Shard("Org: #RedSocks") + .Uri("Kids:HappySocks") .Subscribe(new SequenceOfConsumerBridge(_subscriber)); _ch.Writer.Complete(); await subscription.DisposeAsync(); await _ch.Reader.Completion; - A.CallTo(() => _subscriber.RegisterAsync(A.Ignored)) + A.CallTo(() => _subscriber.RegisterAsync(A.Ignored, A.Ignored)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.LoginAsync("admin", "1234")) + A.CallTo(() => _subscriber.LoginAsync(A.Ignored, "admin", "1234")) .MustHaveHappenedOnceExactly(); - A.CallTo(() => _subscriber.EarseAsync(4335)) + A.CallTo(() => _subscriber.EarseAsync(A.Ignored, 4335)) .MustHaveHappenedOnceExactly(); A.CallTo(() => _producerStorageStrategyA.SaveBucketAsync( A.Ignored, diff --git a/Tests/EventSourcing.Backbone.UnitTests/Subscriptions/SimpleEventSubscription.cs b/Tests/EventSourcing.Backbone.UnitTests/Subscriptions/SimpleEventSubscription.cs new file mode 100644 index 00000000..405b93f5 --- /dev/null +++ b/Tests/EventSourcing.Backbone.UnitTests/Subscriptions/SimpleEventSubscription.cs @@ -0,0 +1,27 @@ +using EventSourcing.Backbone.UnitTests.Entities; + +namespace EventSourcing.Backbone +{ + + /// + /// In-Memory Channel (excellent for testing) + /// + /// + public class SimpleEventSubscription : SimpleEventSubscriptionBase + { + private readonly ISimpleEventConsumer _target; + + #region Ctor + + public SimpleEventSubscription(ISimpleEventConsumer target) + { + _target = target; + } + + #endregion // Ctor + + protected override ValueTask ExecuteAsync(ConsumerMetadata consumerMetadata, string key, int value) => _target.ExecuteAsync(consumerMetadata, key, value); + + protected override ValueTask RunAsync(ConsumerMetadata consumerMetadata, int id, DateTime date) => _target.RunAsync(consumerMetadata, id, date); + } +} diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionBase.cs b/Tests/EventSourcing.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionBase.cs similarity index 71% rename from Tests/Weknow.EventSource.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionBase.cs rename to Tests/EventSourcing.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionBase.cs index 66dd490c..eed5eeaa 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionBase.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionBase.cs @@ -1,6 +1,6 @@ -using Weknow.EventSource.Backbone.UnitTests.Entities; +using EventSourcing.Backbone.UnitTests.Entities; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// @@ -12,20 +12,21 @@ public abstract class SimpleEventSubscriptionBase : ISubscriptionBridge async Task ISubscriptionBridge.BridgeAsync(Announcement announcement, IConsumerBridge consumerBridge) { + ConsumerMetadata consumerMetadata = ConsumerMetadata.Context; switch (announcement.Metadata.Operation) { case nameof(ISimpleEventConsumer.ExecuteAsync): { var p0 = await consumerBridge.GetParameterAsync(announcement, "key"); var p1 = await consumerBridge.GetParameterAsync(announcement, "value"); - await ExecuteAsync(p0, p1); + await ExecuteAsync(consumerMetadata, p0, p1); return true; } case nameof(ISimpleEventConsumer.RunAsync): { var p0 = await consumerBridge.GetParameterAsync(announcement, "id"); var p1 = await consumerBridge.GetParameterAsync(announcement, "date"); - await RunAsync(p0, p1); + await RunAsync(consumerMetadata, p0, p1); return true; } default: @@ -37,17 +38,19 @@ async Task ISubscriptionBridge.BridgeAsync(Announcement announcement, ICon /// /// Executes the asynchronous. /// + /// The consumer metadata. /// The key. /// The value. /// - protected abstract ValueTask ExecuteAsync(string key, int value); + protected abstract ValueTask ExecuteAsync(ConsumerMetadata consumerMetadata, string key, int value); /// /// Runs the asynchronous. /// + /// The consumer metadata. /// The identifier. /// The date. /// - protected abstract ValueTask RunAsync(int id, DateTime date); + protected abstract ValueTask RunAsync(ConsumerMetadata consumerMetadata, int id, DateTime date); } } diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionBridge.cs b/Tests/EventSourcing.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionBridge.cs similarity index 81% rename from Tests/Weknow.EventSource.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionBridge.cs rename to Tests/EventSourcing.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionBridge.cs index b6e08c00..f9a5bf74 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionBridge.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionBridge.cs @@ -1,12 +1,12 @@ -using Weknow.EventSource.Backbone.UnitTests.Entities; +using EventSourcing.Backbone.UnitTests.Entities; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// In-Memory Channel (excellent for testing) /// - /// + /// public class SimpleEventSubscriptionBridge : ISubscriptionBridge { private readonly ISimpleEventConsumer _target; @@ -26,20 +26,21 @@ public SimpleEventSubscriptionBridge(ISimpleEventConsumer target) async Task ISubscriptionBridge.BridgeAsync(Announcement announcement, IConsumerBridge consumerBridge) { + var meta = ConsumerMetadata.Context; switch (announcement.Metadata.Operation) { case nameof(ISimpleEventConsumer.ExecuteAsync): { var p0 = await consumerBridge.GetParameterAsync(announcement, "key"); var p1 = await consumerBridge.GetParameterAsync(announcement, "value"); - await _target.ExecuteAsync(p0, p1); + await _target.ExecuteAsync(meta, p0, p1); return true; } case nameof(ISimpleEventConsumer.RunAsync): { var p0 = await consumerBridge.GetParameterAsync(announcement, "id"); var p1 = await consumerBridge.GetParameterAsync(announcement, "date"); - await _target.RunAsync(p0, p1); + await _target.RunAsync(meta, p0, p1); return true; } default: diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionBridgeExtensions.cs b/Tests/EventSourcing.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionBridgeExtensions.cs similarity index 72% rename from Tests/Weknow.EventSource.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionBridgeExtensions.cs rename to Tests/EventSourcing.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionBridgeExtensions.cs index f7d47b32..f0a4d485 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionBridgeExtensions.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionBridgeExtensions.cs @@ -1,7 +1,7 @@ -using Weknow.EventSource.Backbone.Building; -using Weknow.EventSource.Backbone.UnitTests.Entities; +using EventSourcing.Backbone.Building; +using EventSourcing.Backbone.UnitTests.Entities; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public static class SimpleEventSubscriptionBridgeExtensions { diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionFromGen.cs b/Tests/EventSourcing.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionFromGen.cs similarity index 50% rename from Tests/Weknow.EventSource.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionFromGen.cs rename to Tests/EventSourcing.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionFromGen.cs index 0d7aed98..014caa68 100644 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionFromGen.cs +++ b/Tests/EventSourcing.Backbone.UnitTests/Subscriptions/SimpleEventSubscriptionFromGen.cs @@ -1,7 +1,7 @@ -using Weknow.EventSource.Backbone.UnitTests.Entities; -using Weknow.EventSource.Backbone.UnitTests.Entities.Hidden; +using EventSourcing.Backbone.UnitTests.Entities; +using EventSourcing.Backbone.UnitTests.Entities.Hidden; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { public class SimpleEventSubscriptionFromGen : SimpleEventConsumerBase @@ -21,8 +21,8 @@ public SimpleEventSubscriptionFromGen(ISimpleEventConsumer target) #endregion // Ctor - protected override ValueTask ExecuteAsync(string key, int value) => _target.ExecuteAsync(key, value); + protected override ValueTask ExecuteAsync(ConsumerMetadata consumerMetadata, string key, int value) => _target.ExecuteAsync(consumerMetadata, key, value); - protected override ValueTask RunAsync(int id, DateTime date) => _target.RunAsync(id, date); + protected override ValueTask RunAsync(ConsumerMetadata consumerMetadata, int id, DateTime date) => _target.RunAsync(consumerMetadata, id, date); } } diff --git a/Tests/EventSourcing.Backbone.UnitTests/icon.png b/Tests/EventSourcing.Backbone.UnitTests/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/Tests/EventSourcing.Backbone.UnitTests/icon.png differ diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/.editorconfig b/Tests/EventSourcing.Backbone.WebEventTest/.editorconfig similarity index 100% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/.editorconfig rename to Tests/EventSourcing.Backbone.WebEventTest/.editorconfig diff --git a/Tests/EventSourcing.Backbone.WebEventTest/AspCoreExtensions.cs b/Tests/EventSourcing.Backbone.WebEventTest/AspCoreExtensions.cs new file mode 100644 index 00000000..af2dba5d --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/AspCoreExtensions.cs @@ -0,0 +1,78 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +using EventSourcing.Backbone; + +using Microsoft.AspNetCore.Server.Kestrel.Core; + +using StackExchange.Redis; + +// Configuration: https://medium.com/@gparlakov/the-confusion-of-asp-net-configuration-with-environment-variables-c06c545ef732 + + +namespace Microsoft.Extensions.Configuration +{ + /// + /// core extensions for ASP.NET Core + /// + public static class AspCoreExtensions + { + #region AddRedis + + /// + /// Adds the standard configuration. + /// + /// The services. + /// + public static IConnectionMultiplexer AddRedis( + this IServiceCollection services) + { + IConnectionMultiplexer redisConnection = RedisClientFactory.CreateProviderAsync().Result; + services.AddSingleton(redisConnection); + + return redisConnection; + } + + #endregion // AddRedis + + #region WithJsonOptions + + /// + /// Set Controller's with the standard json configuration. + /// + /// The controllers. + /// + public static IMvcBuilder WithJsonOptions( + this IMvcBuilder controllers) + { + return controllers.AddJsonOptions(options => + { + // https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-how-to + JsonSerializerOptions setting = options.JsonSerializerOptions; + setting.WithDefault(); + + }); + } + + #endregion // WithJsonOptions + + #region WithDefault + + /// + /// Withes the default. + /// + /// The options. + /// + public static JsonSerializerOptions WithDefault(this JsonSerializerOptions options) + { + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; + options.WriteIndented = true; + options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); + options.Converters.Add(JsonMemoryBytesConverterFactory.Default); + return options; + } + + #endregion // WithDefault + } +} diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/Contracts/IEventFlow.cs b/Tests/EventSourcing.Backbone.WebEventTest/Contracts/IEventFlow.cs similarity index 66% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/Contracts/IEventFlow.cs rename to Tests/EventSourcing.Backbone.WebEventTest/Contracts/IEventFlow.cs index 251db66b..a503183b 100644 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/Contracts/IEventFlow.cs +++ b/Tests/EventSourcing.Backbone.WebEventTest/Contracts/IEventFlow.cs @@ -1,12 +1,12 @@ using System.ComponentModel; using System.Text.Json; -namespace Weknow.EventSource.Backbone.WebEventTest +namespace EventSourcing.Backbone.WebEventTest { - [GenerateEventSource(EventSourceGenType.Consumer)] - [GenerateEventSource(EventSourceGenType.Producer)] - //[GenerateEventSource(EventSourceGenType.Consumer, Namespace = "Weknow.EventSource.Backbone.WebEventTest")] - //[GenerateEventSource(EventSourceGenType.Producer, Namespace = "Weknow.EventSource.Backbone.WebEventTest")] + [EventsContract(EventsContractType.Consumer)] + [EventsContract(EventsContractType.Producer)] + //[EventsContract(EventSourceGenType.Consumer, Namespace = "EventSourcing.Backbone.WebEventTest")] + //[EventsContract(EventSourceGenType.Producer, Namespace = "EventSourcing.Backbone.WebEventTest")] [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Used for code generation, use the producer / consumer version of it", true)] public interface IEventFlow diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/Contracts/IEventsMigration.cs b/Tests/EventSourcing.Backbone.WebEventTest/Contracts/IEventsMigration.cs similarity index 90% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/Contracts/IEventsMigration.cs rename to Tests/EventSourcing.Backbone.WebEventTest/Contracts/IEventsMigration.cs index 3e6d503b..b3a39091 100644 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/Contracts/IEventsMigration.cs +++ b/Tests/EventSourcing.Backbone.WebEventTest/Contracts/IEventsMigration.cs @@ -1,6 +1,6 @@ using Refit; -namespace Weknow.EventSource.Backbone.WebEventTest +namespace EventSourcing.Backbone.WebEventTest { /// diff --git a/Tests/EventSourcing.Backbone.WebEventTest/Contracts/IS3Test.cs b/Tests/EventSourcing.Backbone.WebEventTest/Contracts/IS3Test.cs new file mode 100644 index 00000000..735b640d --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/Contracts/IS3Test.cs @@ -0,0 +1,10 @@ +namespace EventSourcing.Backbone.WebEventTest +{ + [EventsContract(EventsContractType.Consumer)] + [EventsContract(EventsContractType.Producer)] + [Obsolete("Used for code generation, use the producer / consumer version of it", true)] + public interface IS3Test + { + ValueTask NameAsync(string payload); + } +} diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/Contracts/IVersionedEvents.cs b/Tests/EventSourcing.Backbone.WebEventTest/Contracts/IVersionedEvents.cs similarity index 86% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/Contracts/IVersionedEvents.cs rename to Tests/EventSourcing.Backbone.WebEventTest/Contracts/IVersionedEvents.cs index f9b84fb5..65c62a51 100644 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/Contracts/IVersionedEvents.cs +++ b/Tests/EventSourcing.Backbone.WebEventTest/Contracts/IVersionedEvents.cs @@ -1,9 +1,9 @@ using System.ComponentModel; -namespace Weknow.EventSource.Backbone.WebEventTest +namespace EventSourcing.Backbone.WebEventTest { - [GenerateEventSource(EventSourceGenType.Consumer)] - [GenerateEventSource(EventSourceGenType.Producer)] + [EventsContract(EventsContractType.Consumer)] + [EventsContract(EventsContractType.Producer)] [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Used for code generation, use the producer / consumer version of it", true)] public interface IVersionedEvents diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/Controllers/EventSourceApiController.cs b/Tests/EventSourcing.Backbone.WebEventTest/Controllers/EventSourceApiController.cs similarity index 86% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/Controllers/EventSourceApiController.cs rename to Tests/EventSourcing.Backbone.WebEventTest/Controllers/EventSourceApiController.cs index b0e70a07..d9605b5e 100644 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/Controllers/EventSourceApiController.cs +++ b/Tests/EventSourcing.Backbone.WebEventTest/Controllers/EventSourceApiController.cs @@ -3,9 +3,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using StackExchange.Redis; - -namespace Weknow.EventSource.Backbone.WebEventTest.Controllers +namespace EventSourcing.Backbone.WebEventTest.Controllers { [Route("api/[controller]")] [ApiController] @@ -15,20 +13,17 @@ public class EventSourceApiController : ControllerBase private readonly IEventFlowProducer _eventFlowProducer; private readonly IConsumerReadyBuilder _consumerBuilder; private readonly IConsumerHooksBuilder _baseBuilder; - private readonly IDatabase _db; public EventSourceApiController( ILogger logger, IEventFlowProducer eventFlowProducer, IConsumerReadyBuilder consumerBuilder, - IConsumerHooksBuilder baseBuilder, - IConnectionMultiplexer redis) + IConsumerHooksBuilder baseBuilder) { _logger = logger; _eventFlowProducer = eventFlowProducer; _consumerBuilder = consumerBuilder; _baseBuilder = baseBuilder; - _db = redis.GetDatabase(); } #region GetAsync @@ -68,12 +63,12 @@ public async ValueTask GetAsync(string eventKey, string env) /// /// /// - [HttpGet("more/{partition}/{shard}/{eventKey}/{env?}")] + [HttpGet("more/{uri}/{eventKey}/{env?}")] //[AllowAnonymous] [ProducesResponseType(StatusCodes.Status201Created)] - public async ValueTask GetMoreAsync(string partition, string shard, string eventKey, string? env = null) + public async ValueTask GetMoreAsync(string uri, string eventKey, string? env = null) { - var receiver = _baseBuilder.Partition(partition).Shard(shard).Environment(env ?? string.Empty).BuildReceiver(); + var receiver = _baseBuilder.Uri(uri).Environment(env ?? string.Empty).BuildReceiver(); var json = await receiver.GetJsonByIdAsync(eventKey); return json; } diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/Controllers/MigrationController.cs b/Tests/EventSourcing.Backbone.WebEventTest/Controllers/MigrationController.cs similarity index 93% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/Controllers/MigrationController.cs rename to Tests/EventSourcing.Backbone.WebEventTest/Controllers/MigrationController.cs index dc8ad944..d99b138e 100644 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/Controllers/MigrationController.cs +++ b/Tests/EventSourcing.Backbone.WebEventTest/Controllers/MigrationController.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc; -namespace Weknow.EventSource.Backbone.WebEventTest.Controllers +namespace EventSourcing.Backbone.WebEventTest.Controllers { [Route("api/[controller]")] [ApiController] diff --git a/Tests/EventSourcing.Backbone.WebEventTest/Controllers/ProducerController.cs b/Tests/EventSourcing.Backbone.WebEventTest/Controllers/ProducerController.cs new file mode 100644 index 00000000..46ad1d5a --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/Controllers/ProducerController.cs @@ -0,0 +1,117 @@ +using Microsoft.AspNetCore.Mvc; + +namespace EventSourcing.Backbone.WebEventTest.Controllers; + +[ApiController] +[Route("[controller]")] +public class ProducerController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IProductCycleProducer _producer; + + public ProducerController( + ILogger logger, + IProductCycleProducer producer) + { + _logger = logger; + _producer = producer; + } + + /// + /// Post order state. + /// + /// The payload. + /// + [HttpPost("idea")] + [ProducesResponseType(StatusCodes.Status201Created)] + //[AllowAnonymous] + public async Task IdeaAsync(Idea payload) + { + var (title, describe) = payload; + _logger.LogDebug("Sending idea event"); + EventKey key = await _producer.IdeaAsync(title, describe); + return key; + } + + /// + /// Post packing state. + /// + /// The payload. + /// + [HttpPost("plan")] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task PlanAsync([FromBody] Plan payload) + { + var (id_, describe) = payload; + var (id, version) = id_; + _logger.LogDebug("Sending plan event"); + EventKey key = await _producer.PlanedAsync(id, version, describe); + return key; + } + + /// + /// Post on-delivery state. + /// + /// The payload. + /// + [HttpPost("review")] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task ReviewAsync([FromBody] Review payload) + { + var (id_, notes) = payload; + var (id, version) = id_; + + _logger.LogDebug("Sending review event"); + EventKey key = await _producer.ReviewedAsync(id, version, notes); + return key; + } + + /// + /// Post on-received state. + /// + /// The payload. + /// + [HttpPost("implement")] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task ImplementAsync([FromBody] Id payload) + { + var (id, version) = payload; + + _logger.LogDebug("Sending implement event"); + EventKey key = await _producer.ImplementedAsync(id, version); + return key; + } + + /// + /// Post on-received state. + /// + /// The payload. + /// + [HttpPost("test")] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task TestAsync([FromBody] Test payload) + { + var (id_, notes) = payload; + var (id, version) = id_; + + _logger.LogDebug("Sending test event"); + EventKey key = await _producer.TestedAsync(id, version, notes); + return key; + } + + /// + /// Post on-received state. + /// + /// The payload. + /// + [HttpPost("Deploy")] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task DeployAsync([FromBody] Id payload) + { + var (id, version) = payload; + + _logger.LogDebug("Sending deploy event"); + EventKey key = await _producer.DeployedAsync(id, version); + return key; + } +} \ No newline at end of file diff --git a/Tests/EventSourcing.Backbone.WebEventTest/Controllers/TestController.cs b/Tests/EventSourcing.Backbone.WebEventTest/Controllers/TestController.cs new file mode 100644 index 00000000..0853ef50 --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/Controllers/TestController.cs @@ -0,0 +1,74 @@ +using Microsoft.AspNetCore.Mvc; + +using StackExchange.Redis; + +namespace EventSourcing.Backbone.WebEventTest.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class TestController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IEventSourceRedisConnectionFactory _connFacroty; + + public TestController( + ILogger logger, + IEventSourceRedisConnectionFactory connFacroty) + { + _logger = logger; + _connFacroty = connFacroty; + } + + /// + /// Post Analysts + /// + /// + /// + [HttpGet("conn")] + //[AllowAnonymous] + [ProducesResponseType(StatusCodes.Status201Created)] + public async ValueTask GetAsync() + { + var conn = await RedisClientFactory.CreateProviderAsync(logger: _logger); + var status = conn.GetStatus(); + var db = conn.GetDatabase(); + var p = await db.PingAsync(); + + return @$"ClientName: {conn.ClientName}, IsConnected: {conn.IsConnected}, +timeout (ms): {conn.TimeoutMilliseconds}, +status: {status}, ping: {p}"; + } + + /// + /// Post Analysts + /// + /// + /// + [HttpGet("ping")] + //[AllowAnonymous] + [ProducesResponseType(StatusCodes.Status201Created)] + public async ValueTask GetPingAsync() + { + IDatabaseAsync db = await _connFacroty.GetDatabaseAsync(CancellationToken.None); + var p = await db.PingAsync(); + _logger.LogInformation("Schema: {schema}", Request.Scheme); + return p; + } + + /// + /// Post Analysts + /// + /// + /// + [HttpGet("static")] + //[AllowAnonymous] + [ProducesResponseType(StatusCodes.Status201Created)] + public async ValueTask GetStaticAsync() + { + IDatabaseAsync db = await _connFacroty.GetDatabaseAsync(CancellationToken.None); + var p = await db.PingAsync(); + + return @$"ping: {p}"; + } +} + diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/Dockerfile b/Tests/EventSourcing.Backbone.WebEventTest/Dockerfile similarity index 100% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/Dockerfile rename to Tests/EventSourcing.Backbone.WebEventTest/Dockerfile diff --git a/Tests/EventSourcing.Backbone.WebEventTest/Dockerfile.develop b/Tests/EventSourcing.Backbone.WebEventTest/Dockerfile.develop new file mode 100644 index 00000000..e605f2e9 --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/Dockerfile.develop @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/dotnet/sdk:5.0 +ARG BUILD_CONFIGURATION=Debug +ENV ASPNETCORE_ENVIRONMENT=Development +ENV ASPNETCORE_URLS=http://+:80 +ENV DOTNET_USE_POLLING_FILE_WATCHER=true +EXPOSE 80 + +WORKDIR /src +COPY ["Tests/.EventSourcing.Backbone.WebEventTest/.EventSourcing.Backbone.WebEventTest.csproj", "Tests/.EventSourcing.Backbone.WebEventTest/"] + +RUN dotnet restore "Tests/.EventSourcing.Backbone.WebEventTest/.EventSourcing.Backbone.WebEventTest.csproj" +COPY . . +WORKDIR "/src/Tests/.EventSourcing.Backbone.WebEventTest" +RUN dotnet build --no-restore ".EventSourcing.Backbone.WebEventTest.csproj" -c $BUILD_CONFIGURATION + +RUN echo "exec dotnet run --no-build --no-launch-profile -c $BUILD_CONFIGURATION --" > /entrypoint.sh + +ENTRYPOINT ["/bin/bash", "/entrypoint.sh"] \ No newline at end of file diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/Entities/Address.cs b/Tests/EventSourcing.Backbone.WebEventTest/Entities/Address.cs similarity index 58% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/Entities/Address.cs rename to Tests/EventSourcing.Backbone.WebEventTest/Entities/Address.cs index 5ac69bfd..4507e0f7 100644 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/Entities/Address.cs +++ b/Tests/EventSourcing.Backbone.WebEventTest/Entities/Address.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.WebEventTest +namespace EventSourcing.Backbone.WebEventTest { public record Address(string country, string city, string street); } diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/Entities/Person.cs b/Tests/EventSourcing.Backbone.WebEventTest/Entities/Person.cs similarity index 58% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/Entities/Person.cs rename to Tests/EventSourcing.Backbone.WebEventTest/Entities/Person.cs index 5c65ab0e..27625a48 100644 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/Entities/Person.cs +++ b/Tests/EventSourcing.Backbone.WebEventTest/Entities/Person.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.WebEventTest +namespace EventSourcing.Backbone.WebEventTest { public record Person(int Id, string Name, Address? Address = null); diff --git a/Tests/EventSourcing.Backbone.WebEventTest/EventSourcing.Backbone.WebEventTest.csproj b/Tests/EventSourcing.Backbone.WebEventTest/EventSourcing.Backbone.WebEventTest.csproj new file mode 100644 index 00000000..fbab0d74 --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/EventSourcing.Backbone.WebEventTest.csproj @@ -0,0 +1,56 @@ + + + + Linux + ..\.. + Debug;Release;Gen + True + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/EventSourcing.Backbone.WebEventTest/Jobs/ConsumerJob.cs b/Tests/EventSourcing.Backbone.WebEventTest/Jobs/ConsumerJob.cs new file mode 100644 index 00000000..93226d36 --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/Jobs/ConsumerJob.cs @@ -0,0 +1,227 @@ +using EventSourcing.Backbone.Building; + +namespace EventSourcing.Backbone.WebEventTest; + +/// +/// Consumer job +/// +/// +/// +/// +public sealed class ConsumerJob : IHostedService, IProductCycleConsumer +{ + private readonly IConsumerSubscribeBuilder _builder; + private CancellationTokenSource? _cancellationTokenSource; + private IConsumerLifetime? _subscription; + + private readonly ILogger _logger; + private readonly IProductCycleProducer _producer; + + #region Ctor + + /// + /// Initializes a new instance. + /// + /// The logger. + /// The consumer builder keyed. + /// The producer. + /// consumer is missing DI + public ConsumerJob( + ILogger logger, + IKeyed consumerBuilderKeyed, + IProductCycleProducer producer) + { + if (!consumerBuilderKeyed.TryGet(EventSourcingConstants.URI, out var consumerBuilder)) throw new EventSourcingException("consumer is missing DI"); + _builder = consumerBuilder.WithLogger(logger); + _logger = logger; + _producer = producer; + } + + #endregion Ctor + + #region OnStartAsync + + /// + /// Start Consumer Job. + /// + /// The cancellation token. + Task IHostedService.StartAsync(CancellationToken cancellationToken) + { + _cancellationTokenSource = new CancellationTokenSource(); + var canellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token); + _subscription = _builder + .Group(EventSourcingConstants.CONSUMER_GROUP) + .WithCancellation(canellation.Token) + // this extension is generate (if you change the interface use the correlated new generated extension method) + .SubscribeProductCycleConsumer(this); + + return Task.CompletedTask; + } + + #endregion // OnStartAsync + + #region StopAsync + + /// + /// Stops the Consumer Job. + /// + /// The cancellation token. + /// + async Task IHostedService.StopAsync(CancellationToken cancellationToken) + { + _cancellationTokenSource?.CancelSafe(); + await (_subscription?.Completion ?? Task.CompletedTask); + } + + #endregion // StopAsync + + // TODO: enrich telemetry + + async ValueTask IProductCycleConsumer.IdeaAsync(ConsumerMetadata consumerMetadata, string title, string describe) + { + var meta = consumerMetadata.Metadata; + LogLevel level = meta.Environment == "prod" ? LogLevel.Debug : LogLevel.Information; + _logger.Log(level, """ + handling {event} [{id}]: {title} + Let's build a plan..... + """, meta.Operation, meta.MessageId, title); + await Task.Delay(Environment.TickCount % 1000 + 100); + + await _producer.PlanedAsync(meta.MessageId, new Version(0, 0, 1, 0), "Just do it this way...."); + await consumerMetadata.AckAsync(); // not required when configuring the consumer to Ack on success. + } + + async ValueTask IProductCycleConsumer.PlanedAsync(ConsumerMetadata consumerMetadata, string id, Version version, string doc) + { + var meta = consumerMetadata.Metadata; + LogLevel level = meta.Environment == "prod" ? LogLevel.Debug : LogLevel.Information; + _logger.Log(level, """ + handling {event} [{id}]: {version} + --- + {doc} + --- + Now we'll review it than, either approve or reject the plan. + """, meta.Operation, id, version, doc); + + int delay = Environment.TickCount % 2_000 + 500; + await Task.Delay(delay); + if (delay < 550) + await _producer.RejectedAsync(id, version, meta.Operation, NextStage.Abandon, "Seems to complex"); + else if (delay < 600) + await _producer.RejectedAsync(id, version, meta.Operation, NextStage.Reject, "Need some refinement about ..."); + else + await _producer.ReviewedAsync(id, version, "Great plan"); + await consumerMetadata.AckAsync(); // not required when configuring the consumer to Ack on success. + } + + async ValueTask IProductCycleConsumer.ReviewedAsync(ConsumerMetadata consumerMetadata, string id, Version version, string[] notes) + { + var meta = consumerMetadata.Metadata; + LogLevel level = meta.Environment == "prod" ? LogLevel.Debug : LogLevel.Information; + _logger.Log(level, """ + handling {event} [{id}]: {version} + --- + {notes} + --- + Let's implement it! + """, meta.Operation, id, version, string.Join("\r\n- ", notes)); + + int delay = Environment.TickCount % 5_000 + 1_500; + await Task.Delay(delay); + await _producer.ImplementedAsync(id, version); + await consumerMetadata.AckAsync(); // not required when configuring the consumer to Ack on success. + } + + async ValueTask IProductCycleConsumer.ImplementedAsync(ConsumerMetadata consumerMetadata, string id, Version version) + { + var meta = consumerMetadata.Metadata; + LogLevel level = meta.Environment == "prod" ? LogLevel.Debug : LogLevel.Information; + _logger.Log(level, """ + handling {event} [{id}]: {version} + --- + Now it's time for QA. + """, meta.Operation, id, version); + + int delay = Environment.TickCount % 3_000 + 100; + await Task.Delay(delay); + if (delay < 600) + await _producer.RejectedAsync(id, version, meta.Operation, NextStage.Reject, "Performance doesn't meet our SLA..."); + else + await _producer.TestedAsync(id, version, "Ready for deployment"); + await consumerMetadata.AckAsync(); // not required when configuring the consumer to Ack on success. + } + + async ValueTask IProductCycleConsumer.TestedAsync(ConsumerMetadata consumerMetadata, string id, Version version, string[] notes) + { + var meta = consumerMetadata.Metadata; + LogLevel level = meta.Environment == "prod" ? LogLevel.Debug : LogLevel.Information; + _logger.Log(level, """ + handling {event} [{id}]: {version} + --- + {notes} + --- + Start deploying + """, meta.Operation, id, version, string.Join("\r\n- ", notes)); + + int delay = Environment.TickCount % 2_000 + 100; + await Task.Delay(delay); + await _producer.DeployedAsync(id, version); + + if (delay < 1_500) + { + _logger.Log(level, "planing next iteration..."); + await _producer.DeployedAsync(id, version); + } + + await consumerMetadata.AckAsync(); // not required when configuring the consumer to Ack on success. + } + + async ValueTask IProductCycleConsumer.DeployedAsync(ConsumerMetadata consumerMetadata, string id, Version version) + { + var meta = consumerMetadata.Metadata; + LogLevel level = meta.Environment == "prod" ? LogLevel.Debug : LogLevel.Information; + _logger.Log(level, "handling {event} [{id}]: {version}", meta.Operation, id, version); + int delay = Environment.TickCount % 2_000 + 100; + if (delay > 600) + { + await Task.Delay(delay); + _logger.Log(level, "planing next iteration..."); + await _producer.PlanedAsync(id, new Version(version.Major, version.Minor, version.Build, version.Revision + 1), "improving xyz..."); + } + } + + async ValueTask IProductCycleConsumer.RejectedAsync(ConsumerMetadata consumerMetadata, + string id, + System.Version version, + string operation, + NextStage nextStage, + string[] notes) + { + var meta = consumerMetadata.Metadata; + LogLevel level = meta.Environment == "prod" ? LogLevel.Debug : LogLevel.Information; + _logger.Log(level, """ + handling {event} [{id}]: {version} + --- + operation = {operation} + + {notes} + --- + """, meta.Operation, id, version, operation, string.Join("\r\n- ", notes)); + + if (nextStage != NextStage.Abandon) + { + string message = operation switch + { + nameof(IProductCycleConsumer.ImplementedAsync) => "Fix bugs", + nameof(IProductCycleConsumer.PlanedAsync) => "Improve plans", + _ => string.Empty + }; + _logger.Log(level, "Re-plan"); + await Task.Delay(1000); + await _producer.PlanedAsync(id, version, message); + } + + await consumerMetadata.AckAsync(); // not required when configuring the consumer to Ack on success. + } +} + diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/Jobs/HostedServiceBase.cs b/Tests/EventSourcing.Backbone.WebEventTest/Jobs/HostedServiceBase.cs similarity index 100% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/Jobs/HostedServiceBase.cs rename to Tests/EventSourcing.Backbone.WebEventTest/Jobs/HostedServiceBase.cs diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/Jobs/MicroDemoJob.cs b/Tests/EventSourcing.Backbone.WebEventTest/Jobs/MicroDemoJob.cs similarity index 74% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/Jobs/MicroDemoJob.cs rename to Tests/EventSourcing.Backbone.WebEventTest/Jobs/MicroDemoJob.cs index 7ab7ee6e..d85adb5e 100644 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/Jobs/MicroDemoJob.cs +++ b/Tests/EventSourcing.Backbone.WebEventTest/Jobs/MicroDemoJob.cs @@ -1,10 +1,8 @@ using System.Text.Json; -using Weknow.EventSource.Backbone.Building; +using EventSourcing.Backbone.Building; -// TODO: Register the service at the Program.cs file services.AddHostedService<...> - -namespace Weknow.EventSource.Backbone.WebEventTest.Jobs +namespace EventSourcing.Backbone.WebEventTest.Jobs { /// /// MicroDemo Event Source Listener @@ -59,13 +57,15 @@ protected override async Task OnStartAsync(CancellationToken cancellationToken) /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// +#pragma warning disable S1186 // Methods should not be empty public void Dispose() { } +#pragma warning restore S1186 // Methods should not be empty #endregion Dispose - private class Subscriber : IEventFlowConsumer + private sealed class Subscriber : IEventFlowConsumer { private readonly ILogger _logger; private readonly IEventFlowProducer _producer; @@ -79,23 +79,23 @@ public Subscriber( _producer = producer; } - async ValueTask IEventFlowConsumer.Stage1Async(Person PII, string payload) + async ValueTask IEventFlowConsumer.Stage1Async(ConsumerMetadata consumerMeta, Person PII, string payload) { - Metadata? meta = ConsumerMetadata.Context; + Metadata meta = consumerMeta.Metadata; - _logger.LogInformation("Consume First Stage {partition} {shard} {PII} {data}", - meta?.Partition, meta?.Shard, PII, payload); + _logger.LogInformation("Consume First Stage {uri} {PII} {data}", + meta?.Uri, PII, payload); await _producer.Stage2Async( JsonDocument.Parse("{\"name\":\"john\"}").RootElement, JsonDocument.Parse("{\"data\":10}").RootElement); } - ValueTask IEventFlowConsumer.Stage2Async(JsonElement PII, JsonElement data) + ValueTask IEventFlowConsumer.Stage2Async(ConsumerMetadata consumerMeta, JsonElement PII, JsonElement data) { - var meta = ConsumerMetadata.Context; - _logger.LogInformation("Consume 2 Stage {partition} {shard} {PII} {data}", - meta?.Metadata?.Partition, meta?.Metadata?.Shard, PII, data); + Metadata meta = consumerMeta.Metadata; + _logger.LogInformation("Consume 2 Stage {uri} {PII} {data}", + meta?.Uri, PII, data); return ValueTask.CompletedTask; } diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/Jobs/MigrationJob.cs b/Tests/EventSourcing.Backbone.WebEventTest/Jobs/MigrationJob.cs similarity index 89% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/Jobs/MigrationJob.cs rename to Tests/EventSourcing.Backbone.WebEventTest/Jobs/MigrationJob.cs index a48ad18b..3559b012 100644 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/Jobs/MigrationJob.cs +++ b/Tests/EventSourcing.Backbone.WebEventTest/Jobs/MigrationJob.cs @@ -1,10 +1,10 @@ -using Weknow.EventSource.Backbone.Building; +using EventSourcing.Backbone.Building; // TODO: Register the service at the Program.cs file services.AddHostedService<...> -namespace Weknow.EventSource.Backbone.WebEventTest.Jobs +namespace EventSourcing.Backbone.WebEventTest.Jobs { /// /// MicroDemo Event Source Listener @@ -48,7 +48,7 @@ public MigrationJob( /// The cancellation token. protected override async Task OnStartAsync(CancellationToken cancellationToken) { - SubscriptionBridge subscription = new(_forwarder, _client); + SubscriptionBridge subscription = new(_forwarder); _builder.Group("Demo-Migration-GROUP") .Subscribe(subscription); @@ -73,12 +73,10 @@ public void Dispose() private class SubscriptionBridge : ISubscriptionBridge { private readonly IEventsMigration _fw; - private readonly HttpClient _client; - public SubscriptionBridge(IEventsMigration fw, HttpClient client) + public SubscriptionBridge(IEventsMigration fw) { _fw = fw; - _client = client; } public async Task BridgeAsync(Announcement announcement, IConsumerBridge consumerBridge) diff --git a/Tests/EventSourcing.Backbone.WebEventTest/OpenTelemetryExtensions.cs b/Tests/EventSourcing.Backbone.WebEventTest/OpenTelemetryExtensions.cs new file mode 100644 index 00000000..f22031f9 --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/OpenTelemetryExtensions.cs @@ -0,0 +1,94 @@ +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +// see: +// https://opentelemetry.io/docs/instrumentation/net/getting-started/ +// https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.Jaeger/README.md#environment-variables + +namespace EventSourcing.Backbone.WebEventTest; + +/// +/// Open telemetry extensions for ASP.NET Core +/// +internal static class OpenTelemetryExtensions +{ + #region AddOpenTelemetryEventSourcing + + /// + /// Adds open telemetry for event sourcing. + /// + /// The builder. + /// + public static IServiceCollection AddOpenTelemetryEventSourcing(this WebApplicationBuilder builder) + { + IWebHostEnvironment environment = builder.Environment; + IServiceCollection services = builder.Services; + + // see: + // https://opentelemetry.io/docs/instrumentation/net/getting-started/ + // https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.Jaeger/README.md#environment-variables + services.AddOpenTelemetry() + .WithEventSourcingTracing(environment, + cfg => + { + cfg + //.AddSource("StackExchange.Redis") + //.AddSource("StackExchangeRedisConnectionInstrumentation") + .AddRedisInstrumentation(RedisClientFactory.CreateProviderAsync().Result, c => + { + c.FlushInterval = TimeSpan.FromSeconds(5); + }) + //.ConfigureRedisInstrumentation((provider, b) => { + // var conn = provider.GetRequiredService(); + // b.AddConnection(conn); + //}) + .AddAspNetCoreInstrumentation(m => + { + m.Filter = TraceFilter; + m.RecordException = true; + m.EnableGrpcAspNetCoreSupport = true; + }) + .AddHttpClientInstrumentation(m => + { + // m.Enrich + m.RecordException = true; + }) + .AddGrpcClientInstrumentation() + .AddOtlpExporter(); + //if (environment.IsDevelopment()) + // cfg.AddConsoleExporter(); + }) + .WithEventSourcingMetrics(environment, cfg => + { + cfg.AddAspNetCoreInstrumentation( /* m => m.Filter = filter */) + .AddOtlpExporter() + .AddPrometheusExporter(); + //if (environment.IsDevelopment()) + // cfg.AddConsoleExporter(); + }); + + return services; + } + + #endregion // AddOpenTelemetryEventSourcing + + #region TraceFilter + + /// + /// Telemetries the filter. + /// + /// The CTX. + /// + private static bool TraceFilter(HttpContext ctx) => ctx.Request.Path.Value switch + { + "/health" => false, + "/readiness" => false, + "/metrics" => false, + string x when x.StartsWith("/swagger") => false, + string x when x.StartsWith("/_framework/") => false, + string x when x.StartsWith("/_vs/") => false, + _ => true + }; + + #endregion // TraceFilter +} diff --git a/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Entities/Id.cs b/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Entities/Id.cs new file mode 100644 index 00000000..238488fd --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Entities/Id.cs @@ -0,0 +1,3 @@ +namespace EventSourcing.Backbone.WebEventTest; + +public record Id(string id, Version version); \ No newline at end of file diff --git a/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Entities/Idea.cs b/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Entities/Idea.cs new file mode 100644 index 00000000..106836a0 --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Entities/Idea.cs @@ -0,0 +1,3 @@ +namespace EventSourcing.Backbone.WebEventTest; + +public record Idea(string title, string describe); diff --git a/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Entities/Plan.cs b/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Entities/Plan.cs new file mode 100644 index 00000000..7068ad21 --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Entities/Plan.cs @@ -0,0 +1,3 @@ +namespace EventSourcing.Backbone.WebEventTest; + +public record Plan(Id id, string describe); diff --git a/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Entities/Review.cs b/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Entities/Review.cs new file mode 100644 index 00000000..b8f88eb1 --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Entities/Review.cs @@ -0,0 +1,4 @@ + +namespace EventSourcing.Backbone.WebEventTest; + +public record Review(Id id, params string[] notes); diff --git a/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Entities/Test.cs b/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Entities/Test.cs new file mode 100644 index 00000000..0b9fc624 --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Entities/Test.cs @@ -0,0 +1,4 @@ + +namespace EventSourcing.Backbone.WebEventTest; + +public record Test(Id id, params string[] notes); diff --git a/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/EventSourcingConstants.cs b/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/EventSourcingConstants.cs new file mode 100644 index 00000000..d951d6b5 --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/EventSourcingConstants.cs @@ -0,0 +1,11 @@ +namespace EventSourcing.Backbone.WebEventTest; + +/// +/// Common constants +/// +public static class EventSourcingConstants +{ + public const string URI = "CHANGE-ME"; + public const string S3_BUCKET = "event-sourcing-change-me"; + public const string CONSUMER_GROUP = "CHANGE_ME-EVENT-SOURCING-CONSUMER-GROUP"; +} diff --git a/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Extensions/ConsumerExtensions.cs b/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Extensions/ConsumerExtensions.cs new file mode 100644 index 00000000..9f80479a --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Extensions/ConsumerExtensions.cs @@ -0,0 +1,43 @@ +// Configuration: https://medium.com/@gparlakov/the-confusion-of-asp-net-configuration-with-environment-variables-c06c545ef732 + +namespace EventSourcing.Backbone.WebEventTest; + +/// +/// DI Extensions for ASP.NET Core +/// +public static class ConsumerExtensions +{ + /// + /// Adds the shipment tracking producer. + /// + /// The services. + /// The URI. + /// The s3 bucket. + /// The environment. + /// + public static IServiceCollection AddConsumer + ( + this IServiceCollection services, + string uri, + string s3Bucket, + Env env) + { + var s3Options = new S3Options { Bucket = s3Bucket }; + services.AddKeyedSingleton(ioc => + { + IConsumerReadyBuilder consumer = + ioc.ResolveRedisConsumerChannel() + .ResolveS3Storage(s3Options) + .WithOptions(o => o with + { + OriginFilter = MessageOrigin.Original, + AckBehavior = AckBehavior.OnSucceed + }) + .Environment(env) + .Uri(uri); + return consumer; + }, EventSourcingConstants.URI); + + return services; + } +} diff --git a/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Extensions/ProductCycle/ProductCycleProducerExtensions.cs b/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Extensions/ProductCycle/ProductCycleProducerExtensions.cs new file mode 100644 index 00000000..89238fbd --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/Extensions/ProductCycle/ProductCycleProducerExtensions.cs @@ -0,0 +1,40 @@ +// Configuration: https://medium.com/@gparlakov/the-confusion-of-asp-net-configuration-with-environment-variables-c06c545ef732 + +namespace EventSourcing.Backbone.WebEventTest; + +/// +/// DI Extensions for ASP.NET Core +/// +public static class ProductCycleProducerExtensions +{ + /// + /// Adds the shipment tracking producer. + /// + /// The services. + /// The URI. + /// The s3 bucket. + /// The environment. + /// + public static IServiceCollection AddProductCycleProducer + ( + this IServiceCollection services, + string uri, + string s3Bucket, + Env env) + { + var s3Options = new S3Options { Bucket = s3Bucket }; + services.AddSingleton(ioc => + { + ILogger logger = ioc.GetService>() ?? throw new EventSourcingException("Logger is missing"); + IProductCycleProducer producer = ioc.ResolveRedisProducerChannel() + .ResolveS3Storage(s3Options) + .Environment(env) + .Uri(uri) + .WithLogger(logger) + .BuildProductCycleProducer(); + return producer; + }); + + return services; + } +} diff --git a/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/IProductCycle.cs b/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/IProductCycle.cs new file mode 100644 index 00000000..fe68ae35 --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/IProductCycle.cs @@ -0,0 +1,22 @@ +#pragma warning disable S1133 // Deprecated code should be removed + + +namespace EventSourcing.Backbone.WebEventTest; + +/// +/// Event's schema definition +/// Return type of each method should be +/// +[EventsContract(EventsContractType.Producer)] +[EventsContract(EventsContractType.Consumer)] +[Obsolete("Either use the Producer or Consumer version of this interface", true)] +public interface IProductCycle +{ + ValueTask IdeaAsync(string title, string describe); + ValueTask PlanedAsync(string id, Version version, string doc); + ValueTask ReviewedAsync(string id, Version version, params string[] notes); + ValueTask ImplementedAsync(string id, Version version); + ValueTask TestedAsync(string id, Version version, params string[] notes); + ValueTask DeployedAsync(string id, Version version); + ValueTask RejectedAsync(string id, Version version, string operation, NextStage nextStage, params string[] notes); +} \ No newline at end of file diff --git a/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/NextStage.cs b/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/NextStage.cs new file mode 100644 index 00000000..03e9f1bc --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/ProductCycle/NextStage.cs @@ -0,0 +1,10 @@ +#pragma warning disable S1133 // Deprecated code should be removed + + +namespace EventSourcing.Backbone.WebEventTest; + +public enum NextStage +{ + Reject, + Abandon +} diff --git a/Tests/EventSourcing.Backbone.WebEventTest/Program.cs b/Tests/EventSourcing.Backbone.WebEventTest/Program.cs new file mode 100644 index 00000000..edea79e7 --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/Program.cs @@ -0,0 +1,112 @@ +using System.Text.Json; + +using Amazon.S3; + +using EventSourcing.Backbone.WebEventTest; +using EventSourcing.Backbone.WebEventTest.Jobs; + +using Microsoft.OpenApi.Models; + +using Refit; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDefaultAWSOptions(builder.Configuration.GetAWSOptions()); +builder.Services.AddAWSService(); + +IWebHostEnvironment environment = builder.Environment; +string env = environment.EnvironmentName; +string appName = environment.ApplicationName; + + +var services = builder.Services; +// Add services to the container. + + +services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +services.AddEndpointsApiExplorer(); +services.AddSwaggerGen( + opt => + { + opt.SupportNonNullableReferenceTypes(); + opt.IgnoreObsoleteProperties(); + opt.IgnoreObsoleteActions(); + opt.DescribeAllParametersInCamelCase(); + + opt.SwaggerDoc("v1", new OpenApiInfo + { + Version = "v1", + Title = "Environment setup", + Description = @"

Use the following docker in order to setup the environment

+

docker run -p 6379:6379 -it --rm --name redis-Json redislabs/rejson:latest

+

docker run --name jaeger-otel --rm -it -e COLLECTOR_OTLP_ENABLED=true -p 16686:16686 -p 4317:4317 -p 4318:4318 jaegertracing/all-in-one:latest

+", + }); + }); + +services.AddProductCycleProducer(EventSourcingConstants.URI, EventSourcingConstants.S3_BUCKET, env); +services.AddConsumer(EventSourcingConstants.URI, EventSourcingConstants.S3_BUCKET, env); + +services.AddHostedService(); +services.AddHostedService(); +services.AddHostedService(); +string fwPort = Environment.GetEnvironmentVariable("FW") ?? "MISSING-PORT"; + +var jsonOptions = new JsonSerializerOptions().WithDefault(); + +var refitSetting = new RefitSettings +{ + ContentSerializer = new SystemTextJsonContentSerializer(jsonOptions) +}; +services.AddRefitClient(refitSetting) // https://github.com/reactiveui/refit + .ConfigureHttpClient(c => + { + c.BaseAddress = new Uri($"http://localhost:{fwPort}/api/Migration"); + c.DefaultRequestHeaders.Add("wk-pattern", "migration"); + }); +services.AddHttpClient("migration", c => + { + c.BaseAddress = new Uri($"http://localhost:{fwPort}/api/Migration"); + c.DefaultRequestHeaders.Add("wk-pattern", "migration"); + }); + +//IConnectionMultiplexer redisConnection = services.AddRedis(); +builder.AddOpenTelemetryEventSourcing(); + +//services.AddOpenTelemetry(environment, shortAppName, redisConnection); + + +services.AddOptions(); // enable usage of IOptionsSnapshot dependency injection +//var redis = await RedisClientFactory.CreateProviderAsync(); +//services.AddSingleton(redis); +services.AddEventSourceRedisConnection(); +services.AddConsumer(EventSourcingConstants.URI, EventSourcingConstants.S3_BUCKET, env); +services.AddProductCycleProducer(EventSourcingConstants.URI, EventSourcingConstants.S3_BUCKET, env); +services.AddEventSource(env); + +//void RedisConfigEnrichment(ConfigurationOptions configuration) +//{ +// configuration.ReconnectRetryPolicy = new RedisReconnectRetryPolicy(); +// configuration.ClientName = "Web Test"; +//} +//services.AddSingleton>(RedisConfigEnrichment); + +services.AddControllers() + .WithJsonOptions(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseOpenTelemetryPrometheusScrapingEndpoint(); + +app.UseHttpsRedirection(); +app.MapControllers(); + +app.Run(); diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/Properties/launchSettings.json b/Tests/EventSourcing.Backbone.WebEventTest/Properties/launchSettings.json similarity index 100% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/Properties/launchSettings.json rename to Tests/EventSourcing.Backbone.WebEventTest/Properties/launchSettings.json diff --git a/Tests/EventSourcing.Backbone.WebEventTest/RedisReconnectRetryPolicy.cs b/Tests/EventSourcing.Backbone.WebEventTest/RedisReconnectRetryPolicy.cs new file mode 100644 index 00000000..3a9fcbf6 --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/RedisReconnectRetryPolicy.cs @@ -0,0 +1,23 @@ +using StackExchange.Redis; + +namespace EventSourcing.Backbone.WebEventTest; + +/// +/// Redis reconnect retry policy +/// +/// +public class RedisReconnectRetryPolicy : IReconnectRetryPolicy +{ + /// + /// Shoulds the retry. + /// + /// The current retry count. + /// The time elapsed milliseconds since last retry. + /// + bool IReconnectRetryPolicy.ShouldRetry( + long currentRetryCount, + int timeElapsedMillisecondsSinceLastRetry) + { + return timeElapsedMillisecondsSinceLastRetry > 1000; + } +} \ No newline at end of file diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/RegisterEventSourceExtensions.cs b/Tests/EventSourcing.Backbone.WebEventTest/RegisterEventSourceExtensions.cs similarity index 52% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/RegisterEventSourceExtensions.cs rename to Tests/EventSourcing.Backbone.WebEventTest/RegisterEventSourceExtensions.cs index 0406c81f..2765bbe4 100644 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/RegisterEventSourceExtensions.cs +++ b/Tests/EventSourcing.Backbone.WebEventTest/RegisterEventSourceExtensions.cs @@ -1,6 +1,6 @@ -using Weknow.EventSource.Backbone; +using EventSourcing.Backbone; -using Weknow.EventSource.Backbone.WebEventTest; +using EventSourcing.Backbone.WebEventTest; // Configuration: https://medium.com/@gparlakov/the-confusion-of-asp-net-configuration-with-environment-variables-c06c545ef732 @@ -8,32 +8,31 @@ namespace Microsoft.Extensions.Configuration { /// - /// Weknow core extensions for ASP.NET Core + /// core extensions for ASP.NET Core /// public static class RegisterEventSourceExtensions { - private const string PARTITION = "demo"; + private const string URI = "demo"; public static IServiceCollection AddEventSource( this IServiceCollection services, Env env) { + var s3Options = new S3Options { Bucket = "event-sourcing-web" }; services.AddSingleton(ioc => { - ILogger logger = ioc.GetService>() ?? throw new ArgumentNullException(); - IRawProducer producer = ProducerBuilder.Empty.UseRedisChannelInjection(ioc) - // .AddS3Strategy(new S3Options { EnvironmentConvension = S3EnvironmentConvention.BucketPrefix }) + IRawProducer producer = ioc.ResolveRedisProducerChannel() + .ResolveS3Storage(s3Options) .BuildRaw(); return producer; }); services.AddSingleton(ioc => { - ILogger logger = ioc.GetService>() ?? throw new ArgumentNullException(); - IEventFlowProducer producer = ProducerBuilder.Empty.UseRedisChannelInjection(ioc) - // .AddS3Strategy(new S3Options { EnvironmentConvension = S3EnvironmentConvention.BucketPrefix }) + ILogger logger = ioc.GetService>() ?? throw new EventSourcingException("Logger is missing"); + IEventFlowProducer producer = ioc.ResolveRedisProducerChannel() + .ResolveS3Storage(s3Options) .Environment(env) - .Partition(PARTITION) - .Shard("default") + .Uri(URI) .WithLogger(logger) .BuildEventFlowProducer(); return producer; @@ -41,26 +40,25 @@ public static IServiceCollection AddEventSource( services.AddSingleton(ioc => { IConsumerReadyBuilder consumer = - ConsumerBuilder.Empty.UseRedisChannelInjection(ioc) - // .AddS3Strategy(new S3Options { EnvironmentConvension = S3EnvironmentConvention.BucketPrefix }) + ioc.ResolveRedisConsumerChannel() + .ResolveS3Storage(s3Options) + // .AddS3Storage(new S3Options { EnvironmentConvension = S3EnvironmentConvention.BucketPrefix }) .WithOptions(o => o with { - TraceAsParent = TimeSpan.FromMinutes(10), OriginFilter = MessageOrigin.Original }) .Environment(env) - .Partition(PARTITION) - .Shard("default"); + .Uri(URI); return consumer; }); services.AddSingleton(ioc => { IConsumerHooksBuilder consumer = - ConsumerBuilder.Empty.UseRedisChannelInjection(ioc) - // .AddS3Strategy(new S3Options { EnvironmentConvension = S3EnvironmentConvention.BucketPrefix }) + ioc.ResolveRedisConsumerChannel() + .ResolveS3Storage(s3Options) + // .AddS3Storage(new S3Options { EnvironmentConvension = S3EnvironmentConvention.BucketPrefix }) .WithOptions(o => o with { - TraceAsParent = TimeSpan.FromMinutes(10), OriginFilter = MessageOrigin.Original }); return consumer; diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/TestSampler.cs b/Tests/EventSourcing.Backbone.WebEventTest/TestSampler.cs similarity index 100% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/TestSampler.cs rename to Tests/EventSourcing.Backbone.WebEventTest/TestSampler.cs diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/appsettings.Development.json b/Tests/EventSourcing.Backbone.WebEventTest/appsettings.Development.json similarity index 100% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/appsettings.Development.json rename to Tests/EventSourcing.Backbone.WebEventTest/appsettings.Development.json diff --git a/Tests/EventSourcing.Backbone.WebEventTest/appsettings.json b/Tests/EventSourcing.Backbone.WebEventTest/appsettings.json new file mode 100644 index 00000000..b689c6e4 --- /dev/null +++ b/Tests/EventSourcing.Backbone.WebEventTest/appsettings.json @@ -0,0 +1,14 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "AWS": { + "Region": "us-east-1", + "Profile": "playground" + } +} diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/azds.yaml b/Tests/EventSourcing.Backbone.WebEventTest/azds.yaml similarity index 74% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/azds.yaml rename to Tests/EventSourcing.Backbone.WebEventTest/azds.yaml index 819af4bd..2a513855 100644 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/azds.yaml +++ b/Tests/EventSourcing.Backbone.WebEventTest/azds.yaml @@ -4,7 +4,7 @@ build: context: ..\.. dockerfile: Dockerfile install: - chart: charts/weknoweventsourcebackbonewebeventtest + chart: charts/eventsourcebackbonewebeventtest values: - values.dev.yaml? - secrets.dev.yaml? @@ -20,17 +20,17 @@ install: # - name: myRegistryKeySecretName replicaCount: 1 image: - repository: weknoweventsourcebackbonewebeventtest + repository: eventsourcebackbonewebeventtest tag: $(tag) pullPolicy: Never ingress: annotations: kubernetes.io/ingress.class: traefik-azds hosts: - # This expands to form the service's public URL: [space.s.][rootSpace.]weknoweventsourcebackbonewebeventtest...azds.io - # Customize the public URL by changing the 'weknoweventsourcebackbonewebeventtest' text between the $(rootSpacePrefix) and $(hostSuffix) tokens + # This expands to form the service's public URL: [space.s.][rootSpace.]eventsourcebackbonewebeventtest...azds.io + # Customize the public URL by changing the 'eventsourcebackbonewebeventtest' text between the $(rootSpacePrefix) and $(hostSuffix) tokens # For more information see https://aka.ms/devspaces/routing - - $(spacePrefix)$(rootSpacePrefix)weknoweventsourcebackbonewebeventtest$(hostSuffix) + - $(spacePrefix)$(rootSpacePrefix)eventsourcebackbonewebeventtest$(hostSuffix) configurations: develop: build: @@ -46,6 +46,6 @@ configurations: - "!**/*.{sln,csproj}" command: [dotnet, run, --no-restore, --no-build, --no-launch-profile, -c, "${BUILD_CONFIGURATION:-Debug}"] iterate: - processesToKill: [dotnet, vsdbg, Weknow.EventSource.Backbone.WebEventTest] + processesToKill: [dotnet, vsdbg, .EventSourcing.Backbone.WebEventTest] buildCommands: - [dotnet, build, --no-restore, -c, "${BUILD_CONFIGURATION:-Debug}"] diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/.helmignore b/Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/.helmignore similarity index 100% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/.helmignore rename to Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/.helmignore diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/Chart.yaml b/Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/Chart.yaml similarity index 66% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/Chart.yaml rename to Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/Chart.yaml index b6dcbff9..a4a04851 100644 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/Chart.yaml +++ b/Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v1 appVersion: "1.0" description: A Helm chart for Kubernetes -name: weknoweventsourcebackbonewebeventtest +name: eventsourcebackbonewebeventtest version: 0.1.0 diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/NOTES.txt b/Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/NOTES.txt similarity index 73% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/NOTES.txt rename to Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/NOTES.txt index d13c4600..210bbe59 100644 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/NOTES.txt +++ b/Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/NOTES.txt @@ -4,16 +4,16 @@ http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} {{- end }} {{- else if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "weknoweventsourcebackbonewebeventtest.fullname" . }}) + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "eventsourcebackbonewebeventtest.fullname" . }}) export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") echo http://$NODE_IP:$NODE_PORT {{- else if contains "LoadBalancer" .Values.service.type }} NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch the status of by running 'kubectl get svc -w {{ template "weknoweventsourcebackbonewebeventtest.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "weknoweventsourcebackbonewebeventtest.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + You can watch the status of by running 'kubectl get svc -w {{ template "eventsourcebackbonewebeventtest.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "eventsourcebackbonewebeventtest.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') echo http://$SERVICE_IP:{{ .Values.service.port }} {{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "weknoweventsourcebackbonewebeventtest.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "eventsourcebackbonewebeventtest.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") echo "Visit http://127.0.0.1:8080 to use your application" kubectl port-forward $POD_NAME 8080:80 {{- end }} diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/_helpers.tpl b/Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/_helpers.tpl similarity index 83% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/_helpers.tpl rename to Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/_helpers.tpl index e534ff90..2242ea66 100644 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/_helpers.tpl +++ b/Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/_helpers.tpl @@ -2,7 +2,7 @@ {{/* Expand the name of the chart. */}} -{{- define "weknoweventsourcebackbonewebeventtest.name" -}} +{{- define "eventsourcebackbonewebeventtest.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} {{- end -}} @@ -11,7 +11,7 @@ Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} -{{- define "weknoweventsourcebackbonewebeventtest.fullname" -}} +{{- define "eventsourcebackbonewebeventtest.fullname" -}} {{- if .Values.fullnameOverride -}} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} {{- else -}} @@ -27,6 +27,6 @@ If release name contains chart name it will be used as a full name. {{/* Create chart name and version as used by the chart label. */}} -{{- define "weknoweventsourcebackbonewebeventtest.chart" -}} +{{- define "eventsourcebackbonewebeventtest.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} {{- end -}} diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/deployment.yaml b/Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/deployment.yaml similarity index 79% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/deployment.yaml rename to Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/deployment.yaml index f2989b75..912068f2 100644 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/deployment.yaml +++ b/Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/deployment.yaml @@ -1,10 +1,10 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ template "weknoweventsourcebackbonewebeventtest.fullname" . }} + name: {{ template "eventsourcebackbonewebeventtest.fullname" . }} labels: - app: {{ template "weknoweventsourcebackbonewebeventtest.name" . }} - chart: {{ template "weknoweventsourcebackbonewebeventtest.chart" . }} + app: {{ template "eventsourcebackbonewebeventtest.name" . }} + chart: {{ template "eventsourcebackbonewebeventtest.chart" . }} draft: {{ .Values.draft | default "draft-app" }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} @@ -13,12 +13,12 @@ spec: replicas: {{ .Values.replicaCount }} selector: matchLabels: - app: {{ template "weknoweventsourcebackbonewebeventtest.name" . }} + app: {{ template "eventsourcebackbonewebeventtest.name" . }} release: {{ .Release.Name }} template: metadata: labels: - app: {{ template "weknoweventsourcebackbonewebeventtest.name" . }} + app: {{ template "eventsourcebackbonewebeventtest.name" . }} draft: {{ .Values.draft | default "draft-app" }} release: {{ .Release.Name }} annotations: @@ -49,7 +49,7 @@ spec: - name: {{ $ref }}_{{ $key }} valueFrom: secretKeyRef: - name: {{ template "weknoweventsourcebackbonewebeventtest.fullname" $root }}-{{ $ref | lower }} + name: {{ template "eventsourcebackbonewebeventtest.fullname" $root }}-{{ $ref | lower }} key: {{ $key }} {{- end }} {{- end }} diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/ingress.yaml b/Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/ingress.yaml similarity index 81% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/ingress.yaml rename to Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/ingress.yaml index 2a3fa054..db263a4b 100644 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/ingress.yaml +++ b/Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/ingress.yaml @@ -1,5 +1,5 @@ {{- if .Values.ingress.enabled -}} -{{- $fullName := include "weknoweventsourcebackbonewebeventtest.fullname" . -}} +{{- $fullName := include "eventsourcebackbonewebeventtest.fullname" . -}} {{- $servicePort := .Values.service.port -}} {{- $ingressPath := .Values.ingress.path -}} {{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} @@ -11,8 +11,8 @@ kind: Ingress metadata: name: {{ $fullName }} labels: - app: {{ template "weknoweventsourcebackbonewebeventtest.name" . }} - chart: {{ template "weknoweventsourcebackbonewebeventtest.chart" . }} + app: {{ template "eventsourcebackbonewebeventtest.name" . }} + chart: {{ template "eventsourcebackbonewebeventtest.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} {{- with .Values.ingress.annotations }} diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/secrets.yaml b/Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/secrets.yaml similarity index 68% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/secrets.yaml rename to Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/secrets.yaml index e030e892..a59c70bd 100644 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/secrets.yaml +++ b/Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/secrets.yaml @@ -3,7 +3,7 @@ apiVersion: v1 kind: Secret metadata: - name: {{ template "weknoweventsourcebackbonewebeventtest.fullname" $root }}-{{ $name | lower }} + name: {{ template "eventsourcebackbonewebeventtest.fullname" $root }}-{{ $name | lower }} data: {{- range $key, $value := $values }} {{ $key }}: {{ $value | b64enc }} diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/service.yaml b/Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/service.yaml similarity index 51% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/service.yaml rename to Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/service.yaml index 010f382a..1423ff07 100644 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/service.yaml +++ b/Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/templates/service.yaml @@ -1,10 +1,10 @@ apiVersion: v1 kind: Service metadata: - name: {{ template "weknoweventsourcebackbonewebeventtest.fullname" . }} + name: {{ template "eventsourcebackbonewebeventtest.fullname" . }} labels: - app: {{ template "weknoweventsourcebackbonewebeventtest.name" . }} - chart: {{ template "weknoweventsourcebackbonewebeventtest.chart" . }} + app: {{ template "eventsourcebackbonewebeventtest.name" . }} + chart: {{ template "eventsourcebackbonewebeventtest.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: @@ -15,5 +15,5 @@ spec: protocol: TCP name: http selector: - app: {{ template "weknoweventsourcebackbonewebeventtest.name" . }} + app: {{ template "eventsourcebackbonewebeventtest.name" . }} release: {{ .Release.Name }} diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/values.yaml b/Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/values.yaml similarity index 91% rename from Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/values.yaml rename to Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/values.yaml index d6a75a1d..320adc5b 100644 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/values.yaml +++ b/Tests/EventSourcing.Backbone.WebEventTest/charts/weknoweventsourcebackbonewebeventtest/values.yaml @@ -1,10 +1,10 @@ -# Default values for weknoweventsourcebackbonewebeventtest. +# Default values for eventsourcebackbonewebeventtest. # This is a YAML-formatted file. # Declare variables to be passed into your templates. -fullnameOverride: weknoweventsourcebackbonewebeventtest +fullnameOverride: eventsourcebackbonewebeventtest replicaCount: 1 image: - repository: weknoweventsourcebackbonewebeventtest + repository: eventsourcebackbonewebeventtest tag: stable pullPolicy: IfNotPresent imagePullSecrets: [] diff --git a/Tests/EventSourcing.Backbone.WebEventTest/icon.png b/Tests/EventSourcing.Backbone.WebEventTest/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/Tests/EventSourcing.Backbone.WebEventTest/icon.png differ diff --git a/Tests/HelloWorld/ConsumingEvents/ConsumingEvents.csproj b/Tests/HelloWorld/ConsumingEvents/ConsumingEvents.csproj new file mode 100644 index 00000000..ff03128c --- /dev/null +++ b/Tests/HelloWorld/ConsumingEvents/ConsumingEvents.csproj @@ -0,0 +1,23 @@ + + + + Exe + false + + + + + + + + + + + True + + + + + + + diff --git a/Tests/HelloWorld/ConsumingEvents/Program.cs b/Tests/HelloWorld/ConsumingEvents/Program.cs new file mode 100644 index 00000000..b7495500 --- /dev/null +++ b/Tests/HelloWorld/ConsumingEvents/Program.cs @@ -0,0 +1,35 @@ +using EventsAbstractions; + +using EventSourcing.Backbone; + +Console.WriteLine("Consuming Events"); + +IConsumerLifetime subscription = RedisConsumerBuilder.Create() + .Uri(URIs.Default) + //.Group("sample.hello-world") + .SubscribeHelloEventsConsumer(Subscription.Instance); +Console.ReadKey(false); + +class Subscription : IHelloEventsConsumer +{ + public static readonly Subscription Instance = new Subscription(); + public ValueTask NameAsync(ConsumerMetadata meta, string name) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(); + Console.Write($"Hello {name}: "); + return ValueTask.CompletedTask; + } + + public ValueTask ColorAsync(ConsumerMetadata meta, ConsoleColor color) + { + Console.ForegroundColor = color; + return ValueTask.CompletedTask; + } + + public ValueTask StarAsync(ConsumerMetadata meta) + { + Console.Write("✱"); + return ValueTask.CompletedTask; + } +} diff --git a/Tests/HelloWorld/ConsumingEvents/icon.png b/Tests/HelloWorld/ConsumingEvents/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/Tests/HelloWorld/ConsumingEvents/icon.png differ diff --git a/Tests/HelloWorld/EventsAbstractions/EventsAbstractions.csproj b/Tests/HelloWorld/EventsAbstractions/EventsAbstractions.csproj new file mode 100644 index 00000000..e7f59b96 --- /dev/null +++ b/Tests/HelloWorld/EventsAbstractions/EventsAbstractions.csproj @@ -0,0 +1,24 @@ + + + false + + + + + + + + + + + + + + True + + + + + + + diff --git a/Tests/HelloWorld/EventsAbstractions/IHelloEvents.cs b/Tests/HelloWorld/EventsAbstractions/IHelloEvents.cs new file mode 100644 index 00000000..4e23e044 --- /dev/null +++ b/Tests/HelloWorld/EventsAbstractions/IHelloEvents.cs @@ -0,0 +1,17 @@ +using EventSourcing.Backbone; + +namespace EventsAbstractions; + + +/// +/// Event's schema definition +/// Return type of each method should be +/// +[EventsContract(EventsContractType.Producer)] +[EventsContract(EventsContractType.Consumer)] +public interface IHelloEvents +{ + ValueTask NameAsync(string name); + ValueTask ColorAsync(ConsoleColor color); + ValueTask StarAsync(); +} \ No newline at end of file diff --git a/Tests/HelloWorld/EventsAbstractions/URIs.cs b/Tests/HelloWorld/EventsAbstractions/URIs.cs new file mode 100644 index 00000000..e6d816a3 --- /dev/null +++ b/Tests/HelloWorld/EventsAbstractions/URIs.cs @@ -0,0 +1,7 @@ +namespace EventsAbstractions +{ + public class URIs + { + public const string Default = "hello.event-sourcing"; + } +} diff --git a/Tests/HelloWorld/EventsAbstractions/icon.png b/Tests/HelloWorld/EventsAbstractions/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/Tests/HelloWorld/EventsAbstractions/icon.png differ diff --git a/Tests/HelloWorld/ProducingEvents/ProducingEvents.csproj b/Tests/HelloWorld/ProducingEvents/ProducingEvents.csproj new file mode 100644 index 00000000..d02edc28 --- /dev/null +++ b/Tests/HelloWorld/ProducingEvents/ProducingEvents.csproj @@ -0,0 +1,23 @@ + + + + Exe + false + + + + + + + + + + + True + + + + + + + diff --git a/Tests/HelloWorld/ProducingEvents/Program.cs b/Tests/HelloWorld/ProducingEvents/Program.cs new file mode 100644 index 00000000..418dafc0 --- /dev/null +++ b/Tests/HelloWorld/ProducingEvents/Program.cs @@ -0,0 +1,52 @@ +using EventsAbstractions; + +using EventSourcing.Backbone; +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. + +Console.WriteLine("Producing events"); + +IHelloEventsProducer producer = RedisProducerBuilder.Create() + .Uri(URIs.Default) + .BuildHelloEventsProducer(); + + +Console.Write("What is your name? "); +string name = Console.ReadLine(); +await producer.NameAsync(name ?? "Unknown"); + +var rnd = new Random(Guid.NewGuid().GetHashCode()); +Console.WriteLine("Press Esc to exit"); +Console.Write("Press Number for delay: "); +Console.WriteLine("1 is ms, 2 is 10 ms, 3 is 100 ms, 4 is 1s, 5 is 10s, 6 is 30s, 7 is 1m"); + + +var colors = Enum.GetValues() ?? Array.Empty(); +while (!Console.KeyAvailable || Console.ReadKey(true).Key == ConsoleKey.Escape) +{ + int index = Environment.TickCount % colors.Length; + var color = colors[index]; + await producer.ColorAsync(color); + await producer.StarAsync(); + + ConsoleKey press = Console.KeyAvailable ? Console.ReadKey(true).Key : ConsoleKey.Clear; + int delay = press switch + { + ConsoleKey.D1 => 1, + ConsoleKey.D2 => 10, + ConsoleKey.D3 => 100, + ConsoleKey.D4 => 1_000, + ConsoleKey.D5 => 10_000, + ConsoleKey.D6 => 30_000, + ConsoleKey.D7 => 60_000, + ConsoleKey.Escape => -1, + _ => 0 + }; + if (delay == -1) + break; + if (delay != 0) + await Task.Delay(delay); + Console.ForegroundColor = color; + Console.Write("☆"); +} + +Console.WriteLine(" Done"); \ No newline at end of file diff --git a/Tests/HelloWorld/ProducingEvents/icon.png b/Tests/HelloWorld/ProducingEvents/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/Tests/HelloWorld/ProducingEvents/icon.png differ diff --git a/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Controllers/ProductCycleConsumerController.cs b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Controllers/ProductCycleConsumerController.cs new file mode 100644 index 00000000..e5bdac33 --- /dev/null +++ b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Controllers/ProductCycleConsumerController.cs @@ -0,0 +1,40 @@ +using System.Text.Json; + +using EventSourcing.Backbone; + +using Microsoft.AspNetCore.Mvc; + +using Tests.Events.WebTest.Abstractions; + +namespace Tests.Events.ConsumerWebTest.Controllers; + +[ApiController] +[Route("[controller]")] +public class ProductCycleConsumerController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IConsumerReceiver _receiver; + + public ProductCycleConsumerController( + ILogger logger, + IKeyed consumerBuilderKeyed) + { + _logger = logger; + if (!consumerBuilderKeyed.TryGet(ProductCycleConstants.URI, out var consumerBuilder)) + throw new EventSourcingException($"The Consumer's registration found under the [{ProductCycleConstants.URI}] key."); + _receiver = consumerBuilder.BuildReceiver(); + } + + /// + /// Gets an event by event key. + /// + /// The event key. + /// + [HttpGet("{eventKey}")] + public async Task GetAsync(string eventKey) + { + _logger.LogDebug("fetching event [{key}]", eventKey); + var json = await _receiver.GetJsonByIdAsync(eventKey); + return json; + } +} \ No newline at end of file diff --git a/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Dockerfile b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Dockerfile new file mode 100644 index 00000000..71b11d4c --- /dev/null +++ b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Dockerfile @@ -0,0 +1,22 @@ +#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +WORKDIR /src +COPY ["Tests.Events.ConsumerWebTest/Tests.Events.ConsumerWebTest.csproj", "Tests.Events.ConsumerWebTest/"] +RUN dotnet restore "Tests.Events.ConsumerWebTest/Tests.Events.ConsumerWebTest.csproj" +COPY . . +WORKDIR "/src/Tests.Events.ConsumerWebTest" +RUN dotnet build "Tests.Events.ConsumerWebTest.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Tests.Events.ConsumerWebTest.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Tests.Events.ConsumerWebTest.dll"] \ No newline at end of file diff --git a/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Extensions/ConsumerExtensions.cs b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Extensions/ConsumerExtensions.cs new file mode 100644 index 00000000..111be172 --- /dev/null +++ b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Extensions/ConsumerExtensions.cs @@ -0,0 +1,81 @@ +using EventSourcing.Backbone; + +// Configuration: https://medium.com/@gparlakov/the-confusion-of-asp-net-configuration-with-environment-variables-c06c545ef732 + +namespace Tests.Events.ConsumerWebTest; + +/// +/// DI Extensions for ASP.NET Core +/// +public static class ConsumerExtensions +{ + /// + /// Register a consumer. + /// + /// The builder. + /// The URI. + /// + public static WebApplicationBuilder AddConsumer( + this WebApplicationBuilder builder, + string uri) + { + IServiceCollection services = builder.Services; + IWebHostEnvironment environment = builder.Environment; + string env = environment.EnvironmentName; + + services.AddSingleton(ioc => + { + return BuildConsumer(uri, env, ioc + ); + }); + + return builder; + } + + /// + /// Register a consumer when the URI of the service used as the registration's key. + /// See: https://medium.com/weknow-network/keyed-dependency-injection-using-net-630bd73d3672 + /// + /// The builder. + /// The URI of the stream (which is also used as the DI key). + /// + public static WebApplicationBuilder AddKeyedConsumer( + this WebApplicationBuilder builder, + string uri) + { + IServiceCollection services = builder.Services; + IWebHostEnvironment environment = builder.Environment; + string env = environment.EnvironmentName; + + services.AddKeyedSingleton(ioc => + { + return BuildConsumer(uri + , env + , ioc + ); + }, uri); + + return builder; + } + + /// + /// Builds the consumer. + /// + /// The URI. + /// The environment. + /// The DI provider. + /// + private static IConsumerReadyBuilder BuildConsumer(string uri + , Env env, IServiceProvider ioc + ) + { + return ioc.ResolveRedisConsumerChannel() + .WithOptions(o => o with + { + OriginFilter = MessageOrigin.Original, + AckBehavior = AckBehavior.OnSucceed + }) + .Environment(env) + .Uri(uri); + } +} diff --git a/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Jobs/ProductCycleConsumerJob.cs b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Jobs/ProductCycleConsumerJob.cs new file mode 100644 index 00000000..ece3a433 --- /dev/null +++ b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Jobs/ProductCycleConsumerJob.cs @@ -0,0 +1,167 @@ +using EventSourcing.Backbone; +using EventSourcing.Backbone.Building; + +using Tests.Events.WebTest.Abstractions; + +namespace Tests.Events.ConsumerWebTest.Controllers; + +/// +/// Consumer job +/// +/// +/// +/// +public sealed class ConsumerJob : IHostedService, IProductCycleConsumer +{ + private readonly IConsumerSubscribeBuilder _builder; + private CancellationTokenSource? _cancellationTokenSource; + private IConsumerLifetime? _subscription; + + private readonly ILogger _logger; + + #region Ctor + + /// + /// Initializes a new instance. + /// + /// The logger. + /// The consumer builder. + public ConsumerJob( + ILogger logger, + IKeyed consumerBuilderKeyed) + { + if (!consumerBuilderKeyed.TryGet(ProductCycleConstants.URI, out var consumerBuilder)) + throw new EventSourcingException($"Consumer's registration found under the [{ProductCycleConstants.URI}] key."); + _builder = consumerBuilder.WithLogger(logger); + _logger = logger; + logger.LogInformation("Consumer starts listening on: {URI}", ProductCycleConstants.URI); + } + + #endregion Ctor + + #region OnStartAsync + + /// + /// Start Consumer Job. + /// + /// The cancellation token. + Task IHostedService.StartAsync(CancellationToken cancellationToken) + { + _cancellationTokenSource = new CancellationTokenSource(); + var canellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token); + _subscription = _builder + .Group(ProductCycleConstants.CONSUMER_GROUP) + .WithCancellation(canellation.Token) + // this extension is generate (if you change the interface use the correlated new generated extension method) + .SubscribeProductCycleConsumer(this); + + return Task.CompletedTask; + } + + #endregion // OnStartAsync + + #region StopAsync + + /// + /// Stops the Consumer Job. + /// + /// The cancellation token. + /// + async Task IHostedService.StopAsync(CancellationToken cancellationToken) + { + _cancellationTokenSource?.CancelSafe(); + await (_subscription?.Completion ?? Task.CompletedTask); + } + + #endregion // StopAsync + + // TODO: enrich telemetry + + async ValueTask IProductCycleConsumer.IdeaAsync(ConsumerMetadata consumerMetadata, string title, string describe) + { + var meta = consumerMetadata.Metadata; + LogLevel level = meta.Environment == "prod" ? LogLevel.Debug : LogLevel.Information; + _logger.Log(level, "handling {event} [{id}]: {title}", meta.Operation, meta.MessageId, title); + await consumerMetadata.AckAsync(); // not required on default setting or when configuring the consumer to Ack on success. + } + + async ValueTask IProductCycleConsumer.PlanedAsync(ConsumerMetadata consumerMetadata, string id, Version version, string doc) + { + var meta = consumerMetadata.Metadata; + LogLevel level = meta.Environment == "prod" ? LogLevel.Debug : LogLevel.Information; + _logger.Log(level, """ + handling {event} [{id}]: {version} + --- + {doc} + """, meta.Operation, id, version, doc); + + await consumerMetadata.AckAsync(); // not required on default setting or when configuring the consumer to Ack on success. + } + + async ValueTask IProductCycleConsumer.ReviewedAsync(ConsumerMetadata consumerMetadata, string id, Version version, string[] notes) + { + var meta = consumerMetadata.Metadata; + LogLevel level = meta.Environment == "prod" ? LogLevel.Debug : LogLevel.Information; + _logger.Log(level, """ + handling {event} [{id}]: {version} + --- + {notes} + """, meta.Operation, id, version, string.Join("\r\n- ", notes)); + + await consumerMetadata.AckAsync(); // not required on default setting or when configuring the consumer to Ack on success. + } + + async ValueTask IProductCycleConsumer.ImplementedAsync(ConsumerMetadata consumerMetadata, string id, Version version) + { + var meta = consumerMetadata.Metadata; + LogLevel level = meta.Environment == "prod" ? LogLevel.Debug : LogLevel.Information; + _logger.Log(level, """ + handling {event} [{id}]: {version} + """, meta.Operation, id, version); + await consumerMetadata.AckAsync(); // not required on default setting or when configuring the consumer to Ack on success. + } + + async ValueTask IProductCycleConsumer.TestedAsync(ConsumerMetadata consumerMetadata, string id, Version version, string[] notes) + { + var meta = consumerMetadata.Metadata; + LogLevel level = meta.Environment == "prod" ? LogLevel.Debug : LogLevel.Information; + _logger.Log(level, """ + handling {event} [{id}]: {version} + --- + {notes} + """, meta.Operation, id, version, string.Join("\r\n- ", notes)); + + await consumerMetadata.AckAsync(); // not required on default setting or when configuring the consumer to Ack on success. + } + + async ValueTask IProductCycleConsumer.DeployedAsync(ConsumerMetadata consumerMetadata, string id, Version version) + { + var meta = consumerMetadata.Metadata; + LogLevel level = meta.Environment == "prod" ? LogLevel.Debug : LogLevel.Information; + _logger.Log(level, "handling {event} [{id}]: {version}", meta.Operation, id, version); + + await consumerMetadata.AckAsync(); // not required on default setting or when configuring the consumer to Ack on success. + } + + async ValueTask IProductCycleConsumer.RejectedAsync(ConsumerMetadata consumerMetadata, + string id, + System.Version version, + string operation, + NextStage nextStage, + string[] notes) + { + var meta = consumerMetadata.Metadata; + LogLevel level = meta.Environment == "prod" ? LogLevel.Debug : LogLevel.Information; + _logger.Log(level, """ + handling {event} [{id}]: {version} + --- + operation = {operation} + + {notes} + --- + """, meta.Operation, id, version, operation, string.Join("\r\n- ", notes)); + + await consumerMetadata.AckAsync(); // not required on default setting or when configuring the consumer to Ack on success. + } +} + diff --git a/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Program.cs b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Program.cs new file mode 100644 index 00000000..a9eaeb69 --- /dev/null +++ b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Program.cs @@ -0,0 +1,69 @@ +using Microsoft.OpenApi.Models; + +using Tests.Events.ConsumerWebTest; +using Tests.Events.ConsumerWebTest.Controllers; +using Tests.Events.WebTest.Abstractions; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +// ############### EVENT SOURCING CONFIGURATION STARTS ############################ + +var services = builder.Services; + + +IWebHostEnvironment environment = builder.Environment; +services.AddOpenTelemetry() + .WithEventSourcingTracing(environment) + .WithEventSourcingMetrics(environment); + +services.AddEventSourceRedisConnection(); +builder.AddKeyedConsumer(ProductCycleConstants.URI); + +services.AddHostedService(); + + +// ############### EVENT SOURCING CONFIGURATION ENDS ############################ + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen( + opt => + { + opt.SupportNonNullableReferenceTypes(); + opt.IgnoreObsoleteProperties(); + opt.IgnoreObsoleteActions(); + opt.DescribeAllParametersInCamelCase(); + + opt.SwaggerDoc("v1", new OpenApiInfo + { + Version = "v1", + Title = "Environment setup", + Description = @"

Use the following docker in order to setup the environment

+

docker run -p 6379:6379 -it --rm --name redis-Json redislabs/rejson:latest

+

docker run --name jaeger-otel --rm -it -e COLLECTOR_OTLP_ENABLED=true -p 16686:16686 -p 4317:4317 -p 4318:4318 jaegertracing/all-in-one:latest

+", + }); + }); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +var logger = app.Services.GetService>(); +List switches = new(); +switches.Add("Consumer"); +logger?.LogInformation("Service Configuration Event Sourcing `{event-bundle}` on URI: `{URI}`, Features: [{features}]", "ProductCycle", ProductCycleConstants.URI, string.Join(", ", switches)); + +app.Run(); diff --git a/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Properties/launchSettings.json b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Properties/launchSettings.json new file mode 100644 index 00000000..b5fe3370 --- /dev/null +++ b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Properties/launchSettings.json @@ -0,0 +1,48 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5238" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7138;http://localhost:5238" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "publishAllPorts": true, + "useSSL": true + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:33798", + "sslPort": 0 + } + } +} \ No newline at end of file diff --git a/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Tests - Backup.Events.ConsumerWebTest.Service.csproj b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Tests - Backup.Events.ConsumerWebTest.Service.csproj new file mode 100644 index 00000000..4fc3777d --- /dev/null +++ b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Tests - Backup.Events.ConsumerWebTest.Service.csproj @@ -0,0 +1,30 @@ + + + + 8e0c42af-b506-4d2b-bc6e-0566a6701529 + Linux + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Tests.Events.ConsumerWebTest.Service.csproj b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Tests.Events.ConsumerWebTest.Service.csproj new file mode 100644 index 00000000..5bc4abe2 --- /dev/null +++ b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/Tests.Events.ConsumerWebTest.Service.csproj @@ -0,0 +1,44 @@ + + + Debug;Release;Gen + false + + + + 8e0c42af-b506-4d2b-bc6e-0566a6701529 + Linux + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + + + + + + + diff --git a/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/appsettings.Development.json b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/appsettings.json b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/icon.png b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/Tests/Specialized/Tests.Events.ConsumerWebTest.Service/icon.png differ diff --git a/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Controllers/ProductCycleProducerController.cs b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Controllers/ProductCycleProducerController.cs new file mode 100644 index 00000000..f489a817 --- /dev/null +++ b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Controllers/ProductCycleProducerController.cs @@ -0,0 +1,124 @@ +using EventSourcing.Backbone; + +using Microsoft.AspNetCore.Mvc; + +using Tests.Events.ProducerWebTest.Service.Entities; +using Tests.Events.WebTest.Abstractions; + +namespace Tests.Events.ProducerWebTest.Controllers; + +[ApiController] +[Route("[controller]")] +public class ProductCycleProducerController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IProductCycleProducer _producer; + + public ProductCycleProducerController( + ILogger logger, + IKeyed producerKeyed) + { + _logger = logger; + if (!producerKeyed.TryGet(ProductCycleConstants.URI, out var producer)) + throw new EventSourcingException($"Producer's registration found under the [{ProductCycleConstants.URI}] key."); + _producer = producer; + } + + /// + /// Post order state. + /// + /// The payload. + /// + [HttpPost("idea")] + [ProducesResponseType(StatusCodes.Status201Created)] + //[AllowAnonymous] + public async Task IdeaAsync(Idea payload) + { + var (title, describe) = payload; + _logger.LogDebug("Sending idea event"); + EventKey key = await _producer.IdeaAsync(title, describe); + return key; + } + + /// + /// Post packing state. + /// + /// The payload. + /// + [HttpPost("plan")] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task PlanAsync([FromBody] Plan payload) + { + var (id_, describe) = payload; + var (id, version) = id_; + _logger.LogDebug("Sending plan event"); + EventKey key = await _producer.PlanedAsync(id, version, describe); + return key; + } + + /// + /// Post on-delivery state. + /// + /// The payload. + /// + [HttpPost("review")] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task ReviewAsync([FromBody] Review payload) + { + var (id_, notes) = payload; + var (id, version) = id_; + + _logger.LogDebug("Sending review event"); + EventKey key = await _producer.ReviewedAsync(id, version, notes); + return key; + } + + /// + /// Post on-received state. + /// + /// The payload. + /// + [HttpPost("implement")] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task ImplementAsync([FromBody] Id payload) + { + var (id, version) = payload; + + _logger.LogDebug("Sending implement event"); + EventKey key = await _producer.ImplementedAsync(id, version); + return key; + } + + /// + /// Post on-received state. + /// + /// The payload. + /// + [HttpPost("test")] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task TestAsync([FromBody] Test payload) + { + var (id_, notes) = payload; + var (id, version) = id_; + + _logger.LogDebug("Sending test event"); + EventKey key = await _producer.TestedAsync(id, version, notes); + return key; + } + + /// + /// Post on-received state. + /// + /// The payload. + /// + [HttpPost("Deploy")] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task DeployAsync([FromBody] Id payload) + { + var (id, version) = payload; + + _logger.LogDebug("Sending deploy event"); + EventKey key = await _producer.DeployedAsync(id, version); + return key; + } +} \ No newline at end of file diff --git a/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Dockerfile b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Dockerfile new file mode 100644 index 00000000..5ed32258 --- /dev/null +++ b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Dockerfile @@ -0,0 +1,22 @@ +#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +WORKDIR /src +COPY ["Tests.Events.ProducerWebTest/Tests.Events.ProducerWebTest.csproj", "Tests.Events.ProducerWebTest/"] +RUN dotnet restore "Tests.Events.ProducerWebTest/Tests.Events.ProducerWebTest.csproj" +COPY . . +WORKDIR "/src/Tests.Events.ProducerWebTest" +RUN dotnet build "Tests.Events.ProducerWebTest.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Tests.Events.ProducerWebTest.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Tests.Events.ProducerWebTest.dll"] \ No newline at end of file diff --git a/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Entities/Id.cs b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Entities/Id.cs new file mode 100644 index 00000000..01f24bdd --- /dev/null +++ b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Entities/Id.cs @@ -0,0 +1,3 @@ +namespace Tests.Events.ProducerWebTest.Service.Entities; + +public record Id(string id, Version version); \ No newline at end of file diff --git a/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Entities/Idea.cs b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Entities/Idea.cs new file mode 100644 index 00000000..b8976d75 --- /dev/null +++ b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Entities/Idea.cs @@ -0,0 +1,3 @@ +namespace Tests.Events.ProducerWebTest.Service.Entities; + +public record Idea(string title, string describe); diff --git a/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Entities/Plan.cs b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Entities/Plan.cs new file mode 100644 index 00000000..1c735c79 --- /dev/null +++ b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Entities/Plan.cs @@ -0,0 +1,3 @@ +namespace Tests.Events.ProducerWebTest.Service.Entities; + +public record Plan(Id id, string describe); diff --git a/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Entities/Review.cs b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Entities/Review.cs new file mode 100644 index 00000000..0874c21e --- /dev/null +++ b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Entities/Review.cs @@ -0,0 +1,3 @@ +namespace Tests.Events.ProducerWebTest.Service.Entities; + +public record Review(Id id, params string[] notes); diff --git a/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Entities/Test.cs b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Entities/Test.cs new file mode 100644 index 00000000..bcecae2b --- /dev/null +++ b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Entities/Test.cs @@ -0,0 +1,3 @@ +namespace Tests.Events.ProducerWebTest.Service.Entities; + +public record Test(Id id, params string[] notes); diff --git a/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Extensions/ProductCycle/ProductCycleProducerExtensions.cs b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Extensions/ProductCycle/ProductCycleProducerExtensions.cs new file mode 100644 index 00000000..bdb7bdef --- /dev/null +++ b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Extensions/ProductCycle/ProductCycleProducerExtensions.cs @@ -0,0 +1,73 @@ +using EventSourcing.Backbone; + +using Tests.Events.WebTest.Abstractions; + +// Configuration: https://medium.com/@gparlakov/the-confusion-of-asp-net-configuration-with-environment-variables-c06c545ef732 + + +namespace Tests.Events.ProducerWebTest; + +/// +/// DI Extensions for ASP.NET Core +/// +public static class ProductCycleProducerExtensions +{ + /// + /// Register a producer. + /// + /// The builder. + /// The URI. + /// + public static WebApplicationBuilder AddProductCycleProducer( + this WebApplicationBuilder builder, + string uri) + { + IServiceCollection services = builder.Services; + IWebHostEnvironment environment = builder.Environment; + string env = environment.EnvironmentName; + + services.AddSingleton(ioc => + { + return BuildProducer(uri, env, ioc + ); + }); + + return builder; + } + + /// + /// Register a producer when the URI of the service used as the registration's key. + /// See: https://medium.com/weknow-network/keyed-dependency-injection-using-net-630bd73d3672. + /// + /// The builder. + /// The URI. + /// + public static WebApplicationBuilder AddKeyedProductCycleProducer( + this WebApplicationBuilder builder, + string uri) + { + IServiceCollection services = builder.Services; + IWebHostEnvironment environment = builder.Environment; + string env = environment.EnvironmentName; + + services.AddKeyedSingleton(ioc => + { + return BuildProducer(uri, env, ioc + ); + }, uri); + + return builder; + } + + private static IProductCycleProducer BuildProducer(string uri, Env env, IServiceProvider ioc + ) + { + ILogger logger = ioc.GetService>() ?? throw new EventSourcingException("Logger is missing"); + IProductCycleProducer producer = ioc.ResolveRedisProducerChannel() + .Environment(env) + .Uri(uri) + .WithLogger(logger) + .BuildProductCycleProducer(); + return producer; + } +} diff --git a/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Program.cs b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Program.cs new file mode 100644 index 00000000..a4db8db4 --- /dev/null +++ b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Program.cs @@ -0,0 +1,68 @@ +using Microsoft.OpenApi.Models; + +using Tests.Events.ProducerWebTest; +using Tests.Events.WebTest.Abstractions; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +// ############### EVENT SOURCING CONFIGURATION STARTS ############################ + +var services = builder.Services; + + +IWebHostEnvironment environment = builder.Environment; + +services.AddOpenTelemetry() + .WithEventSourcingTracing(environment) + .WithEventSourcingMetrics(environment); + +services.AddEventSourceRedisConnection(); +builder.AddKeyedProductCycleProducer(ProductCycleConstants.URI); + + + +// ############### EVENT SOURCING CONFIGURATION ENDS ############################ + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen( + opt => + { + opt.SupportNonNullableReferenceTypes(); + opt.IgnoreObsoleteProperties(); + opt.IgnoreObsoleteActions(); + opt.DescribeAllParametersInCamelCase(); + + opt.SwaggerDoc("v1", new OpenApiInfo + { + Version = "v1", + Title = "Environment setup", + Description = @"

Use the following docker in order to setup the environment

+

docker run -p 6379:6379 -it --rm --name redis-Json redislabs/rejson:latest

+

docker run --name jaeger-otel --rm -it -e COLLECTOR_OTLP_ENABLED=true -p 16686:16686 -p 4317:4317 -p 4318:4318 jaegertracing/all-in-one:latest

+", + }); + }); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +var logger = app.Services.GetService>(); +List switches = new(); +switches.Add("Producer"); +logger?.LogInformation("Service Configuration Event Sourcing `{event-bundle}` on URI: `{URI}`, Features: [{features}]", "ProductCycle", ProductCycleConstants.URI, string.Join(", ", switches)); + +app.Run(); diff --git a/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Properties/launchSettings.json b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Properties/launchSettings.json new file mode 100644 index 00000000..76a84d7d --- /dev/null +++ b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Properties/launchSettings.json @@ -0,0 +1,48 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5132" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7063;http://localhost:5132" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "publishAllPorts": true, + "useSSL": true + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:48432", + "sslPort": 0 + } + } +} \ No newline at end of file diff --git a/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Tests.Events.ProducerWebTest.Service.csproj b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Tests.Events.ProducerWebTest.Service.csproj new file mode 100644 index 00000000..c5655d0c --- /dev/null +++ b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/Tests.Events.ProducerWebTest.Service.csproj @@ -0,0 +1,47 @@ + + + Debug;Release;Gen + false + + + + 8c43cc25-10b6-412b-9772-94e1a069889a + Linux + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + + + + + + + diff --git a/Tests/Specialized/Tests.Events.ProducerWebTest.Service/appsettings.Development.json b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Tests/Specialized/Tests.Events.ProducerWebTest.Service/appsettings.json b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Tests/Specialized/Tests.Events.ProducerWebTest.Service/icon.png b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/Tests/Specialized/Tests.Events.ProducerWebTest.Service/icon.png differ diff --git a/Tests/Specialized/Tests.Events.WebTest.Abstractions/IProductCycle.cs b/Tests/Specialized/Tests.Events.WebTest.Abstractions/IProductCycle.cs new file mode 100644 index 00000000..e3cbc01a --- /dev/null +++ b/Tests/Specialized/Tests.Events.WebTest.Abstractions/IProductCycle.cs @@ -0,0 +1,23 @@ +#pragma warning disable S1133 // Deprecated code should be removed + +using EventSourcing.Backbone; + +namespace Tests.Events.WebTest.Abstractions; + +/// +/// Event's schema definition +/// Return type of each method should be +/// +[EventsContract(EventsContractType.Consumer)] +[EventsContract(EventsContractType.Producer)] +[Obsolete("Either use the Producer or Consumer version of this interface", true)] +public interface IProductCycle +{ + ValueTask IdeaAsync(string title, string describe); + ValueTask PlanedAsync(string id, Version version, string doc); + ValueTask ReviewedAsync(string id, Version version, params string[] notes); + ValueTask ImplementedAsync(string id, Version version); + ValueTask TestedAsync(string id, Version version, params string[] notes); + ValueTask DeployedAsync(string id, Version version); + ValueTask RejectedAsync(string id, Version version, string operation, NextStage nextStage, params string[] notes); +} \ No newline at end of file diff --git a/Tests/Specialized/Tests.Events.WebTest.Abstractions/NextStage.cs b/Tests/Specialized/Tests.Events.WebTest.Abstractions/NextStage.cs new file mode 100644 index 00000000..ee65c86c --- /dev/null +++ b/Tests/Specialized/Tests.Events.WebTest.Abstractions/NextStage.cs @@ -0,0 +1,7 @@ +namespace Tests.Events.WebTest.Abstractions; + +public enum NextStage +{ + Reject, + Abandon +} diff --git a/Tests/Specialized/Tests.Events.WebTest.Abstractions/ProductCycleConstants.cs b/Tests/Specialized/Tests.Events.WebTest.Abstractions/ProductCycleConstants.cs new file mode 100644 index 00000000..1b69cd17 --- /dev/null +++ b/Tests/Specialized/Tests.Events.WebTest.Abstractions/ProductCycleConstants.cs @@ -0,0 +1,10 @@ +namespace Tests.Events.WebTest.Abstractions; + +/// +/// Common constants +/// +public static class ProductCycleConstants +{ + public const string URI = "event-demo"; + public const string CONSUMER_GROUP = "main"; +} diff --git a/Tests/Specialized/Tests.Events.WebTest.Abstractions/Tests.Events.WebTest.Abstractions.csproj b/Tests/Specialized/Tests.Events.WebTest.Abstractions/Tests.Events.WebTest.Abstractions.csproj new file mode 100644 index 00000000..554e3e61 --- /dev/null +++ b/Tests/Specialized/Tests.Events.WebTest.Abstractions/Tests.Events.WebTest.Abstractions.csproj @@ -0,0 +1,22 @@ + + + Debug;Release;Gen + false + + + + + + + + + + + True + + + + + + + diff --git a/Tests/Specialized/Tests.Events.WebTest.Abstractions/icon.png b/Tests/Specialized/Tests.Events.WebTest.Abstractions/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/Tests/Specialized/Tests.Events.WebTest.Abstractions/icon.png differ diff --git a/Tests/WebSampleS3/Controllers/ConsumerController.cs b/Tests/WebSampleS3/Controllers/ConsumerController.cs new file mode 100644 index 00000000..c1a43d14 --- /dev/null +++ b/Tests/WebSampleS3/Controllers/ConsumerController.cs @@ -0,0 +1,36 @@ +using System.Text.Json; + +using EventSourcing.Backbone; + +using Microsoft.AspNetCore.Mvc; + +namespace WebSampleS3.Controllers; + +[ApiController] +[Route("[controller]")] +public class ConsumerController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IConsumerReceiver _receiver; + + public ConsumerController( + ILogger logger, + IConsumerReadyBuilder consumerBuilder) + { + _logger = logger; + _receiver = consumerBuilder.BuildReceiver(); + } + + /// + /// Gets an event by event key. + /// + /// The event key. + /// + [HttpGet("{eventKey}")] + public async Task GetAsync(string eventKey) + { + _logger.LogDebug("fetching event [{key}]", eventKey); + var json = await _receiver.GetJsonByIdAsync(eventKey); + return json; + } +} \ No newline at end of file diff --git a/Tests/WebSampleS3/Controllers/ProducerController.cs b/Tests/WebSampleS3/Controllers/ProducerController.cs new file mode 100644 index 00000000..280150d6 --- /dev/null +++ b/Tests/WebSampleS3/Controllers/ProducerController.cs @@ -0,0 +1,84 @@ +using EventSourcing.Backbone; + +using Microsoft.AspNetCore.Mvc; + +namespace WebSampleS3.Controllers; + +public record Ord(User user, Product Product); + +[ApiController] +[Route("[controller]")] +public class ProducerController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IShipmentTrackingProducer _producer; + + public ProducerController( + ILogger logger, + IShipmentTrackingProducer producer) + { + _logger = logger; + _producer = producer; + } + + /// + /// Post order state. + /// + /// The payload. + /// + [HttpPost("order-placed")] + [ProducesResponseType(StatusCodes.Status201Created)] + //[AllowAnonymous] + public async Task PostOrderPlacedAsync(Ord payload) + { + var (user, product) = payload; + _logger.LogDebug("Sending order-placed event"); + EventKey id = await _producer.OrderPlacedAsync(user, product, DateTimeOffset.Now); + return id; + } + + /// + /// Post packing state. + /// + /// The payload. + /// + [HttpPost("packing")] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task PostPackingAsync([FromBody] (string email, int productId) payload) + { + var (email, productId) = payload; + _logger.LogDebug("Sending packing event"); + EventKey id = await _producer.PackingAsync(email, productId, DateTimeOffset.Now); + return id; + } + + /// + /// Post on-delivery state. + /// + /// The payload. + /// + [HttpPost("on-delivery")] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task PostOnDeliveryAsync([FromBody] (string email, int productId) payload) + { + var (email, productId) = payload; + _logger.LogDebug("Sending on-delivery event"); + EventKey id = await _producer.OnDeliveryAsync(email, productId, DateTimeOffset.Now); + return id; + } + + /// + /// Post on-received state. + /// + /// The payload. + /// + [HttpPost("on-received")] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task PostOnReceivedAsync([FromBody] (string email, int productId) payload) + { + var (email, productId) = payload; + _logger.LogDebug("Sending on-received event"); + EventKey id = await _producer.OnReceivedAsync(email, productId, DateTimeOffset.Now); + return id; + } +} \ No newline at end of file diff --git a/Tests/WebSampleS3/Entities/Constants.cs b/Tests/WebSampleS3/Entities/Constants.cs new file mode 100644 index 00000000..0c5c4235 --- /dev/null +++ b/Tests/WebSampleS3/Entities/Constants.cs @@ -0,0 +1,35 @@ +using Amazon; +using Amazon.Runtime; +using Amazon.Runtime.CredentialManagement; +using Amazon.S3; + +using EventSourcing.Backbone; + +namespace WebSampleS3; + +public static class Constants +{ + public const string URI = "hello-event-sourcing"; + public const string S3_BUCKET = "event-sourcing-demo"; + + public static IAmazonS3 CreateS3Client(RegionEndpoint region, string profile = "playground") + { + var chain = new CredentialProfileStoreChain(); + AWSCredentials awsCredentials; + if (chain.TryGetAWSCredentials(profile, out awsCredentials)) + { + // Use awsCredentials to create an Amazon S3 service client + var client = new AmazonS3Client(awsCredentials, region); + return client; + } + + var s3Client = EventSourcing.Backbone.Channels.S3RepositoryFactory.CreateClient(); + return s3Client; + } + + public static readonly S3Options S3Options = new S3Options + { + Bucket = Constants.S3_BUCKET + }; + +} diff --git a/Tests/WebSampleS3/Entities/IShipmentTracking.cs b/Tests/WebSampleS3/Entities/IShipmentTracking.cs new file mode 100644 index 00000000..645a8610 --- /dev/null +++ b/Tests/WebSampleS3/Entities/IShipmentTracking.cs @@ -0,0 +1,22 @@ +#pragma warning disable S1133 // Deprecated code should be removed + +using EventSourcing.Backbone; + +namespace WebSampleS3; + + +/// +/// Event's schema definition +/// Return type of each method should be +/// +[EventsContract(EventsContractType.Producer)] +[EventsContract(EventsContractType.Consumer)] +[Obsolete("Either use the Producer or Consumer version of this interface", true)] +public interface IShipmentTracking +{ + // the following method will be ValueTask OrderPlacedAsync(...) at the producer interface and ValueTask OrderPlacedAsync(...) at the consumer + void OrderPlaced(User user, Product product, DateTimeOffset time); + ValueTask PackingAsync(string email, int productId, DateTimeOffset time); + ValueTask OnDeliveryAsync(string email, int productId, DateTimeOffset time); + ValueTask OnReceivedAsync(string email, int productId, DateTimeOffset time); +} \ No newline at end of file diff --git a/Tests/WebSampleS3/Entities/Product.cs b/Tests/WebSampleS3/Entities/Product.cs new file mode 100644 index 00000000..08d7313d --- /dev/null +++ b/Tests/WebSampleS3/Entities/Product.cs @@ -0,0 +1,3 @@ +namespace WebSampleS3; + +public record Product(int id, string name, double price); diff --git a/Tests/WebSampleS3/Entities/User.cs b/Tests/WebSampleS3/Entities/User.cs new file mode 100644 index 00000000..7f8d3d61 --- /dev/null +++ b/Tests/WebSampleS3/Entities/User.cs @@ -0,0 +1,3 @@ +namespace WebSampleS3; + +public record User(int id, string email, string name); diff --git a/Tests/WebSampleS3/Extensions/ConsumerExtensions.cs b/Tests/WebSampleS3/Extensions/ConsumerExtensions.cs new file mode 100644 index 00000000..1bc36415 --- /dev/null +++ b/Tests/WebSampleS3/Extensions/ConsumerExtensions.cs @@ -0,0 +1,48 @@ +using EventSourcing.Backbone; + +// Configuration: https://medium.com/@gparlakov/the-confusion-of-asp-net-configuration-with-environment-variables-c06c545ef732 + + +namespace WebSample.Extensions +{ + /// + /// DI Extensions for ASP.NET Core + /// + public static class ConsumerExtensions + { + /// + /// Adds the shipment tracking producer. + /// + /// The builder. + /// The URI. + /// The s3 bucket. + /// + public static IServiceCollection AddShipmentTrackingConsumer + ( + this WebApplicationBuilder builder, + string uri, + string s3Bucket) + { + IServiceCollection services = builder.Services; + IWebHostEnvironment environment = builder.Environment; + string env = environment.EnvironmentName; + + var s3Options = new S3Options { Bucket = s3Bucket }; + services.AddSingleton(ioc => + { + IConsumerReadyBuilder consumer = + ioc.ResolveRedisConsumerChannel() + .ResolveS3Storage(s3Options) + .WithOptions(o => o with + { + OriginFilter = MessageOrigin.Original + }) + .Environment(env) + .Uri(uri); + return consumer; + }); + + return services; + } + } +} diff --git a/Tests/WebSampleS3/Extensions/OpenTelemetryExtensions.cs b/Tests/WebSampleS3/Extensions/OpenTelemetryExtensions.cs new file mode 100644 index 00000000..78a956d0 --- /dev/null +++ b/Tests/WebSampleS3/Extensions/OpenTelemetryExtensions.cs @@ -0,0 +1,87 @@ +using EventSourcing.Backbone; + +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +// see: +// https://opentelemetry.io/docs/instrumentation/net/getting-started/ +// https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.Jaeger/README.md#environment-variables + +namespace WebSampleS3; + +/// +/// Open telemetry extensions for ASP.NET Core +/// +internal static class OpenTelemetryExtensions +{ + #region AddOpenTelemetryEventSourcing + + /// + /// Adds open telemetry for event sourcing. + /// + /// The builder. + /// + public static IServiceCollection AddOpenTelemetryEventSourcing(this WebApplicationBuilder builder) + { + IWebHostEnvironment environment = builder.Environment; + IServiceCollection services = builder.Services; + + // see: + // https://opentelemetry.io/docs/instrumentation/net/getting-started/ + // https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.Jaeger/README.md#environment-variables + services.AddOpenTelemetry() + .WithEventSourcingTracing(environment, + cfg => + { + cfg.AddRedisInstrumentation( + RedisClientFactory.CreateProviderAsync().Result, + options => options.SetVerboseDatabaseStatements = true) + .AddAspNetCoreInstrumentation(m => + { + m.Filter = TraceFilter; + m.RecordException = true; + m.EnableGrpcAspNetCoreSupport = true; + }) + .AddHttpClientInstrumentation(m => + { + // m.Enrich + m.RecordException = true; + }) + .AddOtlpExporter(); + if (environment.IsDevelopment()) + cfg.AddConsoleExporter(); + }) + .WithEventSourcingMetrics(environment, cfg => + { + cfg.AddAspNetCoreInstrumentation( /* m => m.Filter = filter */) + .AddOtlpExporter() + .AddPrometheusExporter(); + //if (environment.IsDevelopment()) + // cfg.AddConsoleExporter(); + }); + + return services; + } + + #endregion // AddOpenTelemetryEventSourcing + + #region TraceFilter + + /// + /// Telemetries the filter. + /// + /// The CTX. + /// + private static bool TraceFilter(HttpContext ctx) => ctx.Request.Path.Value switch + { + "/health" => false, + "/readiness" => false, + "/metrics" => false, + string x when x.StartsWith("/swagger") => false, + string x when x.StartsWith("/_framework/") => false, + string x when x.StartsWith("/_vs/") => false, + _ => true + }; + + #endregion // TraceFilter +} diff --git a/Tests/WebSampleS3/Extensions/ShipmentTracking/ShipmentTrackingProducerExtensions.cs b/Tests/WebSampleS3/Extensions/ShipmentTracking/ShipmentTrackingProducerExtensions.cs new file mode 100644 index 00000000..0cd82be0 --- /dev/null +++ b/Tests/WebSampleS3/Extensions/ShipmentTracking/ShipmentTrackingProducerExtensions.cs @@ -0,0 +1,45 @@ +using EventSourcing.Backbone; + +// Configuration: https://medium.com/@gparlakov/the-confusion-of-asp-net-configuration-with-environment-variables-c06c545ef732 + + +namespace WebSampleS3; + +/// +/// DI Extensions for ASP.NET Core +/// +public static class ShipmentTrackingProducerExtensions +{ + /// + /// Adds the shipment tracking producer. + /// + /// The builder. + /// The URI. + /// The s3 bucket. + /// + public static WebApplicationBuilder AddShipmentTrackingProducer + ( + this WebApplicationBuilder builder, + string uri, + string s3Bucket) + { + IWebHostEnvironment environment = builder.Environment; + string env = environment.EnvironmentName; + IServiceCollection services = builder.Services; + + var s3Options = new S3Options { Bucket = s3Bucket }; + services.AddSingleton(ioc => + { + ILogger logger = ioc.GetService>() ?? throw new EventSourcingException("Logger is missing"); + IShipmentTrackingProducer producer = ioc.ResolveRedisProducerChannel() + .ResolveS3Storage(s3Options) + .Environment(env) + .Uri(uri) + .WithLogger(logger) + .BuildShipmentTrackingProducer(); + return producer; + }); + + return builder; + } +} diff --git a/Tests/WebSampleS3/Jobs/ConsumerJob.cs b/Tests/WebSampleS3/Jobs/ConsumerJob.cs new file mode 100644 index 00000000..c93a9ca1 --- /dev/null +++ b/Tests/WebSampleS3/Jobs/ConsumerJob.cs @@ -0,0 +1,178 @@ +using EventSourcing.Backbone; +using EventSourcing.Backbone.Building; + +namespace WebSampleS3; + +/// +/// Consumer job +/// +/// +/// +/// +public sealed class ConsumerJob : IHostedService, IAsyncDisposable +{ + private readonly IConsumerSubscribeBuilder _builder; + private CancellationTokenSource? _cancellationTokenSource; + private readonly IShipmentTrackingConsumer _subscriber; + private IConsumerLifetime? _subscription; + //private const string CONSUMER_GROUP = "CONSUMER"; + + #region Ctor + + /// + /// Initializes a new instance. + /// + /// The logger. + /// The builder. + public ConsumerJob( + ILogger logger, + IConsumerReadyBuilder consumerBuilder) + { + _builder = consumerBuilder.WithLogger(logger); + _subscriber = new Subscriber(logger); + } + + #endregion Ctor + + #region OnStartAsync + + /// + /// Start Consumer Job. + /// + /// The cancellation token. + Task IHostedService.StartAsync(CancellationToken cancellationToken) + { + _cancellationTokenSource = new CancellationTokenSource(); + var canellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token); + _subscription = _builder + // .Group(CONSUMER_GROUP) + .WithCancellation(canellation.Token) + // this extension is generate (if you change the interface use the correlated new generated extension method) + .SubscribeShipmentTrackingConsumer(_subscriber); + + return Task.CompletedTask; + //await _subscription.Completion; + } + + #endregion // OnStartAsync + + #region StopAsync + + /// + /// Stops the Consumer Job. + /// + /// The cancellation token. + /// + async Task IHostedService.StopAsync(CancellationToken cancellationToken) + { + _cancellationTokenSource?.CancelSafe(); + await (_subscription?.Completion ?? Task.CompletedTask); + } + + #endregion // StopAsync + + #region DisposeAsync + + /// + /// Disposes the asynchronous. + /// + /// + async ValueTask IAsyncDisposable.DisposeAsync() + { + _cancellationTokenSource?.CancelSafe(); + await (_subscription?.Completion ?? Task.CompletedTask); + } + + #endregion // DisposeAsync + + #region class Subscriber : IShipmentTrackingConsumer + + /// + /// The subscriber implementation + /// + private sealed class Subscriber : IShipmentTrackingConsumer + { + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + public Subscriber( + //ConsumerMetadata metadata, + ILogger logger) + { + _logger = logger; + } + + /// + /// Handle [OrderPlaced] event. + /// + /// The consumer meta. + /// The user. + /// The product. + /// The time. + /// + ValueTask IShipmentTrackingConsumer.OrderPlacedAsync(ConsumerMetadata consumerMeta, User user, Product product, DateTimeOffset time) + { + // get the current event metadata + Metadata meta = consumerMeta; + + _logger.LogInformation("handling OrderPlaced [{message-id}]: email: {email}, product: {productId}, which produce at {time}", meta.MessageId, user.email, product.id, time); + return ValueTask.CompletedTask; + } + + /// + /// Handle [Packings] event. + /// + /// The consumer meta. + /// The email. + /// The product identifier. + /// The time. + /// + ValueTask IShipmentTrackingConsumer.PackingAsync(ConsumerMetadata consumerMeta, string email, int productId, DateTimeOffset time) + { + // get the current event metadata + Metadata meta = consumerMeta; + + _logger.LogInformation("handling Packing [{message-id}]: email: {email}, product: {productId}, which produce at {time}", meta.MessageId, email, productId, time); + return ValueTask.CompletedTask; + } + + /// + /// Handle [on-delivery] event. + /// + /// The consumer meta. + /// The email. + /// The product identifier. + /// The time. + /// + ValueTask IShipmentTrackingConsumer.OnDeliveryAsync(ConsumerMetadata consumerMeta, string email, int productId, DateTimeOffset time) + { + // get the current event metadata + Metadata meta = consumerMeta; + + _logger.LogInformation("handling OnDelivery [{message-id}]: email: {email}, product: {productId}, which produce at {time}", meta.MessageId, email, productId, time); + return ValueTask.CompletedTask; + } + + /// + /// Handle [on-received] event. + /// + /// The consumer metadata. + /// The email. + /// The product identifier. + /// The time. + /// + ValueTask IShipmentTrackingConsumer.OnReceivedAsync(ConsumerMetadata consumerMeta, string email, int productId, DateTimeOffset time) + { + // get the current event metadata + Metadata meta = consumerMeta; + + _logger.LogInformation("handling OnReceived [{message-id}]: email: {email}, product: {productId}, which produce at {time}", meta.MessageId, email, productId, time); + return ValueTask.CompletedTask; + } + } + + #endregion // class Subscriber : IShipmentTrackingConsumer +} diff --git a/Tests/WebSampleS3/Program.cs b/Tests/WebSampleS3/Program.cs new file mode 100644 index 00000000..c0f0f7e4 --- /dev/null +++ b/Tests/WebSampleS3/Program.cs @@ -0,0 +1,50 @@ +using Amazon.S3; + +using WebSample.Extensions; + +using WebSampleS3; + + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDefaultAWSOptions(builder.Configuration.GetAWSOptions()); +builder.Services.AddAWSService(); +builder.Services.AddEventSourceRedisConnection(); + +// Add services to the container. + +builder.AddOpenTelemetryEventSourcing(); + +string URI = "shipment-tracking"; +// make sure to create the bucket on AWS S3 with both prefix 'dev.' and 'prod.' and any other environment you're using (like staging,etc.) +string s3Bucket = "shipment-tracking-sample"; + + +builder.AddShipmentTrackingProducer(URI, s3Bucket); +builder.AddShipmentTrackingConsumer(URI, s3Bucket); + +builder.Services.AddHostedService(); + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +app.UseOpenTelemetryPrometheusScrapingEndpoint(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/Tests/WebSampleS3/Properties/launchSettings.json b/Tests/WebSampleS3/Properties/launchSettings.json new file mode 100644 index 00000000..adc338eb --- /dev/null +++ b/Tests/WebSampleS3/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:49788", + "sslPort": 44330 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5280", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7204;http://localhost:5280", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Tests/WebSampleS3/WebSampleS3.csproj b/Tests/WebSampleS3/WebSampleS3.csproj new file mode 100644 index 00000000..51551c33 --- /dev/null +++ b/Tests/WebSampleS3/WebSampleS3.csproj @@ -0,0 +1,56 @@ + + + + Debug;Release;Gen + false + + + + + + + + + + + + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/WebSampleS3/appsettings.Development.json b/Tests/WebSampleS3/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Tests/WebSampleS3/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Tests/WebSampleS3/appsettings.json b/Tests/WebSampleS3/appsettings.json new file mode 100644 index 00000000..58dcdba5 --- /dev/null +++ b/Tests/WebSampleS3/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "AWS": { + "Region": "us-east-1", + "Profile": "playground" + } +} diff --git a/Tests/WebSampleS3/icon.png b/Tests/WebSampleS3/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/Tests/WebSampleS3/icon.png differ diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Class1.cs b/Tests/Weknow.EventSource.Backbone.IntegrationTests/Class1.cs deleted file mode 100644 index 9f9ef7be..00000000 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Class1.cs +++ /dev/null @@ -1,22 +0,0 @@ - -#nullable enable - -using System.CodeDom.Compiler; - -namespace Weknow.EventSource.Backbone.UnitTests.Entities -{ - /// - /// Entity mapper is responsible of mapping announcement to DTO generated from SequenceOperationsConsumer - /// - /// - [GeneratedCode("Weknow.EventSource.Backbone.SrcGen", "1.1.141.0")] - public static class SequenceOperationsConsumerEntityMapperExtensions1 - { - /// - /// Specialize Enumerator of event produced by ISequenceOperationsConsumer - /// - public static IConsumerIterator SpecializeSequenceOperationsConsumer(IConsumerIterator iterator) => - iterator.Specialize(SequenceOperationsConsumerEntityMapper.Default); - } - -} diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/Inheritance/IFlowA.cs b/Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/Inheritance/IFlowA.cs deleted file mode 100644 index 57c06983..00000000 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/Inheritance/IFlowA.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Weknow.EventSource.Backbone.UnitTests.Entities; - -[GenerateEventSource(EventSourceGenType.Producer)] -[GenerateEventSource(EventSourceGenType.Consumer)] -public interface IFlowA -{ - ValueTask AAsync(int id); -} diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/Inheritance/IFlowAB.cs b/Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/Inheritance/IFlowAB.cs deleted file mode 100644 index c7aa7be1..00000000 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/Inheritance/IFlowAB.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Weknow.EventSource.Backbone.UnitTests.Entities; - -[GenerateEventSource(EventSourceGenType.Producer)] -[GenerateEventSource(EventSourceGenType.Consumer)] -public interface IFlowAB : IFlowA, IFlowB -{ - ValueTask DerivedAsync(string key); -} diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/Inheritance/IFlowB.cs b/Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/Inheritance/IFlowB.cs deleted file mode 100644 index 33f3d379..00000000 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Contracts/Inheritance/IFlowB.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Weknow.EventSource.Backbone.UnitTests.Entities; - -[GenerateEventSource(EventSourceGenType.Producer)] -[GenerateEventSource(EventSourceGenType.Consumer)] -public interface IFlowB -{ - ValueTask BAsync(DateTimeOffset date); -} diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/DeleteKeysTests.cs b/Tests/Weknow.EventSource.Backbone.IntegrationTests/DeleteKeysTests.cs deleted file mode 100644 index 56e9ddbc..00000000 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/DeleteKeysTests.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.Diagnostics; -using System.Threading.Tasks.Dataflow; - -using StackExchange.Redis; - -using Xunit; -using Xunit.Abstractions; - -using static Weknow.EventSource.Backbone.Channels.RedisProvider.Common.RedisChannelConstants; - -namespace Weknow.EventSource.Backbone.Tests -{ - public class DeleteKeysTests - { - private readonly ITestOutputHelper _outputHelper; - - public DeleteKeysTests( - ITestOutputHelper outputHelper) - { - _outputHelper = outputHelper; - - } - #region DELETE_KEYS_TEST - - - [Theory(Skip = "cleanup")] - [InlineData("*test*")] - [InlineData("dev:*")] - [Trait("type", "delete-keys")] - public async Task DELETE_KEYS_TEST(string pattern) - { - IConnectionMultiplexer conn = RedisClientFactory.CreateProviderBlocking( - cfg => cfg.AllowAdmin = true); - string serverName = Environment.GetEnvironmentVariable(END_POINT_KEY) ?? "localhost:6379"; - var server = conn.GetServer(serverName); - IEnumerable keys = server.Keys(pattern: pattern).ToArray(); - // IEnumerable keys = server.Keys(pattern: "dev:*").ToArray(); - IDatabaseAsync db = conn.GetDatabase(); - - var ab = new ActionBlock(k => LocalAsync(k), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 30 }); - foreach (string? key in keys) - { - if (string.IsNullOrWhiteSpace(key)) continue; - ab.Post(key); - } - - ab.Complete(); - await ab.Completion; - - async Task LocalAsync(string k) - { - try - { - await db.KeyDeleteAsync(k, CommandFlags.DemandMaster); - Trace.WriteLine(k); - } - #region Exception Handling - - catch (RedisTimeoutException ex) - { - _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); - } - catch (Exception ex) - { - _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); - } - - #endregion // Exception Handling - } - } - - #endregion // DELETE_KEYS_TEST - } -} diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Entities/Person.cs b/Tests/Weknow.EventSource.Backbone.IntegrationTests/Entities/Person.cs deleted file mode 100644 index 37ade477..00000000 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Entities/Person.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Weknow.EventSource.Backbone.UnitTests.Entities -{ - public record Person(int Id, string Name); - -} diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/MigrationReceiverTest.cs b/Tests/Weknow.EventSource.Backbone.IntegrationTests/MigrationReceiverTest.cs deleted file mode 100644 index bbc1432b..00000000 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/MigrationReceiverTest.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System.Diagnostics; - -using FakeItEasy; - -using Microsoft.Extensions.Logging; - -using Weknow.EventSource.Backbone.Building; - -using Xunit; -using Xunit.Abstractions; - -// docker run -p 6379:6379 -it --rm --name redis-event-source redislabs/rejson:latest - -namespace Weknow.EventSource.Backbone.Tests -{ - /// - /// The end to end tests. - /// - public class MigrationReceiverTest // : IDisposable - { - private const string TARGET_KEY = "REDIS_MIGRATION_TARGET_ENDPOINT"; - private const string SOURCE_KEY = "REDIS_EVENT_SOURCE_ENDPOINT"; - - private readonly ITestOutputHelper _outputHelper; - private readonly IProducerStoreStrategyBuilder _targetProducerBuilder; - private readonly IConsumerStoreStrategyBuilder _sourceConsumerBuilder; - private readonly string ENV = "Production"; - //private readonly string ENV = "Development"; - private readonly string PARTITION = "analysts"; - //private readonly string PARTITION = $"{DateTime.UtcNow:yyyy-MM-dd HH_mm_ss}:{Guid.NewGuid():N}"; - private readonly string SHARD = "default"; - - private readonly ILogger _fakeLogger = A.Fake(); - private const int TIMEOUT = 1_000 * 300; - - #region Ctor - - /// - /// Initializes a new instance of the class. - /// - /// The output helper. - public MigrationReceiverTest(ITestOutputHelper outputHelper) - { - _outputHelper = outputHelper; - _targetProducerBuilder = ProducerBuilder.Empty.UseRedisChannel(credentialsKeys: new RedisCredentialsKeys { EndpointKey = TARGET_KEY }) - .AddVoidStrategy(); - //.AddS3Strategy(new S3Options { Bucket="temp"}); - - _sourceConsumerBuilder = ConsumerBuilder.Empty.UseRedisChannel(credentialsKeys: new RedisCredentialsKeys { EndpointKey = SOURCE_KEY }) - .AddS3Strategy(new S3Options { Bucket = "event-source-storage", EnvironmentConvension = S3EnvironmentConvention.BucketPrefix }); - } - - #endregion // Ctor - - - #region Migration_By_Receiver_Test - - [Fact(Timeout = TIMEOUT, Skip = "Use to migrate data between 2 different sources")] - //[Fact(Timeout = TIMEOUT)] - public async Task Migration_By_Receiver_Test() - { - #region IRawProducer rawProducer = ... - - IRawProducer rawProducer = _targetProducerBuilder - .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) - .WithLogger(_fakeLogger) - .BuildRaw(new RawProducerOptions { KeepOriginalMeta = true }); - - #endregion // IRawProducer rawProducer = ... - - - CancellationToken cancellation = GetCancellationToken(); - - IAsyncEnumerable announcements = _sourceConsumerBuilder - .WithCancellation(cancellation) - .Environment(ENV) - .Partition(PARTITION) - .Shard(SHARD) - .WithLogger(_fakeLogger) - .BuildIterator() - .GetAsyncEnumerable(new ConsumerAsyncEnumerableOptions { ExitWhenEmpty = true }); - int count = 0; - await foreach (var announcement in announcements.WithCancellation(cancellation)) - { - _fakeLogger.LogInformation(announcement.ToString()); - await rawProducer.Produce(announcement); - count++; - _outputHelper.WriteLine($"{count} events processed"); - } - _outputHelper.WriteLine($"Total events = {count}"); - } - - #endregion // Migration_By_Receiver_Test - - - #region GetCancellationToken - - /// - /// Gets the cancellation token. - /// - /// - private static CancellationToken GetCancellationToken() - { - return new CancellationTokenSource(Debugger.IsAttached - ? TimeSpan.FromMinutes(10) - : TimeSpan.FromMinutes(5)).Token; - } - - #endregion // GetCancellationToken - - #region // Dispose pattern - - - //~MigrationReceiverTest() - //{ - // Dispose(); - //} - - //public void Dispose() - //{ - // GC.SuppressFinalize(this); - // try - // { - // IConnectionMultiplexer conn = RedisClientFactory.CreateProviderBlocking( - // cfg => cfg.AllowAdmin = true); - // string serverName = Environment.GetEnvironmentVariable(END_POINT_KEY) ?? "localhost:6379"; - // var server = conn.GetServer(serverName); - // IEnumerable keys = server.Keys(pattern: $"*{PARTITION}*"); - // IDatabaseAsync db = conn.GetDatabase(); - - // var ab = new ActionBlock(k => LocalAsync(k), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 30 }); - // foreach (string key in keys) - // { - // ab.Post(key); - // } - - // ab.Complete(); - // ab.Completion.Wait(); - - // async Task LocalAsync(string k) - // { - // try - // { - // await db.KeyDeleteAsync(k, CommandFlags.DemandMaster); - // _outputHelper.WriteLine($"Cleanup: delete key [{k}]"); - // } - // #region Exception Handling - - // catch (RedisTimeoutException ex) - // { - // _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); - // } - // catch (Exception ex) - // { - // _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); - // } - - // #endregion // Exception Handling - // } - // } - // #region Exception Handling - - // catch (RedisTimeoutException ex) - // { - // _outputHelper.WriteLine($"Test dispose timeout error (delete keys) {ex.FormatLazy()}"); - // } - // catch (Exception ex) - // { - // _outputHelper.WriteLine($"Test dispose error (delete keys) {ex.FormatLazy()}"); - // } - - // #endregion // Exception Handling - //} - - #endregion // Dispose pattern - - #region SubscriptionBridge - - private class SubscriptionBridge : ISubscriptionBridge - { - private readonly IRawProducer _fw; - - public SubscriptionBridge(IRawProducer fw) - { - _fw = fw; - } - - public async Task BridgeAsync(Announcement announcement, IConsumerBridge consumerBridge) - { - await _fw.Produce(announcement); - return true; - - } - } - - #endregion // SubscriptionBridge - } -} diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Weknow.EventSource.Backbone.IntegrationTests.csproj b/Tests/Weknow.EventSource.Backbone.IntegrationTests/Weknow.EventSource.Backbone.IntegrationTests.csproj deleted file mode 100644 index 5971b63b..00000000 --- a/Tests/Weknow.EventSource.Backbone.IntegrationTests/Weknow.EventSource.Backbone.IntegrationTests.csproj +++ /dev/null @@ -1,64 +0,0 @@ - - - - Debug;Release;Gen - false - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tests/Weknow.EventSource.Backbone.IntegrationTests/icon.png b/Tests/Weknow.EventSource.Backbone.IntegrationTests/icon.png deleted file mode 100644 index d6811ad8..00000000 Binary files a/Tests/Weknow.EventSource.Backbone.IntegrationTests/icon.png and /dev/null differ diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/Factory/SequenceOperations/SequenceOperationsConsumer.cs b/Tests/Weknow.EventSource.Backbone.UnitTests/Factory/SequenceOperations/SequenceOperationsConsumer.cs deleted file mode 100644 index 485a3e42..00000000 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/Factory/SequenceOperations/SequenceOperationsConsumer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Threading.Tasks.Dataflow; - -namespace Weknow.EventSource.Backbone.UnitTests.Entities -{ - public class SequenceOperationsConsumer : ISequenceOperationsConsumer - { - private readonly ActionBlock _block = new ActionBlock( - u => Console.WriteLine(u.Details.Id), - new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 10 }); - public ValueTask RegisterAsync(User user) - { - //var msg = new Ackable(user, ack); - //_block.Post(msg); - return ValueTask.CompletedTask; - } - - public ValueTask UpdateAsync(User user) => throw new NotImplementedException(); - public ValueTask LoginAsync(string email, string password) => throw new NotImplementedException(); - public ValueTask LogoffAsync(int id) => throw new NotImplementedException(); - public ValueTask ApproveAsync(int id) => throw new NotImplementedException(); - public ValueTask SuspendAsync(int id) => throw new NotImplementedException(); - public ValueTask ActivateAsync(int id) => throw new NotImplementedException(); - public ValueTask EarseAsync(int id) => throw new NotImplementedException(); - } -} diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/Subscriptions/SimpleEventSubscription.cs b/Tests/Weknow.EventSource.Backbone.UnitTests/Subscriptions/SimpleEventSubscription.cs deleted file mode 100644 index d26fd76a..00000000 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/Subscriptions/SimpleEventSubscription.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Weknow.EventSource.Backbone.UnitTests.Entities; - -namespace Weknow.EventSource.Backbone -{ - - /// - /// In-Memory Channel (excellent for testing) - /// - /// - public class SimpleEventSubscription : SimpleEventSubscriptionBase - { - private readonly ISimpleEventConsumer _target; - - #region Ctor - - public SimpleEventSubscription(ISimpleEventConsumer target) - { - _target = target; - } - - #endregion // Ctor - - protected override ValueTask ExecuteAsync(string key, int value) => _target.ExecuteAsync(key, value); - - protected override ValueTask RunAsync(int id, DateTime date) => _target.RunAsync(id, date); - } -} diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/Weknow.EventSource.Backbone.UnitTests.csproj b/Tests/Weknow.EventSource.Backbone.UnitTests/Weknow.EventSource.Backbone.UnitTests.csproj deleted file mode 100644 index a54c621d..00000000 --- a/Tests/Weknow.EventSource.Backbone.UnitTests/Weknow.EventSource.Backbone.UnitTests.csproj +++ /dev/null @@ -1,46 +0,0 @@ - - - - Debug;Release;Gen - false - disable - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tests/Weknow.EventSource.Backbone.UnitTests/icon.png b/Tests/Weknow.EventSource.Backbone.UnitTests/icon.png deleted file mode 100644 index d6811ad8..00000000 Binary files a/Tests/Weknow.EventSource.Backbone.UnitTests/icon.png and /dev/null differ diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/AspCoreExtensions.cs b/Tests/Weknow.EventSource.Backbone.WebEventTest/AspCoreExtensions.cs deleted file mode 100644 index 51e96c2e..00000000 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/AspCoreExtensions.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -using Microsoft.AspNetCore.Server.Kestrel.Core; - -using Newtonsoft.Json.Serialization; - -using OpenTelemetry.Exporter; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; - -using StackExchange.Redis; - -using Weknow.EventSource.Backbone; - -// Configuration: https://medium.com/@gparlakov/the-confusion-of-asp-net-configuration-with-environment-variables-c06c545ef732 - - -namespace Microsoft.Extensions.Configuration -{ - /// - /// Weknow core extensions for ASP.NET Core - /// - public static class AspCoreExtensions - { - private static readonly NamingStrategy _namingStrategy = new CamelCaseNamingStrategy(); - - #region AddRedis - - /// - /// Adds the weknow standard configuration. - /// - /// The services. - /// The host env. - /// - /// - public static IConnectionMultiplexer AddRedis( - this IServiceCollection services, - IHostEnvironment hostEnv, - string shortAppName) - { - IConnectionMultiplexer redisConnection = RedisClientFactory.CreateProviderBlocking(); - services.AddSingleton(redisConnection); - - return redisConnection; - } - - #endregion // AddRedis - - #region AddOpenTelemetryWeknow - - /// - /// Adds the weknow open-telemetry binding. - /// - /// The services. - /// The host env. - /// Short name of the application. - /// The redis connection. - /// - public static IServiceCollection AddOpenTelemetryWeknow( - this IServiceCollection services, - IHostEnvironment hostEnv, - string shortAppName, - IConnectionMultiplexer redisConnection) - { - // see: https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.Jaeger/README.md#environment-variables - - Console.WriteLine($"JAEGER endpoint: key='OTEL_EXPORTER_JAEGER_ENDPOINT', env='{hostEnv.EnvironmentName}'"); // will be visible in the pods logs - -#pragma warning disable S125 // Sections of code should not be commented out - services.AddOpenTelemetry() - .WithTracing(builder => - { - builder.SetResourceBuilder(ResourceBuilder.CreateDefault() - .AddService(shortAppName)) - .ListenToEventSourceRedisChannel() - // .SetSampler() - .AddAspNetCoreInstrumentation(m => - { - m.Filter = OpenTelemetryFilter; - // m.Enrich - m.RecordException = true; - m.EnableGrpcAspNetCoreSupport = true; - }) - .AddHttpClientInstrumentation(m => - { - // m.Enrich - m.RecordException = true; - }) - //.AddRedisInstrumentation(redisConnection - // //, m => { - // // m.FlushInterval - // //} - // ) - .AddOtlpExporter() - .SetSampler(TestSampler.Create(LogLevel.Information)); - - if (hostEnv.IsDevelopment()) - { - builder.AddConsoleExporter(options => options.Targets = ConsoleExporterOutputTargets.Console); - } - }); - return services; -#pragma warning restore S125 // Sections of code should not be commented out - - #region OpenTelemetryFilter - - bool OpenTelemetryFilter(HttpContext context) => OpenTelemetryFilterMap(context.Request.Path.Value); - - bool OpenTelemetryFilterMap(string? path) - { - if (string.IsNullOrEmpty(path) || - path == "/health" || - path == "/readiness" || - path == "/version" || - path == "/settings" || - path.StartsWith("/v1/kv/") || // configuration - path == "/api/v2/write" || // influx metrics - path == "/_bulk" || - path.StartsWith("/swagger") || - path.IndexOf("health-check") != -1) - { - return false; - } - return true; - } - - #endregion // OpenTelemetryFilter - } - - #endregion // AddOpenTelemetryWeknow - - #region WithJsonOptions - - /// - /// Set Controller's with the standard json configuration. - /// - /// The controllers. - /// - public static IMvcBuilder WithJsonOptions( - this IMvcBuilder controllers) - { - return controllers.AddJsonOptions(options => - { - // https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-how-to - JsonSerializerOptions setting = options.JsonSerializerOptions; - setting.WithDefault(); - - }); - } - - #endregion // WithJsonOptions - - public static JsonSerializerOptions WithDefault(this JsonSerializerOptions options) - { - options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - options.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; - options.WriteIndented = true; - options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); - options.Converters.Add(JsonImmutableDictionaryConverter.Default); - options.Converters.Add(JsonMemoryBytesConverterFactory.Default); - return options; - } - - #region UseRestDefaultsWeknow - - /// - /// Pre-configured host defaults for rest API. - /// - /// The type of the startup. - /// The builder. - /// The process arguments. - /// - public static IHostBuilder UseRestDefaults( - this IHostBuilder builder, - params string[] args) where TStartup : class - { - builder.ConfigureHostConfiguration(cfg => - { - // https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-2.1&tabs=windows - // cfg.AddEnvironmentVariables() - // cfg.AddJsonFile() - // cfg.AddInMemoryCollection() - // cfg.AddConfiguration() - // cfg.AddUserSecrets() - }) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.ConfigureKestrel( - (WebHostBuilderContext context, - KestrelServerOptions options) => - { - options.ConfigureEndpointDefaults(c => c.Protocols = HttpProtocols.Http1AndHttp2); - }); - webBuilder.UseStartup(); - }).ConfigureLogging((context, builder) => - { - builder.AddOpenTelemetry(options => - { - options.IncludeScopes = true; - options.ParseStateValues = true; - options.IncludeFormattedMessage = true; - }); - }); - return builder; - } - - #endregion // UseRestDefaultsWeknow - } -} diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/Controllers/TestController.cs b/Tests/Weknow.EventSource.Backbone.WebEventTest/Controllers/TestController.cs deleted file mode 100644 index 00f3ddb2..00000000 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/Controllers/TestController.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -using StackExchange.Redis; - -namespace Weknow.EventSource.Backbone.WebEventTest.Controllers -{ - [Route("api/[controller]")] - [ApiController] - public class TestController : ControllerBase - { - private readonly ILogger _logger; - private readonly IEventSourceRedisConnectionFacroty _connFacroty; - - public TestController( - ILogger logger, - IEventSourceRedisConnectionFacroty connFacroty) - { - _logger = logger; - _connFacroty = connFacroty; - } - - /// - /// Post Analysts - /// - /// - /// - [HttpGet("conn")] - //[AllowAnonymous] - [ProducesResponseType(StatusCodes.Status201Created)] - public async ValueTask GetAsync() - { - var conn = await RedisClientFactory.CreateProviderAsync(_logger); - var status = conn.GetStatus(); - var db = conn.GetDatabase(); - var p = await db.PingAsync(); - - return @$"ClientName: {conn.ClientName}, IsConnected: {conn.IsConnected}, -timeout (ms): {conn.TimeoutMilliseconds}, -status: {status}, ping: {p}"; - } - - /// - /// Post Analysts - /// - /// - /// - [HttpGet("ping")] - //[AllowAnonymous] - [ProducesResponseType(StatusCodes.Status201Created)] - public async ValueTask GetPingAsync() - { - IDatabaseAsync db = await _connFacroty.GetDatabaseAsync(); - var p = await db.PingAsync(); - _logger.LogInformation("Schema: {schema}", Request.Scheme); - return p; - } - - /// - /// Post Analysts - /// - /// - /// - [HttpGet("static")] - //[AllowAnonymous] - [ProducesResponseType(StatusCodes.Status201Created)] - public async ValueTask GetStaticAsync() - { - IDatabaseAsync db = await _connFacroty.GetDatabaseAsync(); - var p = await db.PingAsync(); - - return @$"ping: {p}"; - } - } -} - diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/Dockerfile.develop b/Tests/Weknow.EventSource.Backbone.WebEventTest/Dockerfile.develop deleted file mode 100644 index 2087ea49..00000000 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/Dockerfile.develop +++ /dev/null @@ -1,18 +0,0 @@ -FROM mcr.microsoft.com/dotnet/sdk:5.0 -ARG BUILD_CONFIGURATION=Debug -ENV ASPNETCORE_ENVIRONMENT=Development -ENV ASPNETCORE_URLS=http://+:80 -ENV DOTNET_USE_POLLING_FILE_WATCHER=true -EXPOSE 80 - -WORKDIR /src -COPY ["Tests/Weknow.EventSource.Backbone.WebEventTest/Weknow.EventSource.Backbone.WebEventTest.csproj", "Tests/Weknow.EventSource.Backbone.WebEventTest/"] - -RUN dotnet restore "Tests/Weknow.EventSource.Backbone.WebEventTest/Weknow.EventSource.Backbone.WebEventTest.csproj" -COPY . . -WORKDIR "/src/Tests/Weknow.EventSource.Backbone.WebEventTest" -RUN dotnet build --no-restore "Weknow.EventSource.Backbone.WebEventTest.csproj" -c $BUILD_CONFIGURATION - -RUN echo "exec dotnet run --no-build --no-launch-profile -c $BUILD_CONFIGURATION --" > /entrypoint.sh - -ENTRYPOINT ["/bin/bash", "/entrypoint.sh"] \ No newline at end of file diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/Program.cs b/Tests/Weknow.EventSource.Backbone.WebEventTest/Program.cs deleted file mode 100644 index 45f06de9..00000000 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/Program.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System.Text.Json; - -using Microsoft.OpenApi.Models; - -using Refit; - -using StackExchange.Redis; - -using Weknow.EventSource.Backbone.WebEventTest; -using Weknow.EventSource.Backbone.WebEventTest.Jobs; - -const string ENV = $"test"; - -var builder = WebApplication.CreateBuilder(args); - -IWebHostEnvironment environment = builder.Environment; -string env = environment.EnvironmentName; -string appName = environment.ApplicationName; -string shortAppName = appName.Replace("Weknow.", string.Empty) - .Replace("Backend.", string.Empty); - - -var services = builder.Services; -// Add services to the container. - -services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -services.AddEndpointsApiExplorer(); -services.AddSwaggerGen( - opt => - { - //opt.UseInlineDefinitionsForEnums(); - //opt.UseOneOfForPolymorphism(); - //opt.UseAllOfToExtendReferenceSchemas(); - //opt.UseAllOfForInheritance(); - opt.SupportNonNullableReferenceTypes(); - opt.IgnoreObsoleteProperties(); - opt.IgnoreObsoleteActions(); - opt.DescribeAllParametersInCamelCase(); - - - opt.SwaggerDoc("v1", new OpenApiInfo - { - Version = "v1", - Title = "Environment setup", - Description = @"

Use the following docker in order to setup the environment

-

docker run -p 6379:6379 -it --rm --name redis-Json redislabs/rejson:latest

-

docker run --rm -it --name jaeger -p 13133:13133 -p 16686:16686 -p 4317:55680 jaegertracing/opentelemetry-all-in-one

-", - - //TermsOfService = new Uri("https://example.com/terms"), - //Contact = new OpenApiContact - //{ - // Name = "Example Contact", - // Url = new Uri("https://example.com/contact") - //}, - //License = new OpenApiLicense - //{ - // Name = "Example License", - // Url = new Uri("https://example.com/license") - //} - }); - - //opt.SwaggerDoc("Event Source", new Microsoft.OpenApi.Models.OpenApiInfo { Description = "Bla" }); - //opt.SelectSubTypesUsing(); - //opt.SelectDiscriminatorValueUsing(); - //opt.SelectDiscriminatorNameUsing(); - }); -services.AddHostedService(); -services.AddHostedService(); -string fwPort = Environment.GetEnvironmentVariable("FW") ?? "MISSING-PORT"; - -var jsonOptions = new JsonSerializerOptions().WithDefault(); - -var refitSetting = new RefitSettings -{ - ContentSerializer = new SystemTextJsonContentSerializer(jsonOptions) -}; -services.AddRefitClient(refitSetting) // https://github.com/reactiveui/refit - .ConfigureHttpClient(c => - { - c.BaseAddress = new Uri($"http://localhost:{fwPort}/api/Migration"); - c.DefaultRequestHeaders.Add("wk-pattern", "migration"); - }); -services.AddHttpClient("migration", c => - { - c.BaseAddress = new Uri($"http://localhost:{fwPort}/api/Migration"); - c.DefaultRequestHeaders.Add("wk-pattern", "migration"); - }); - -IConnectionMultiplexer redisConnection = services.AddRedis(environment, shortAppName); -services.AddOpenTelemetryWeknow(environment, shortAppName, redisConnection); - - -services.AddOptions(); // enable usage of IOptionsSnapshot dependency injection - -services.AddEventSourceRedisConnection(); - -services.AddEventSource(env); - - -void RedisConfigEnrichment(ConfigurationOptions configuration) -{ - configuration.ReconnectRetryPolicy = new RedisReconnectRetryPolicy(); - configuration.ClientName = "Web Test"; -} -services.AddSingleton>(RedisConfigEnrichment); - -services.AddControllers() - .WithJsonOptions(); - -var app = builder.Build(); - -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - -app.UseHttpsRedirection(); - -//app.UseAuthorization(); - -app.MapControllers(); - -app.Run(); - - -/// -/// Redis reconnect retry policy -/// -/// -public class RedisReconnectRetryPolicy : IReconnectRetryPolicy -{ - /// - /// Shoulds the retry. - /// - /// The current retry count. - /// The time elapsed milliseconds since last retry. - /// - bool IReconnectRetryPolicy.ShouldRetry( - long currentRetryCount, - int timeElapsedMillisecondsSinceLastRetry) - { - return timeElapsedMillisecondsSinceLastRetry > 1000; - } -} \ No newline at end of file diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/Weknow.EventSource.Backbone.WebEventTest.csproj b/Tests/Weknow.EventSource.Backbone.WebEventTest/Weknow.EventSource.Backbone.WebEventTest.csproj deleted file mode 100644 index 976175c8..00000000 --- a/Tests/Weknow.EventSource.Backbone.WebEventTest/Weknow.EventSource.Backbone.WebEventTest.csproj +++ /dev/null @@ -1,44 +0,0 @@ - - - - Linux - ..\.. - Debug;Release;Gen - True - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tests/Weknow.EventSource.Backbone.WebEventTest/icon.png b/Tests/Weknow.EventSource.Backbone.WebEventTest/icon.png deleted file mode 100644 index d6811ad8..00000000 Binary files a/Tests/Weknow.EventSource.Backbone.WebEventTest/icon.png and /dev/null differ diff --git a/Weknow.EventSource.Backbone.Contracts/Attributes/GenerateEventSourceAttribute.cs b/Weknow.EventSource.Backbone.Contracts/Attributes/GenerateEventSourceAttribute.cs deleted file mode 100644 index f5f5fb66..00000000 --- a/Weknow.EventSource.Backbone.Contracts/Attributes/GenerateEventSourceAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Weknow.EventSource.Backbone -{ - /// - /// Mark for code generation - /// - /// - [AttributeUsage(AttributeTargets.Interface, AllowMultiple = true)] - public class GenerateEventSourceAttribute : GenerateEventSourceBaseAttribute - { - /// - /// Initializes a new instance of the class. - /// - /// Type of the generate. - public GenerateEventSourceAttribute(EventSourceGenType generateType) : base(generateType) - { - } - } -} diff --git a/Weknow.EventSource.Backbone.Contracts/Attributes/GenerateEventSourceBridgeAttribute.cs b/Weknow.EventSource.Backbone.Contracts/Attributes/GenerateEventSourceBridgeAttribute.cs deleted file mode 100644 index 2e72f608..00000000 --- a/Weknow.EventSource.Backbone.Contracts/Attributes/GenerateEventSourceBridgeAttribute.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Weknow.EventSource.Backbone -{ - [Obsolete("Deprecated, use GenerateEventSourceAttribute instead", true)] - [AttributeUsage(AttributeTargets.Interface, AllowMultiple = true)] - public class GenerateEventSourceBridgeAttribute : GenerateEventSourceBaseAttribute - { - public GenerateEventSourceBridgeAttribute(EventSourceGenType generateType) : base(generateType) - { - } - - public string? InterfaceName { get; init; } - } -} diff --git a/Weknow.EventSource.Backbone.Contracts/Env.cs b/Weknow.EventSource.Backbone.Contracts/Env.cs deleted file mode 100644 index c0f01814..00000000 --- a/Weknow.EventSource.Backbone.Contracts/Env.cs +++ /dev/null @@ -1,82 +0,0 @@ -using static System.StringComparison; - -namespace Weknow.EventSource.Backbone -{ - /// - /// Common Constants - /// - public class Env - { - private readonly string _value; - - #region Ctor - - /// - /// Initializes a new instance. - /// - /// The value. - public Env(string value) - { - _value = Env.Format(value); - } - - #endregion // Ctor - - #region Cast overloads - - /// - /// Performs an implicit conversion from to . - /// - /// The env. - /// - /// The result of the conversion. - /// - public static implicit operator Env(string env) => new Env(env); - - /// - /// Performs an implicit conversion from to . - /// - /// The env. - /// - /// The result of the conversion. - /// - public static implicit operator string(Env env) => env._value; - - #endregion // Cast overloads - - #region Format - - /// - /// Formats the specified environment into WeKnow convention. - /// - /// - public string Format() => Env.Format(_value); - - /// - /// Formats the specified environment into WeKnow convention. - /// - /// - private static string Format(string e) - { - bool comp(string candidate) => string.Compare(e, candidate, OrdinalIgnoreCase) == 0; - - if (comp("Production")) return "prod"; - if (comp("Prod")) return "prod"; - if (comp("Development")) return "dev"; - if (comp("Dev")) return "dev"; - - return e; - } - - #endregion // Format - - #region ToString - - /// - /// Converts to string. - /// - public override string ToString() => _value; - - #endregion // ToString - } -} diff --git a/Weknow.EventSource.Backbone.Contracts/EventSourceConstants.cs b/Weknow.EventSource.Backbone.Contracts/EventSourceConstants.cs deleted file mode 100644 index 14c5d2b7..00000000 --- a/Weknow.EventSource.Backbone.Contracts/EventSourceConstants.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Weknow.EventSource.Backbone -{ - /// - /// Constants - /// - public static class EventSourceConstants - { - /// - /// The name of redis consumer channel source - /// - public const string REDIS_CONSUMER_CHANNEL_SOURCE = "redis-consumer-channel"; - /// - /// The name of redis producer channel source - /// - public const string REDIS_PRODUCER_CHANNEL_SOURCE = "redis-producer-channel"; - - public static readonly JsonStringEnumConverter EnumConvertor = new JsonStringEnumConverter(JsonNamingPolicy.CamelCase); - public static readonly JsonSerializerOptions SerializerOptionsWithIndent = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - // PropertyNameCaseInsensitive = true, - // IgnoreNullValues = true, - WriteIndented = true, - Converters = - { - EnumConvertor, - JsonDictionaryConverter.Default, - JsonImmutableDictionaryConverter.Default, - JsonMemoryBytesConverterFactory.Default - } - }; - } -} diff --git a/Weknow.EventSource.Backbone.Contracts/Interfaces/IPlanRoute.cs b/Weknow.EventSource.Backbone.Contracts/Interfaces/IPlanRoute.cs deleted file mode 100644 index e97b0104..00000000 --- a/Weknow.EventSource.Backbone.Contracts/Interfaces/IPlanRoute.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace Weknow.EventSource.Backbone -{ - /// - /// Plan routing identification - /// - public interface IPlanRoute - { - /// - /// Environment (part of the stream key). - /// - /// - /// The partition. - /// - Env Environment { get; } - /// - /// Partition key represent logical group of - /// event source shards. - /// For example assuming each ORDERING flow can have its - /// own messaging sequence, yet can live concurrency with - /// other ORDER's sequences. - /// The partition will let consumer the option to be notify and - /// consume multiple shards from single consumer. - /// This way the consumer can handle all orders in - /// central place without affecting sequence of specific order - /// flow or limiting the throughput. - /// - /// - /// The partition. - /// - string Partition { get; } - /// - /// Shard key represent physical sequence. - /// Use same shard when order is matter. - /// For example: assuming each ORDERING flow can have its - /// own messaging sequence, in this case you can split each - /// ORDER into different shard and gain performance bust.. - /// - string Shard { get; } - - } -} \ No newline at end of file diff --git a/Weknow.EventSource.Backbone.Contracts/JsonDataSerializer.cs b/Weknow.EventSource.Backbone.Contracts/JsonDataSerializer.cs deleted file mode 100644 index be9e7006..00000000 --- a/Weknow.EventSource.Backbone.Contracts/JsonDataSerializer.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Text; -using System.Text.Json; - -using static Weknow.Text.Json.Constants; - -namespace Weknow.EventSource.Backbone -{ - /// - /// Json serializer (this is the default serializer) - /// - /// - internal class JsonDataSerializer : IDataSerializer - { - private readonly JsonSerializerOptions _options; - - public JsonDataSerializer(JsonSerializerOptions? options = null) - { - _options = options ?? SerializerOptions; - } - -#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. -#pragma warning disable CS8603 // Possible null reference return. - T IDataSerializer.Deserialize(ReadOnlyMemory serializedData) - { - T result = JsonSerializer.Deserialize(serializedData.Span, _options); - return result; - } -#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. -#pragma warning restore CS8603 // Possible null reference return. - - ReadOnlyMemory IDataSerializer.Serialize(T item) - { - string result = JsonSerializer.Serialize(item, _options); - return Encoding.UTF8.GetBytes(result).AsMemory(); - } - } -} \ No newline at end of file diff --git a/Weknow.EventSource.Backbone.Contracts/TelemetryrExtensions.cs b/Weknow.EventSource.Backbone.Contracts/TelemetryrExtensions.cs deleted file mode 100644 index f00ef7ee..00000000 --- a/Weknow.EventSource.Backbone.Contracts/TelemetryrExtensions.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Diagnostics; - -using OpenTelemetry.Trace; - -namespace Weknow.EventSource.Backbone -{ - public static class TelemetryrExtensions - { - /// - /// Adds the event consumer telemetry source (will result in tracing the consumer). - /// - /// The builder. - /// - public static TracerProviderBuilder ListenToEventSourceRedisChannel(this TracerProviderBuilder builder) => - builder.AddSource( - EventSourceConstants.REDIS_CONSUMER_CHANNEL_SOURCE, - EventSourceConstants.REDIS_PRODUCER_CHANNEL_SOURCE); - - #region InjectMetaTelemetryTags - - /// - /// Adds standard open-telemetry tags (for redis). - /// - /// The meta. - /// The activity. - public static void InjectMetaTelemetryTags(this Metadata meta, Activity? activity) - { - activity?.SetTag("event-source.partition", meta.Partition); - activity?.SetTag("event-source.shard", meta.Shard); - activity?.SetTag("event-source.operation", meta.Operation); - activity?.SetTag("event-source.message-id", meta.MessageId); - activity?.SetTag("event-source.channel-type", meta.ChannelType); - } - - #endregion // InjectMetaTelemetryTags - } -} diff --git a/Weknow.EventSource.Backbone.Contracts/Weknow.EventSource.Backbone.Contracts.csproj b/Weknow.EventSource.Backbone.Contracts/Weknow.EventSource.Backbone.Contracts.csproj deleted file mode 100644 index 861c2131..00000000 --- a/Weknow.EventSource.Backbone.Contracts/Weknow.EventSource.Backbone.Contracts.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - Weknow.EventSource.Backbone - - - - - - - - - - - - - - - diff --git a/Weknow.EventSource.Backbone.Contracts/Weknow.EventSource.Backbone.Contracts.xml b/Weknow.EventSource.Backbone.Contracts/Weknow.EventSource.Backbone.Contracts.xml deleted file mode 100644 index b43a8c8e..00000000 --- a/Weknow.EventSource.Backbone.Contracts/Weknow.EventSource.Backbone.Contracts.xml +++ /dev/null @@ -1,608 +0,0 @@ - - - - Weknow.EventSource.Backbone.Contracts - - - - - When true, it will only generate the contract interface(i.e. won't generate the bridge). - - - - - The name of the interface. - If missing the generator will use a convention. - - - - - The Namespace. - If missing the generator will use a convention. - - - - - Type of the generation - - - - - Empty - - - - - Prevents a default instance of the class from being created. - - - - - Initializes a new instance. - - The data. - - - - Adds an element with the specified key and value to the bucket. - - The key. - The value. - - - - - Adds an elements with the specified key and value to the bucket. - - - - - - - Adds the specified key/value pairs to the bucket. - - The bucket. - - - - - Adds an elements with the specified key and value to the bucket if the key doesn't exists. - - - - - - - Adds an elements with the specified key and value to the bucket if the key doesn't exists. - - - - - - - Adds the specified key/value pairs to the bucket if the key doesn't exists. - - The bucket. - - - - - Removes keys from the bucket. - - The keys. - - - - - Removes keys from the bucket. - - The keys. - - - - - Removes items from the bucket. - - The filter by key. - - - - - Determines whether the specified key contains key. - - The key. - - true if the specified key contains key; otherwise, false. - - - - - Gets the value associated with the specified key. - - The key. - The value. - - - - - Gets the value associated with the specified key. - - The key. - The value. - - - - - Gets the keys. - - - - - Performs an implicit conversion. - - The data. - - The result of the conversion. - - - - - Performs an implicit conversion. - - The instance. - - The result of the conversion. - - - - - Returns an enumerator that iterates through the collection. - - - An enumerator that can be used to iterate through the collection. - - - - - Returns an enumerator that iterates through a collection. - - - An object that can be used to iterate through the collection. - - - - - Non-generics form of announcement representation, - used to transfer data via channels. - - - - - Gets or sets the metadata. - - - - - Gets or sets the segments. - Segmentation is done at the sending side, - by Segmentation provider which can be register in order - to segments different parts of the messages. - The motivation of segmentation can come from regulation like - GDPR (right to erasure: https://gdpr-info.eu/). - - - Segmentation provider can split the message - into personal and non-personal segments. - - - - - Interception data (each interceptor responsible of it own data). - Interception can be use for variety of responsibilities like - flowing auth context or traces, producing metrics, etc. - - - - - Non-generics form of announcement representation, - used to transfer data via channels. - - - - - Gets or sets the segments. - Segmentation is done at the sending side, - by Segmentation provider which can be register in order - to segments different parts of the messages. - The motivation of segmentation can come from regulation like - GDPR (right to erasure: https://gdpr-info.eu/). - - - Segmentation provider can split the message - into personal and non-personal segments. - - - - - Performs an implicit conversion. - - The announcement. - - The result of the conversion. - - - - - - - Unlike the segments, this part can be flow with - message & will be set as async-context.]]> - - - - - The message identifier. - - - - - The sending time. - - - - - Gets or sets the operation. - - - - - Gets the origin environment of the message. - - - - - Gets or sets the partition. - - - - - Gets or sets the shard. - - - - - Gets or sets the shard. - - - - - Metadata extensions - - - - - Calculation of Duration since produce time - - - - - Gets the partition:shard as key. - - - - - Indicate whether it an empty metadata - - - - - Event Id - - - - - Initializes a new instance. - - The identifier. - - - - Converts to string. - - - A that represents this instance. - - - - - Performs an implicit conversion. - - The identifier. - - The result of the conversion. - - - - - Performs an implicit conversion. - - The identifier. - - The result of the conversion. - - - - - Performs an implicit conversion from to . - - The identifier. - - The result of the conversion. - - - - - Event Id - - - - - Initializes a new instance. - - The keys. - - - - Initializes a new instance. - - The keys. - - - - Initializes a new instance. - - The keys. - - - - Initializes a new instance. - - The keys. - - - - Gets the with the specified i. - - The i. - - - - - Returns an enumerator that iterates through a collection. - - - An object that can be used to iterate through the collection. - - - - - Returns an enumerator that iterates through the collection. - - - An enumerator that can be used to iterate through the collection. - - - - - - Converts to string. - - - A that represents this instance. - - - - - Performs an implicit conversion. - - The identifiers. - - The result of the conversion. - - - - - Performs an implicit conversion. - - The identifiers. - - The result of the conversion. - - - - - Performs an implicit conversion. - - The identifiers. - - The result of the conversion. - - - - - Performs an implicit conversion. - - The identifiers. - - The result of the conversion. - - - - - Performs an implicit conversion. - - The identifiers. - - The result of the conversion. - - - - - Performs an implicit conversion. - - The identifiers. - - The result of the conversion. - - - - - Performs an implicit conversion. - Expecting single result - - The identifiers. - - The result of the conversion. - - - - - - - Performs an implicit conversion from to . - - The identifiers. - - The result of the conversion. - - - - - Bucket storage type - - - - - Constants - - - - - The name of redis consumer channel source - - - - - The name of redis producer channel source - - - - - Best practice is to supply proper logger and - not using this class.Default logger. - This class use Trace logger just in case the other logger is missing. - - - - - - - The default - - - - - Begins a logical operation scope. - - The type of the state to begin scope for. - The identifier for the scope. - - An that ends the logical operation scope on dispose. - - - - - Checks if the given is enabled. - - level to be checked. - - true if enabled. - - - - - Writes a log entry. - - The type of the object to be written. - Entry will be written on this level. - Id of the event. - The entry to be written. Can be also an object. - The exception related to this entry. - Function to create a message of the and . - - - - - Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - - - - - Gets the serializer. - - - - - Enable to replace the default serialization - - - - - Serialize item. - - - The item. - - - - - Deserialize data. - - - The serialized data. - - - - - Unique name which represent the correlation - between the producer and consumer interceptor. - It's recommended to use URL format. - - - - - Json serializer (this is the default serializer) - - - - - - Adds the event consumer telemetry source (will result in tracing the consumer). - - The builder. - - - - - Adds standard open-telemetry tags (for redis). - - The meta. - The activity. - - - diff --git a/Weknow.EventSource.Backbone.Contracts/icon.png b/Weknow.EventSource.Backbone.Contracts/icon.png deleted file mode 100644 index d6811ad8..00000000 Binary files a/Weknow.EventSource.Backbone.Contracts/icon.png and /dev/null differ diff --git a/Weknow.EventSource.Backbone.Producers.Contracts/icon.png b/Weknow.EventSource.Backbone.Producers.Contracts/icon.png deleted file mode 100644 index d6811ad8..00000000 Binary files a/Weknow.EventSource.Backbone.Producers.Contracts/icon.png and /dev/null differ diff --git a/Weknow.EventSource.Backbone.sln b/Weknow.EventSource.Backbone.sln deleted file mode 100644 index defbd017..00000000 --- a/Weknow.EventSource.Backbone.sln +++ /dev/null @@ -1,179 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31825.309 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weknow.EventSource.Backbone", "Weknow.EventSource.Backbone\Weknow.EventSource.Backbone.csproj", "{52BFDB45-C61C-4FF8-98BA-41407FFC85F5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{4DE45B98-D18F-4E1D-9D2F-A4E40C11BC1F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weknow.EventSource.Backbone.UnitTests", "Tests\Weknow.EventSource.Backbone.UnitTests\Weknow.EventSource.Backbone.UnitTests.csproj", "{C1618305-4F0B-4AAA-8386-CA31DCBC5F59}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weknow.EventSource.Backbone.Contracts", "Weknow.EventSource.Backbone.Contracts\Weknow.EventSource.Backbone.Contracts.csproj", "{E17F4D78-8645-457E-B4CB-455FCED12F49}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Producers", "Producers", "{3FBE7FA1-700D-4B58-8569-15F533F761D1}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Consumers", "Consumers", "{EC34CEBD-EEDE-4CBA-A28A-A71BCB51D5D2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weknow.EventSource.Backbone.Producers.Contracts", "Producers\Weknow.EventSource.Backbone.Producers.Contracts\Weknow.EventSource.Backbone.Producers.Contracts.csproj", "{1FBD2827-BDF7-4988-89D0-8FDCF0DF2BC2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weknow.EventSource.Backbone.Consumers.Contracts", "Consumers\Weknow.EventSource.Backbone.Consumers.Contracts\Weknow.EventSource.Backbone.Consumers.Contracts.csproj", "{8BDB7BF5-69F3-44B1-BCBE-4A9680215FE9}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weknow.EventSource.Backbone.Consumers", "Consumers\Weknow.EventSource.Backbone.Consumers\Weknow.EventSource.Backbone.Consumers.csproj", "{6821E250-3BE8-4CDA-AFEA-FB8C9B4C4106}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weknow.EventSource.Backbone.Producers", "Producers\Weknow.EventSource.Backbone.Producers\Weknow.EventSource.Backbone.Producers.csproj", "{90B4EBE9-7CDB-42E0-B5E2-9FCAC4AD52CA}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Channels", "Channels", "{75CE8A6F-D63B-491D-8834-8D3BA2127208}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Redis", "Redis", "{64FC6860-BD4C-4C11-BF6E-E99BB4D91723}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weknow.EventSource.Backbone.IntegrationTests", "Tests\Weknow.EventSource.Backbone.IntegrationTests\Weknow.EventSource.Backbone.IntegrationTests.csproj", "{20A4298E-1929-4125-8D66-CABF330DE633}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weknow.EventSource.Backbone.Channels.RedisProvider.Common", "Channels\REDIS\Weknow.EventSource.Backbone.Channels.RedisProvider.Common\Weknow.EventSource.Backbone.Channels.RedisProvider.Common.csproj", "{68387B20-DC7B-4C56-906E-9EC379677DE3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weknow.EventSource.Backbone.Channels.RedisProducerProvider", "Channels\REDIS\Weknow.EventSource.Backbone.Channels.RedisProducerProvider\Weknow.EventSource.Backbone.Channels.RedisProducerProvider.csproj", "{C4D21A30-B556-4272-85BD-8F7793BD7432}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weknow.EventSource.Backbone.Channels.RedisConsumerProvider", "Channels\REDIS\Weknow.EventSource.Backbone.Channels.RedisConsumerProvider\Weknow.EventSource.Backbone.Channels.RedisConsumerProvider.csproj", "{ECC48028-F980-49E9-AAC1-65AF5AF3B16F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common", "Channels\S3\Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common\Weknow.EventSource.Backbone.Channels.S3StoreProvider.Common.csproj", "{54FA986A-47D9-4709-9C3E-AEB08CE73EBD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider", "Channels\S3\Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider\Weknow.EventSource.Backbone.Channels.S3StoreProducerProvider.csproj", "{686220F1-A72B-45FE-8A6C-148926973096}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider", "Channels\S3\Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider\Weknow.EventSource.Backbone.Channels.S3StoreConsumerProvider.csproj", "{39B1BBC2-1924-4B85-9BA4-3319F5C4F3E6}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6BD7580F-9EAE-4046-8EA9-10FBC8CF2C56}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - Directory.Build.props = Directory.Build.props - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "S3", "S3", "{1B19D36E-8348-4CAE-A2B8-87A81076539A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weknow.EventSource.Backbone.WebEventTest", "Tests\Weknow.EventSource.Backbone.WebEventTest\Weknow.EventSource.Backbone.WebEventTest.csproj", "{D648BE46-4208-4DEB-9F2F-0B009B603D0A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src-gen", "src-gen", "{2AE6684B-47A2-46D3-BE4E-C14B05255BB6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weknow.EventSource.Backbone.SrcGen", "src-gen\Weknow.EventSource.Backbone.SrcGen\Weknow.EventSource.Backbone.SrcGen.csproj", "{230DA8DF-00EE-47E2-A7B8-54F05A8966F6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weknow.EventSource.Backbone.SrcGen.Playground", "src-gen\Weknow.EventSource.Backbone.SrcGen.Playground\Weknow.EventSource.Backbone.SrcGen.Playground.csproj", "{49A8B718-DAB6-4D04-874C-0B1EA12826EB}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Gen|Any CPU = Gen|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {52BFDB45-C61C-4FF8-98BA-41407FFC85F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {52BFDB45-C61C-4FF8-98BA-41407FFC85F5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {52BFDB45-C61C-4FF8-98BA-41407FFC85F5}.Gen|Any CPU.ActiveCfg = Gen|Any CPU - {52BFDB45-C61C-4FF8-98BA-41407FFC85F5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {52BFDB45-C61C-4FF8-98BA-41407FFC85F5}.Release|Any CPU.Build.0 = Release|Any CPU - {C1618305-4F0B-4AAA-8386-CA31DCBC5F59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C1618305-4F0B-4AAA-8386-CA31DCBC5F59}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C1618305-4F0B-4AAA-8386-CA31DCBC5F59}.Gen|Any CPU.ActiveCfg = Gen|Any CPU - {C1618305-4F0B-4AAA-8386-CA31DCBC5F59}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C1618305-4F0B-4AAA-8386-CA31DCBC5F59}.Release|Any CPU.Build.0 = Release|Any CPU - {E17F4D78-8645-457E-B4CB-455FCED12F49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E17F4D78-8645-457E-B4CB-455FCED12F49}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E17F4D78-8645-457E-B4CB-455FCED12F49}.Gen|Any CPU.ActiveCfg = Gen|Any CPU - {E17F4D78-8645-457E-B4CB-455FCED12F49}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E17F4D78-8645-457E-B4CB-455FCED12F49}.Release|Any CPU.Build.0 = Release|Any CPU - {1FBD2827-BDF7-4988-89D0-8FDCF0DF2BC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1FBD2827-BDF7-4988-89D0-8FDCF0DF2BC2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1FBD2827-BDF7-4988-89D0-8FDCF0DF2BC2}.Gen|Any CPU.ActiveCfg = Gen|Any CPU - {1FBD2827-BDF7-4988-89D0-8FDCF0DF2BC2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1FBD2827-BDF7-4988-89D0-8FDCF0DF2BC2}.Release|Any CPU.Build.0 = Release|Any CPU - {8BDB7BF5-69F3-44B1-BCBE-4A9680215FE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8BDB7BF5-69F3-44B1-BCBE-4A9680215FE9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8BDB7BF5-69F3-44B1-BCBE-4A9680215FE9}.Gen|Any CPU.ActiveCfg = Gen|Any CPU - {8BDB7BF5-69F3-44B1-BCBE-4A9680215FE9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8BDB7BF5-69F3-44B1-BCBE-4A9680215FE9}.Release|Any CPU.Build.0 = Release|Any CPU - {6821E250-3BE8-4CDA-AFEA-FB8C9B4C4106}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6821E250-3BE8-4CDA-AFEA-FB8C9B4C4106}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6821E250-3BE8-4CDA-AFEA-FB8C9B4C4106}.Gen|Any CPU.ActiveCfg = Gen|Any CPU - {6821E250-3BE8-4CDA-AFEA-FB8C9B4C4106}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6821E250-3BE8-4CDA-AFEA-FB8C9B4C4106}.Release|Any CPU.Build.0 = Release|Any CPU - {90B4EBE9-7CDB-42E0-B5E2-9FCAC4AD52CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {90B4EBE9-7CDB-42E0-B5E2-9FCAC4AD52CA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {90B4EBE9-7CDB-42E0-B5E2-9FCAC4AD52CA}.Gen|Any CPU.ActiveCfg = Gen|Any CPU - {90B4EBE9-7CDB-42E0-B5E2-9FCAC4AD52CA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {90B4EBE9-7CDB-42E0-B5E2-9FCAC4AD52CA}.Release|Any CPU.Build.0 = Release|Any CPU - {20A4298E-1929-4125-8D66-CABF330DE633}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {20A4298E-1929-4125-8D66-CABF330DE633}.Debug|Any CPU.Build.0 = Debug|Any CPU - {20A4298E-1929-4125-8D66-CABF330DE633}.Gen|Any CPU.ActiveCfg = Gen|Any CPU - {20A4298E-1929-4125-8D66-CABF330DE633}.Release|Any CPU.ActiveCfg = Release|Any CPU - {20A4298E-1929-4125-8D66-CABF330DE633}.Release|Any CPU.Build.0 = Release|Any CPU - {68387B20-DC7B-4C56-906E-9EC379677DE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {68387B20-DC7B-4C56-906E-9EC379677DE3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {68387B20-DC7B-4C56-906E-9EC379677DE3}.Gen|Any CPU.ActiveCfg = Gen|Any CPU - {68387B20-DC7B-4C56-906E-9EC379677DE3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {68387B20-DC7B-4C56-906E-9EC379677DE3}.Release|Any CPU.Build.0 = Release|Any CPU - {C4D21A30-B556-4272-85BD-8F7793BD7432}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C4D21A30-B556-4272-85BD-8F7793BD7432}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C4D21A30-B556-4272-85BD-8F7793BD7432}.Gen|Any CPU.ActiveCfg = Gen|Any CPU - {C4D21A30-B556-4272-85BD-8F7793BD7432}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C4D21A30-B556-4272-85BD-8F7793BD7432}.Release|Any CPU.Build.0 = Release|Any CPU - {ECC48028-F980-49E9-AAC1-65AF5AF3B16F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ECC48028-F980-49E9-AAC1-65AF5AF3B16F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ECC48028-F980-49E9-AAC1-65AF5AF3B16F}.Gen|Any CPU.ActiveCfg = Gen|Any CPU - {ECC48028-F980-49E9-AAC1-65AF5AF3B16F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ECC48028-F980-49E9-AAC1-65AF5AF3B16F}.Release|Any CPU.Build.0 = Release|Any CPU - {54FA986A-47D9-4709-9C3E-AEB08CE73EBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {54FA986A-47D9-4709-9C3E-AEB08CE73EBD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {54FA986A-47D9-4709-9C3E-AEB08CE73EBD}.Gen|Any CPU.ActiveCfg = Gen|Any CPU - {54FA986A-47D9-4709-9C3E-AEB08CE73EBD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {54FA986A-47D9-4709-9C3E-AEB08CE73EBD}.Release|Any CPU.Build.0 = Release|Any CPU - {686220F1-A72B-45FE-8A6C-148926973096}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {686220F1-A72B-45FE-8A6C-148926973096}.Debug|Any CPU.Build.0 = Debug|Any CPU - {686220F1-A72B-45FE-8A6C-148926973096}.Gen|Any CPU.ActiveCfg = Gen|Any CPU - {686220F1-A72B-45FE-8A6C-148926973096}.Release|Any CPU.ActiveCfg = Release|Any CPU - {686220F1-A72B-45FE-8A6C-148926973096}.Release|Any CPU.Build.0 = Release|Any CPU - {39B1BBC2-1924-4B85-9BA4-3319F5C4F3E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {39B1BBC2-1924-4B85-9BA4-3319F5C4F3E6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {39B1BBC2-1924-4B85-9BA4-3319F5C4F3E6}.Gen|Any CPU.ActiveCfg = Gen|Any CPU - {39B1BBC2-1924-4B85-9BA4-3319F5C4F3E6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {39B1BBC2-1924-4B85-9BA4-3319F5C4F3E6}.Release|Any CPU.Build.0 = Release|Any CPU - {D648BE46-4208-4DEB-9F2F-0B009B603D0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D648BE46-4208-4DEB-9F2F-0B009B603D0A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D648BE46-4208-4DEB-9F2F-0B009B603D0A}.Gen|Any CPU.ActiveCfg = Gen|Any CPU - {D648BE46-4208-4DEB-9F2F-0B009B603D0A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D648BE46-4208-4DEB-9F2F-0B009B603D0A}.Release|Any CPU.Build.0 = Release|Any CPU - {230DA8DF-00EE-47E2-A7B8-54F05A8966F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {230DA8DF-00EE-47E2-A7B8-54F05A8966F6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {230DA8DF-00EE-47E2-A7B8-54F05A8966F6}.Gen|Any CPU.ActiveCfg = Gen|Any CPU - {230DA8DF-00EE-47E2-A7B8-54F05A8966F6}.Gen|Any CPU.Build.0 = Gen|Any CPU - {230DA8DF-00EE-47E2-A7B8-54F05A8966F6}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {230DA8DF-00EE-47E2-A7B8-54F05A8966F6}.Release|Any CPU.Build.0 = Debug|Any CPU - {49A8B718-DAB6-4D04-874C-0B1EA12826EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {49A8B718-DAB6-4D04-874C-0B1EA12826EB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {49A8B718-DAB6-4D04-874C-0B1EA12826EB}.Gen|Any CPU.ActiveCfg = Gen|Any CPU - {49A8B718-DAB6-4D04-874C-0B1EA12826EB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {49A8B718-DAB6-4D04-874C-0B1EA12826EB}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {C1618305-4F0B-4AAA-8386-CA31DCBC5F59} = {4DE45B98-D18F-4E1D-9D2F-A4E40C11BC1F} - {1FBD2827-BDF7-4988-89D0-8FDCF0DF2BC2} = {3FBE7FA1-700D-4B58-8569-15F533F761D1} - {8BDB7BF5-69F3-44B1-BCBE-4A9680215FE9} = {EC34CEBD-EEDE-4CBA-A28A-A71BCB51D5D2} - {6821E250-3BE8-4CDA-AFEA-FB8C9B4C4106} = {EC34CEBD-EEDE-4CBA-A28A-A71BCB51D5D2} - {90B4EBE9-7CDB-42E0-B5E2-9FCAC4AD52CA} = {3FBE7FA1-700D-4B58-8569-15F533F761D1} - {64FC6860-BD4C-4C11-BF6E-E99BB4D91723} = {75CE8A6F-D63B-491D-8834-8D3BA2127208} - {20A4298E-1929-4125-8D66-CABF330DE633} = {4DE45B98-D18F-4E1D-9D2F-A4E40C11BC1F} - {68387B20-DC7B-4C56-906E-9EC379677DE3} = {64FC6860-BD4C-4C11-BF6E-E99BB4D91723} - {C4D21A30-B556-4272-85BD-8F7793BD7432} = {64FC6860-BD4C-4C11-BF6E-E99BB4D91723} - {ECC48028-F980-49E9-AAC1-65AF5AF3B16F} = {64FC6860-BD4C-4C11-BF6E-E99BB4D91723} - {54FA986A-47D9-4709-9C3E-AEB08CE73EBD} = {1B19D36E-8348-4CAE-A2B8-87A81076539A} - {686220F1-A72B-45FE-8A6C-148926973096} = {1B19D36E-8348-4CAE-A2B8-87A81076539A} - {39B1BBC2-1924-4B85-9BA4-3319F5C4F3E6} = {1B19D36E-8348-4CAE-A2B8-87A81076539A} - {1B19D36E-8348-4CAE-A2B8-87A81076539A} = {75CE8A6F-D63B-491D-8834-8D3BA2127208} - {D648BE46-4208-4DEB-9F2F-0B009B603D0A} = {4DE45B98-D18F-4E1D-9D2F-A4E40C11BC1F} - {230DA8DF-00EE-47E2-A7B8-54F05A8966F6} = {2AE6684B-47A2-46D3-BE4E-C14B05255BB6} - {49A8B718-DAB6-4D04-874C-0B1EA12826EB} = {2AE6684B-47A2-46D3-BE4E-C14B05255BB6} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {103E4CA1-859C-4E6A-9F2D-7FD54A8E687C} - EndGlobalSection -EndGlobal diff --git a/Weknow.EventSource.Backbone/Weknow.EventSource.Backbone.csproj b/Weknow.EventSource.Backbone/Weknow.EventSource.Backbone.csproj deleted file mode 100644 index 2faf234c..00000000 --- a/Weknow.EventSource.Backbone/Weknow.EventSource.Backbone.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/Weknow.EventSource.Backbone/icon.png b/Weknow.EventSource.Backbone/icon.png deleted file mode 100644 index d6811ad8..00000000 Binary files a/Weknow.EventSource.Backbone/icon.png and /dev/null differ diff --git a/src-gen/EventSourcing.Backbone.SrcGen.Playground/EventSourcing.Backbone.SrcGen.Playground.csproj b/src-gen/EventSourcing.Backbone.SrcGen.Playground/EventSourcing.Backbone.SrcGen.Playground.csproj new file mode 100644 index 00000000..a18ce92e --- /dev/null +++ b/src-gen/EventSourcing.Backbone.SrcGen.Playground/EventSourcing.Backbone.SrcGen.Playground.csproj @@ -0,0 +1,19 @@ + + + + Exe + Debug;Release;Gen + False + + + + + + + + + + + + + diff --git a/src-gen/EventSourcing.Backbone.SrcGen.Playground/ISample.cs b/src-gen/EventSourcing.Backbone.SrcGen.Playground/ISample.cs new file mode 100644 index 00000000..91dd7f88 --- /dev/null +++ b/src-gen/EventSourcing.Backbone.SrcGen.Playground/ISample.cs @@ -0,0 +1,19 @@ +namespace EventSourcing.Backbone.SrcGen.Playground +{ + /// + /// Some doc + /// + [EventsContract(EventsContractType.Producer)] + [EventsContract(EventsContractType.Consumer)] + public interface ISample + { + /// + /// Execute. + /// + /// The i. + /// The operation. + /// The notes. + /// + ValueTask ExecAsync(int i, string operation, params string[] notes); + } +} diff --git a/src-gen/EventSourcing.Backbone.SrcGen.Playground/Inheritance/IFlowA.cs b/src-gen/EventSourcing.Backbone.SrcGen.Playground/Inheritance/IFlowA.cs new file mode 100644 index 00000000..82b42d10 --- /dev/null +++ b/src-gen/EventSourcing.Backbone.SrcGen.Playground/Inheritance/IFlowA.cs @@ -0,0 +1,8 @@ +namespace EventSourcing.Backbone.UnitTests.Entities; + +[EventsContract(EventsContractType.Producer)] +[EventsContract(EventsContractType.Consumer)] +public interface IFlowA +{ + ValueTask AAsync(int id); +} diff --git a/src-gen/EventSourcing.Backbone.SrcGen.Playground/Inheritance/IFlowAB.cs b/src-gen/EventSourcing.Backbone.SrcGen.Playground/Inheritance/IFlowAB.cs new file mode 100644 index 00000000..3016e266 --- /dev/null +++ b/src-gen/EventSourcing.Backbone.SrcGen.Playground/Inheritance/IFlowAB.cs @@ -0,0 +1,7 @@ +namespace EventSourcing.Backbone.UnitTests.Entities; + +[EventsContract(EventsContractType.Producer)] +[EventsContract(EventsContractType.Consumer)] +public interface IFlowAB : IFlowA, IFlowB +{ +} diff --git a/src-gen/EventSourcing.Backbone.SrcGen.Playground/Inheritance/IFlowB.cs b/src-gen/EventSourcing.Backbone.SrcGen.Playground/Inheritance/IFlowB.cs new file mode 100644 index 00000000..a8a36e10 --- /dev/null +++ b/src-gen/EventSourcing.Backbone.SrcGen.Playground/Inheritance/IFlowB.cs @@ -0,0 +1,8 @@ +namespace EventSourcing.Backbone.UnitTests.Entities; + +[EventsContract(EventsContractType.Producer)] +[EventsContract(EventsContractType.Consumer)] +public interface IFlowB +{ + ValueTask BAsync(DateTimeOffset date); +} diff --git a/src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/Program.cs b/src-gen/EventSourcing.Backbone.SrcGen.Playground/Program.cs similarity index 72% rename from src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/Program.cs rename to src-gen/EventSourcing.Backbone.SrcGen.Playground/Program.cs index 15022f2b..8db12a7d 100644 --- a/src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/Program.cs +++ b/src-gen/EventSourcing.Backbone.SrcGen.Playground/Program.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.SrcGen.Playground +namespace EventSourcing.Backbone.SrcGen.Playground { internal class Program { diff --git a/src-gen/EventSourcing.Backbone.SrcGen.Playground/icon.png b/src-gen/EventSourcing.Backbone.SrcGen.Playground/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/src-gen/EventSourcing.Backbone.SrcGen.Playground/icon.png differ diff --git a/src-gen/Weknow.EventSource.Backbone.SrcGen/Weknow.EventSource.Backbone.SrcGen.csproj b/src-gen/EventSourcing.Backbone.SrcGen/EventSourcing.Backbone.SrcGen.csproj similarity index 79% rename from src-gen/Weknow.EventSource.Backbone.SrcGen/Weknow.EventSource.Backbone.SrcGen.csproj rename to src-gen/EventSourcing.Backbone.SrcGen/EventSourcing.Backbone.SrcGen.csproj index e8706c04..68f70643 100644 --- a/src-gen/Weknow.EventSource.Backbone.SrcGen/Weknow.EventSource.Backbone.SrcGen.csproj +++ b/src-gen/EventSourcing.Backbone.SrcGen/EventSourcing.Backbone.SrcGen.csproj @@ -7,16 +7,27 @@ enable true false - Code generator for Weknow.EventSource.Backbone + Code generator for EventSourcing.Backbone Debug;Release;Gen + + README.md + + + + + True + \ + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src-gen/Weknow.EventSource.Backbone.SrcGen/Generators/Concrete/BridgeIncrementalGenerator.cs b/src-gen/EventSourcing.Backbone.SrcGen/Generators/Concrete/BridgeIncrementalGenerator.cs similarity index 80% rename from src-gen/Weknow.EventSource.Backbone.SrcGen/Generators/Concrete/BridgeIncrementalGenerator.cs rename to src-gen/EventSourcing.Backbone.SrcGen/Generators/Concrete/BridgeIncrementalGenerator.cs index a3b69db2..7819c328 100644 --- a/src-gen/Weknow.EventSource.Backbone.SrcGen/Generators/Concrete/BridgeIncrementalGenerator.cs +++ b/src-gen/EventSourcing.Backbone.SrcGen/Generators/Concrete/BridgeIncrementalGenerator.cs @@ -1,18 +1,17 @@ using System.Reflection; using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; +using EventSourcing.Backbone.SrcGen.Generators.Entities; +using EventSourcing.Backbone.SrcGen.Generators.EntitiesAndHelpers; -using Weknow.EventSource.Backbone.SrcGen.Generators.Entities; -using Weknow.EventSource.Backbone.SrcGen.Generators.EntitiesAndHelpers; +using Microsoft.CodeAnalysis; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { - [Generator] + //[Generator] internal class BridgeIncrementalGenerator : GeneratorIncrementalBase { - private const string TARGET_ATTRIBUTE = "GenerateEventSourceBridge"; + private const string TARGET_ATTRIBUTE = "EventsContractBridge"; public BridgeIncrementalGenerator() : base(TARGET_ATTRIBUTE) { @@ -24,21 +23,18 @@ public BridgeIncrementalGenerator() : base(TARGET_ATTRIBUTE) protected override GenInstruction[] OnGenerate( SourceProductionContext context, Compilation compilation, - SyntaxReceiverResult info) + SyntaxReceiverResult info, + string[] usingStatements) { string interfaceName = info.FormatName(); var builder = new StringBuilder(); - var (item, att, symbol, name, kind, suffix, ns, isProducer) = info; - - var verrideInterfaceArg = att.ArgumentList?.Arguments.FirstOrDefault(m => m.NameEquals?.Name.Identifier.ValueText == "InterfaceName"); - var overrideInterfaceName = verrideInterfaceArg?.Expression.NormalizeWhitespace().ToString().Replace("\"", ""); if (info.Kind == "Producer") { - var file = OnGenerateProducer(builder, info, interfaceName); + var file = OnGenerateProducer(builder, info, interfaceName, usingStatements); return new[] { new GenInstruction(file, builder.ToString()) }; } - return OnGenerateConsumers(info, interfaceName); + return OnGenerateConsumers(info, interfaceName, usingStatements); } @@ -47,8 +43,9 @@ protected override GenInstruction[] OnGenerate( #region OnGenerateConsumers protected GenInstruction[] OnGenerateConsumers( - SyntaxReceiverResult info, - string interfaceName) + SyntaxReceiverResult info, + string interfaceName, + string[] usingStatements) { string generateFrom = info.FormatName(); string prefix = (info.Name ?? interfaceName).StartsWith("I") && @@ -83,9 +80,6 @@ protected GenInstruction OnGenerateConsumerBridgeExtensions( AssemblyName assemblyName) { var builder = new StringBuilder(); - var (item, att, symbol, name, kind, suffix, ns, isProducer) = info; - - // CopyDocumentation(builder, kind, item, "\t"); string bridge = $"{prefix}Bridge"; string fileName = $"{bridge}Extensions"; @@ -97,6 +91,20 @@ protected GenInstruction OnGenerateConsumerBridgeExtensions( builder.AppendLine($"\tpublic static class {fileName}"); builder.AppendLine("\t{"); + builder.AppendLine("\t\t/// "); + builder.AppendLine($"\t\t/// Subscribe to {interfaceName}"); + builder.AppendLine("\t\t/// "); + builder.AppendLine("\t\t/// The builder."); + builder.AppendLine("\t\t/// The targets handler."); + builder.AppendLine($"\t\tpublic static IConsumerLifetime Subscribe{prefix}("); + builder.AppendLine("\t\t\t\tthis IConsumerSubscribtionHubBuilder source,"); + builder.AppendLine($"\t\t\t\t{interfaceName} target)"); + builder.AppendLine("\t\t{"); + builder.AppendLine($"\t\t\tvar bridge = new {bridge}(target);"); + builder.AppendLine("\t\t\treturn source.Subscribe(bridge);"); + builder.AppendLine("\t\t}"); + builder.AppendLine(); + builder.AppendLine("\t\t/// "); builder.AppendLine($"\t\t/// Subscribe to {interfaceName}"); builder.AppendLine("\t\t/// "); @@ -141,9 +149,7 @@ protected GenInstruction OnGenerateConsumerBridge( AssemblyName assemblyName) { var builder = new StringBuilder(); - var (item, att, symbol, name, kind, suffix, ns, isProducer) = info; - - // CopyDocumentation(builder, kind, item, "\t"); + var symbol = info.Symbol; string fileName = $"{prefix}Bridge"; @@ -163,7 +169,7 @@ protected GenInstruction OnGenerateConsumerBridge( builder.AppendLine("\t\t/// The target."); builder.AppendLine($"\t\tpublic {fileName}({interfaceName} target)"); builder.AppendLine("\t\t{"); - builder.AppendLine("\t\t\t_targets = target.ToYield();"); + builder.AppendLine("\t\t\t_targets = target.ToEnumerable();"); builder.AppendLine("\t\t}"); builder.AppendLine(); @@ -196,11 +202,12 @@ protected GenInstruction OnGenerateConsumerBridge( builder.AppendLine("\t\t{"); if (allMethods.Length != 0) { + builder.AppendLine("\t\t\tConsumerMetadata consumerMetadata = ConsumerMetadata.Context;"); builder.AppendLine("\t\t\tswitch (announcement.Metadata.Operation)"); builder.AppendLine("\t\t\t{"); foreach (var method in allMethods) { - string mtdName = method.Name; + string mtdName = method.ToNameConvention(); string mtdType = method.ContainingType.Name; mtdType = info.FormatName(mtdType); builder.AppendLine($"\t\t\t\tcase nameof({mtdType}.{mtdName}):"); @@ -214,8 +221,8 @@ protected GenInstruction OnGenerateConsumerBridge( i++; } IEnumerable ps = Enumerable.Range(0, prms.Length).Select(m => $"p{m}"); - - builder.AppendLine($"\t\t\t\t\tvar tasks = _targets.Select(async target => await target.{mtdName}({string.Join(", ", ps)}));"); + string metaParam = ps.Any() ? "consumerMetadata, " : "consumerMetadata"; + builder.AppendLine($"\t\t\t\t\tvar tasks = _targets.Select(async target => await target.{mtdName}({metaParam}{string.Join(", ", ps)}));"); builder.AppendLine("\t\t\t\t\tawait Task.WhenAll(tasks);"); builder.AppendLine("\t\t\t\t\treturn true;"); builder.AppendLine("\t\t\t\t}"); @@ -243,9 +250,7 @@ protected GenInstruction OnGenerateConsumerBase( AssemblyName assemblyName) { var builder = new StringBuilder(); - var (item, att, symbol, name, kind, suffix, ns, isProducer) = info; - - // CopyDocumentation(builder, kind, item, "\t"); + var symbol = info.Symbol; string fileName = $"{prefix}Base"; @@ -254,7 +259,6 @@ protected GenInstruction OnGenerateConsumerBase( builder.AppendLine("\t\t/// "); builder.AppendLine($"\t\t/// Base Subscription class of {interfaceName}"); builder.AppendLine("\t\t/// "); - builder.AppendLine($"\t\t[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]"); builder.AppendLine($"\t\t[GeneratedCode(\"{assemblyName.Name}\",\"{assemblyName.Version}\")]"); builder.AppendLine($"\t\tpublic abstract class {fileName}: ISubscriptionBridge"); builder.AppendLine("\t\t{"); @@ -267,11 +271,12 @@ protected GenInstruction OnGenerateConsumerBase( builder.AppendLine("\t\t\t{"); if (allMethods.Length != 0) { + builder.AppendLine("\t\t\t\tConsumerMetadata consumerMetadata = ConsumerMetadata.Context;"); builder.AppendLine("\t\t\t\tswitch (announcement.Metadata.Operation)"); builder.AppendLine("\t\t\t\t{"); foreach (var method in allMethods) { - string mtdName = method.Name; + string mtdName = method.ToNameConvention(); string mtdType = method.ContainingType.Name; mtdType = info.FormatName(mtdType); builder.AppendLine($"\t\t\t\t\tcase nameof({mtdType}.{mtdName}):"); @@ -284,8 +289,9 @@ protected GenInstruction OnGenerateConsumerBase( builder.AppendLine($"\t\t\t\t\t\tvar p{i} = await consumerBridge.GetParameterAsync<{p.Type}>(announcement, \"{pName}\");"); i++; } + string metaParam = prms.Any() ? "consumerMetadata, " : "consumerMetadata"; IEnumerable ps = Enumerable.Range(0, prms.Length).Select(m => $"p{m}"); - builder.AppendLine($"\t\t\t\t\t\tawait {mtdName}({string.Join(", ", ps)});"); + builder.AppendLine($"\t\t\t\t\t\tawait {mtdName}({metaParam}{string.Join(", ", ps)});"); builder.AppendLine("\t\t\t\t\t\treturn true;"); builder.AppendLine("\t\t\t\t\t}"); } @@ -299,12 +305,12 @@ protected GenInstruction OnGenerateConsumerBase( builder.AppendLine(); foreach (var method in allMethods) { - string mtdName = method.Name; + string mtdName = method.ToNameConvention(); - //CopyDocumentation(builder, kind, mds); var prms = method.Parameters; IEnumerable ps = prms.Select(p => $"{p.Type} {p.Name}"); - builder.AppendLine($"\t\t\tprotected abstract ValueTask {mtdName}({string.Join(", ", ps)});"); ; + string metaParam = ps.Any() ? "ConsumerMetadata consumerMetadata, " : "ConsumerMetadata consumerMetadata"; + builder.AppendLine($"\t\t\tprotected abstract ValueTask {mtdName}({metaParam}{string.Join(", ", ps)});"); builder.AppendLine(); } builder.AppendLine("\t\t}"); @@ -318,14 +324,23 @@ protected GenInstruction OnGenerateConsumerBase( #region OnGenerateProducer protected string OnGenerateProducer( - StringBuilder builder, - SyntaxReceiverResult info, - string interfaceName) + StringBuilder builder, + SyntaxReceiverResult info, + string interfaceName, + string[] usingStatements) { - var (item, att, symbol, name, kind, suffix, ns, isProducer) = info; - builder.AppendLine("\tusing Weknow.EventSource.Backbone.Building;"); + var symbol = info.Symbol; + var kind = info.Kind; + var hash = new HashSet(); + hash.Add("using EventSourcing.Backbone.Building;"); + builder.AppendLine("\tusing EventSourcing.Backbone.Building;"); + foreach (var u in usingStatements) + { + if (hash.Contains(u)) + continue; + builder.AppendLine(u); + } - // CopyDocumentation(builder, kind, item, "\t"); string prefix = interfaceName.StartsWith("I") && interfaceName.Length > 1 && char.IsUpper(interfaceName[1]) ? interfaceName.Substring(1) : interfaceName; @@ -386,7 +401,7 @@ private static void GenerateProducerMethods( SyntaxReceiverResult info, IMethodSymbol mds) { - string mtdName = mds.Name; + string mtdName = mds.ToNameConvention(); string interfaceName = mds.ContainingType.Name; interfaceName = info.FormatName(interfaceName); builder.Append("\t\tasync ValueTask"); @@ -398,17 +413,24 @@ private static void GenerateProducerMethods( builder.Append(string.Join(", ", ps)); builder.AppendLine(")"); builder.AppendLine("\t\t{"); - builder.AppendLine($"\t\t\tvar operation = nameof({interfaceName}.{mtdName});"); + builder.AppendLine($"\t\t\tvar operation_ = nameof({interfaceName}.{mtdName});"); int i = 0; var prms = mds.Parameters; - foreach (var p in prms) + foreach (var pName in from p in prms + let pName = p.Name + select pName) { - var pName = p.Name; - builder.AppendLine($"\t\t\tvar classification{i} = CreateClassificationAdaptor(operation, nameof({pName}), {pName});"); + builder.AppendLine($"\t\t\tvar classification_{i}_ = CreateClassificationAdaptor(operation_, nameof({pName}), {pName});"); i++; } - var classifications = Enumerable.Range(0, prms.Length).Select(m => $"classification{m}"); - builder.AppendLine($"\t\t\treturn await SendAsync(operation, {string.Join(", ", classifications)});"); + + var classifications = Enumerable.Range(0, prms.Length).Select(m => $"classification_{m}_"); + + builder.Append($"\t\t\treturn await SendAsync(operation_"); + if (classifications.Any()) + builder.AppendLine($", {string.Join(", ", classifications)});"); + else + builder.AppendLine($");"); builder.AppendLine("\t\t}"); builder.AppendLine(); } diff --git a/src-gen/Weknow.EventSource.Backbone.SrcGen/Generators/Concrete/ContractIncrementalGenerator.cs b/src-gen/EventSourcing.Backbone.SrcGen/Generators/Concrete/ContractIncrementalGenerator.cs similarity index 52% rename from src-gen/Weknow.EventSource.Backbone.SrcGen/Generators/Concrete/ContractIncrementalGenerator.cs rename to src-gen/EventSourcing.Backbone.SrcGen/Generators/Concrete/ContractIncrementalGenerator.cs index 43c59947..cc87b20d 100644 --- a/src-gen/Weknow.EventSource.Backbone.SrcGen/Generators/Concrete/ContractIncrementalGenerator.cs +++ b/src-gen/EventSourcing.Backbone.SrcGen/Generators/Concrete/ContractIncrementalGenerator.cs @@ -1,22 +1,22 @@ using System.Text; +using EventSourcing.Backbone.SrcGen.Generators.Entities; + using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Weknow.EventSource.Backbone.SrcGen.Generators.Entities; - -using static Weknow.EventSource.Backbone.Helper; +using static EventSourcing.Backbone.Helper; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { [Generator] - internal class ContractncrementalGenerator : GeneratorIncrementalBase + internal class ContractIncrementalGenerator : GeneratorIncrementalBase { - private const string TARGET_ATTRIBUTE = "GenerateEventSource"; + private const string TARGET_ATTRIBUTE = "EventsContract"; private readonly BridgeIncrementalGenerator _bridge = new(); - public ContractncrementalGenerator() : base(TARGET_ATTRIBUTE) + public ContractIncrementalGenerator() : base(TARGET_ATTRIBUTE) { } @@ -25,19 +25,20 @@ public ContractncrementalGenerator() : base(TARGET_ATTRIBUTE) /// Called when [execute]. ///
/// The context. + /// /// The information. - /// Name of the interface. - /// /// /// File name /// protected override GenInstruction[] OnGenerate( SourceProductionContext context, Compilation compilation, - SyntaxReceiverResult info) + SyntaxReceiverResult info, + string[] usingStatements) { - - var (type, att, symbol, kind, ns, isProducer) = info; +#pragma warning disable S1481 // Unused local variables should be removed + var (type, att, symbol, kind, ns, isProducer, @using) = info; +#pragma warning restore S1481 // Unused local variables should be removed string interfaceName = info.FormatName(); var builder = new StringBuilder(); CopyDocumentation(builder, kind, type, "\t"); @@ -56,17 +57,34 @@ protected override GenInstruction[] OnGenerate( { if (method is MethodDeclarationSyntax mds) { - CopyDocumentation(builder, kind, mds); - + var sb = new StringBuilder(); + CopyDocumentation(sb, kind, mds); + var ps = mds.ParameterList.Parameters.Select(GetParameter); + if (sb.Length != 0 && !isProducer && ps.Any()) + { + string summaryEnds = "///
"; + int idxRet = sb.ToString().IndexOf(summaryEnds); + if (idxRet != -1) + sb.Insert(idxRet + summaryEnds.Length, "\r\n\t\t/// The consumer metadata."); + } + builder.Append(sb); builder.Append("\t\tValueTask"); if (isProducer) builder.Append(""); - builder.Append($" {mds.Identifier.ValueText}("); + builder.AppendLine($" {mds.ToNameConvention()}("); - var ps = mds.ParameterList.Parameters.Select(p => $"\r\n\t\t\t{p.Type} {p.Identifier.ValueText}"); + if (!isProducer) + { + builder.Append("\t\t\t"); + builder.Append("ConsumerMetadata consumerMetadata"); + if (ps.Any()) + builder.Append(','); + } builder.Append("\t\t\t"); - builder.Append(string.Join(", ", ps)); +#pragma warning disable RS1035 // Do not use APIs banned for analyzers + builder.Append(string.Join(",", ps)); +#pragma warning restore RS1035 // Do not use APIs banned for analyzers builder.AppendLine(");"); builder.AppendLine(); } @@ -76,11 +94,18 @@ protected override GenInstruction[] OnGenerate( var contractOnlyArg = att.ArgumentList?.Arguments.FirstOrDefault(m => m.NameEquals?.Name.Identifier.ValueText == "ContractOnly"); var contractOnly = contractOnlyArg?.Expression.NormalizeWhitespace().ToString() == "true"; - //info = info.OverrideName(interfaceName); if (!contractOnly) _bridge.GenerateSingle(context, compilation, info); return new[] { new GenInstruction(interfaceName, builder.ToString()) }; + + string GetParameter(ParameterSyntax p) + { + var mod = p.Modifiers.FirstOrDefault(); + string modifier = mod == null ? string.Empty : $" {mod} "; + var result = $"\r\n\t\t\t{modifier}{p.Type} {p.Identifier.ValueText}"; + return result; + } } } } diff --git a/src-gen/Weknow.EventSource.Backbone.SrcGen/Generators/EntitiesAndHelpers/EntityGenerator.cs b/src-gen/EventSourcing.Backbone.SrcGen/Generators/EntitiesAndHelpers/EntityGenerator.cs similarity index 93% rename from src-gen/Weknow.EventSource.Backbone.SrcGen/Generators/EntitiesAndHelpers/EntityGenerator.cs rename to src-gen/EventSourcing.Backbone.SrcGen/Generators/EntitiesAndHelpers/EntityGenerator.cs index 09142323..6545f807 100644 --- a/src-gen/Weknow.EventSource.Backbone.SrcGen/Generators/EntitiesAndHelpers/EntityGenerator.cs +++ b/src-gen/EventSourcing.Backbone.SrcGen/Generators/EntitiesAndHelpers/EntityGenerator.cs @@ -1,14 +1,14 @@ using System.Reflection; using System.Text; +using EventSourcing.Backbone.SrcGen.Generators.Entities; + using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Weknow.EventSource.Backbone.SrcGen.Generators.Entities; - -using static Weknow.EventSource.Backbone.Helper; +using static EventSourcing.Backbone.Helper; -namespace Weknow.EventSource.Backbone.SrcGen.Generators.EntitiesAndHelpers +namespace EventSourcing.Backbone.SrcGen.Generators.EntitiesAndHelpers { internal static class EntityGenerator { @@ -32,7 +32,7 @@ internal static GenInstruction[] GenerateEntities( AssemblyName assemblyName) { - var (item, att, symbol, name, kind, suffix, ns, isProducer) = info; + var (item, symbol, kind, ns, usingStatements) = info; var results = new List(); foreach (var method in item.Members) @@ -46,7 +46,7 @@ internal static GenInstruction[] GenerateEntities( if (recordPrefix.EndsWith(nameof(KindFilter.Consumer))) recordPrefix = recordPrefix.Substring(0, recordPrefix.Length - nameof(KindFilter.Consumer).Length); - string mtdName = mds.Identifier.ValueText; + string mtdName = mds.ToNameConvention(); if (mtdName.EndsWith("Async")) mtdName = mtdName.Substring(0, mtdName.Length - 5); builder.AppendLine($"\t[GeneratedCode(\"{assemblyName.Name}\",\"{assemblyName.Version}\")]"); @@ -106,7 +106,7 @@ internal static GenInstruction GenerateEntityMapper( AssemblyName assemblyName) { var builder = new StringBuilder(); - var (item, att, symbol, name, kind, suffix, ns, isProducer) = info; + var (item, symbol, kind, ns, @using) = info; // CopyDocumentation(builder, kind, item, "\t"); @@ -152,20 +152,20 @@ internal static GenInstruction GenerateEntityMapper( builder.AppendLine($"\t\t\t\t\tIConsumerPlan consumerPlan)"); builder.AppendLine($"\t\t\t\t\t\t where TCast : {interfaceName}_EntityFamily"); builder.AppendLine("\t\t\t{"); - builder.AppendLine("\t\t\t\tvar operation = announcement.Metadata.Operation;"); + builder.AppendLine("\t\t\t\tvar operation_ = announcement.Metadata.Operation;"); int j = 0; foreach (var method in item.Members) { if (method is not MethodDeclarationSyntax mds) continue; - string mtdName = mds.Identifier.ValueText; + string mtdName = mds.ToNameConvention(); string recordSuffix = mtdName.EndsWith("Async") ? mtdName.Substring(0, mtdName.Length - 5) : mtdName; string fullRecordName = $"{recordPrefix}_{recordSuffix}"; string nameOfOperetion = $"{info.Name ?? interfaceName}.{mtdName}"; string ifOrElseIf = j++ > 0 ? "else if" : "if"; - builder.AppendLine($"\t\t\t\t{ifOrElseIf}(operation == nameof({nameOfOperetion}))"); + builder.AppendLine($"\t\t\t\t{ifOrElseIf}(operation_ == nameof({nameOfOperetion}))"); builder.AppendLine("\t\t\t\t{"); builder.AppendLine($"\t\t\t\t\tif(typeof(TCast) == typeof({fullRecordName}))"); builder.AppendLine("\t\t\t\t\t{"); @@ -208,7 +208,6 @@ internal static GenInstruction GenerateEntityMapperExtensions( AssemblyName assemblyName) { var builder = new StringBuilder(); - var (item, att, symbol, name, kind, suffix, ns, isProducer) = info; // CopyDocumentation(builder, kind, item, "\t"); diff --git a/src-gen/Weknow.EventSource.Backbone.SrcGen/Generators/EntitiesAndHelpers/GenInstruction.cs b/src-gen/EventSourcing.Backbone.SrcGen/Generators/EntitiesAndHelpers/GenInstruction.cs similarity index 91% rename from src-gen/Weknow.EventSource.Backbone.SrcGen/Generators/EntitiesAndHelpers/GenInstruction.cs rename to src-gen/EventSourcing.Backbone.SrcGen/Generators/EntitiesAndHelpers/GenInstruction.cs index 844c44eb..fb3852e5 100644 --- a/src-gen/Weknow.EventSource.Backbone.SrcGen/Generators/EntitiesAndHelpers/GenInstruction.cs +++ b/src-gen/EventSourcing.Backbone.SrcGen/Generators/EntitiesAndHelpers/GenInstruction.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.SrcGen.Generators.Entities +namespace EventSourcing.Backbone.SrcGen.Generators.Entities { internal class GenInstruction { diff --git a/src-gen/Weknow.EventSource.Backbone.SrcGen/Generators/EntitiesAndHelpers/KindFilter.cs b/src-gen/EventSourcing.Backbone.SrcGen/Generators/EntitiesAndHelpers/KindFilter.cs similarity index 57% rename from src-gen/Weknow.EventSource.Backbone.SrcGen/Generators/EntitiesAndHelpers/KindFilter.cs rename to src-gen/EventSourcing.Backbone.SrcGen/Generators/EntitiesAndHelpers/KindFilter.cs index 4492db8d..8e1281c4 100644 --- a/src-gen/Weknow.EventSource.Backbone.SrcGen/Generators/EntitiesAndHelpers/KindFilter.cs +++ b/src-gen/EventSourcing.Backbone.SrcGen/Generators/EntitiesAndHelpers/KindFilter.cs @@ -1,4 +1,4 @@ -namespace Weknow.EventSource.Backbone.SrcGen.Generators.Entities +namespace EventSourcing.Backbone.SrcGen.Generators.Entities { internal enum KindFilter { diff --git a/src-gen/Weknow.EventSource.Backbone.SrcGen/Generators/GeneratorIncrementalBase.cs b/src-gen/EventSourcing.Backbone.SrcGen/Generators/GeneratorIncrementalBase.cs similarity index 85% rename from src-gen/Weknow.EventSource.Backbone.SrcGen/Generators/GeneratorIncrementalBase.cs rename to src-gen/EventSourcing.Backbone.SrcGen/Generators/GeneratorIncrementalBase.cs index 8685addc..3b806871 100644 --- a/src-gen/Weknow.EventSource.Backbone.SrcGen/Generators/GeneratorIncrementalBase.cs +++ b/src-gen/EventSourcing.Backbone.SrcGen/Generators/GeneratorIncrementalBase.cs @@ -1,15 +1,15 @@ using System.Collections.Immutable; using System.Text; +using EventSourcing.Backbone.SrcGen.Generators.Entities; + using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Weknow.EventSource.Backbone.SrcGen.Generators.Entities; - -using static Weknow.EventSource.Backbone.Helper; +using static EventSourcing.Backbone.Helper; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { internal abstract class GeneratorIncrementalBase : IIncrementalGenerator @@ -22,8 +22,8 @@ internal abstract class GeneratorIncrementalBase : IIncrementalGenerator "System.Collections.Generic", "System.Threading.Tasks", "System.CodeDom.Compiler", - "Weknow.EventSource.Backbone", - "Weknow.EventSource.Backbone.Building" }.Select(u => $"using {u};").ToArray(); + "EventSourcing.Backbone", + "EventSourcing.Backbone.Building" }.Select(u => $"using {u};").ToArray(); private readonly ImmutableHashSet _targetAttribute = ImmutableHashSet.Empty; #region Ctor @@ -149,7 +149,7 @@ public void GenerateSingle( Compilation compilation, SyntaxReceiverResult info) { - var (item, symbol, att, name, kind, suffix, ns, isProducer) = info; + var (item, symbol, kind, ns, usingStatements) = info; #region Validation @@ -161,7 +161,7 @@ public void GenerateSingle( #endregion // Validation - GenInstruction[] codes = OnGenerate(context, compilation, info); + GenInstruction[] codes = OnGenerate(context, compilation, info, usingStatements); foreach (var (fileName, content, dynamicNs, usn) in codes) { @@ -171,32 +171,32 @@ public void GenerateSingle( builder.AppendLine(); builder.AppendLine("#nullable enable"); - foreach (var u in usn) + foreach (var u in usingStatements.Concat(usn)) { if (!usingSet.Contains(u)) usingSet.Add(u); } - var overrideNS = dynamicNs ?? ns; - if (overrideNS == null && item.Parent is NamespaceDeclarationSyntax ns_) - { - foreach (var c in ns_?.Parent?.ChildNodes() ?? Array.Empty()) - { - if (c is UsingDirectiveSyntax use) - { - var u = use.ToFullString().Trim(); - if (!usingSet.Contains(u)) - usingSet.Add(u); - } - } - builder.AppendLine(); - overrideNS = ns_?.Name?.ToString(); - } + var overrideNS = dynamicNs ?? ns ?? symbol.ContainingNamespace.ToDisplayString(); + //if (overrideNS == null && item.Parent is BaseNamespaceDeclarationSyntax ns_) + //{ + // foreach (var c in ns_?.Parent?.ChildNodes() ?? Array.Empty()) + // { + // if (c is UsingDirectiveSyntax use) + // { + // var u = use.ToFullString().Trim(); + // if (!usingSet.Contains(u)) + // usingSet.Add(u); + // } + // } + // builder.AppendLine(); + // overrideNS = ns_?.Name?.ToString(); + //} foreach (var u in usingSet.OrderBy(m => m)) { builder.AppendLine(u); } - builder.AppendLine($"namespace {overrideNS ?? "Weknow.EventSource.Backbone"}"); + builder.AppendLine($"namespace {overrideNS ?? "EventSourcing.Backbone"}"); builder.AppendLine("{"); builder.AppendLine(content); builder.AppendLine("}"); @@ -213,16 +213,17 @@ public void GenerateSingle( /// Called when [execute]. ///
/// The context. + /// The compilation. /// The information. - /// Name of the interface. - /// Source of the generation. + /// The using statements. /// /// File name /// protected abstract GenInstruction[] OnGenerate( SourceProductionContext context, Compilation compilation, - SyntaxReceiverResult info); + SyntaxReceiverResult info, + string[] usingStatements); #endregion // OnGenerate diff --git a/src-gen/Weknow.EventSource.Backbone.SrcGen/Helper.cs b/src-gen/EventSourcing.Backbone.SrcGen/Helper.cs similarity index 98% rename from src-gen/Weknow.EventSource.Backbone.SrcGen/Helper.cs rename to src-gen/EventSourcing.Backbone.SrcGen/Helper.cs index ee3c0258..fcadcf94 100644 --- a/src-gen/Weknow.EventSource.Backbone.SrcGen/Helper.cs +++ b/src-gen/EventSourcing.Backbone.SrcGen/Helper.cs @@ -3,7 +3,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -namespace Weknow.EventSource.Backbone +namespace EventSourcing.Backbone { /// /// diff --git a/src-gen/EventSourcing.Backbone.SrcGen/Properties/launchSettings.json b/src-gen/EventSourcing.Backbone.SrcGen/Properties/launchSettings.json new file mode 100644 index 00000000..b818d10a --- /dev/null +++ b/src-gen/EventSourcing.Backbone.SrcGen/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + ".EventSourcing.Backbone.SrcGen": { + "commandName": "DebugRoslynComponent", + "targetProject": "..\\EventSourcing.Backbone.SrcGen.Playground\\EventSourcing.Backbone.SrcGen.Playground.csproj" + } + } +} \ No newline at end of file diff --git a/src-gen/Weknow.EventSource.Backbone.SrcGen/RoslynHelper.cs b/src-gen/EventSourcing.Backbone.SrcGen/RoslynHelper.cs similarity index 62% rename from src-gen/Weknow.EventSource.Backbone.SrcGen/RoslynHelper.cs rename to src-gen/EventSourcing.Backbone.SrcGen/RoslynHelper.cs index 1e8a07a8..5fc23e8d 100644 --- a/src-gen/Weknow.EventSource.Backbone.SrcGen/RoslynHelper.cs +++ b/src-gen/EventSourcing.Backbone.SrcGen/RoslynHelper.cs @@ -1,4 +1,6 @@ -namespace Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.CodeAnalysis; internal static class RoslynHelper { @@ -66,4 +68,53 @@ public static IEnumerable GetAllMethods(this ITypeSymbol type) } #endregion // GetAllMethods + + #region GetUsing + + /// + /// Gets the using statements. + /// + /// The syntax node. + /// + public static IEnumerable GetUsing(this SyntaxNode syntaxNode) + { + if (syntaxNode is CompilationUnitSyntax m) + { + foreach (var u in m.Usings) + { + var match = u.ToString(); + yield return match; + } + } + + if (syntaxNode.Parent == null) + yield break; + + foreach (var u in GetUsing(syntaxNode.Parent)) + { + yield return u; + } + } + + #endregion // GetUsing + + #region ToNameConvention + + public static string ToNameConvention(this MethodDeclarationSyntax method) + { + string name = method.Identifier.ValueText; + if (name.EndsWith("Async")) + return name; + return $"{name}Async"; + } + + public static string ToNameConvention(this IMethodSymbol method) + { + string name = method.Name; + if (name.EndsWith("Async")) + return name; + return $"{name}Async"; + } + + #endregion // ToNameConvention } diff --git a/src-gen/Weknow.EventSource.Backbone.SrcGen/SyntaxReceiverResult.cs b/src-gen/EventSourcing.Backbone.SrcGen/SyntaxReceiverResult.cs similarity index 70% rename from src-gen/Weknow.EventSource.Backbone.SrcGen/SyntaxReceiverResult.cs rename to src-gen/EventSourcing.Backbone.SrcGen/SyntaxReceiverResult.cs index 21f00319..125588bf 100644 --- a/src-gen/Weknow.EventSource.Backbone.SrcGen/SyntaxReceiverResult.cs +++ b/src-gen/EventSourcing.Backbone.SrcGen/SyntaxReceiverResult.cs @@ -1,7 +1,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Weknow.EventSource.Backbone; +namespace EventSourcing.Backbone; ///// ///// Syntax Receiver Result Extensions @@ -46,10 +46,11 @@ public SyntaxReceiverResult( Type = type; Kind = kind; Name = name; - Namespace = ns; + Namespace = ns ?? symbol?.ContainingNamespace?.ToDisplayString(); Att = att; Symbol = symbol ?? throw new NullReferenceException(nameof(symbol)); GenerateFrom = type.Identifier.ValueText; + Using = att.GetUsing().ToArray(); } /// @@ -100,6 +101,11 @@ public SyntaxReceiverResult( /// public INamedTypeSymbol Symbol { get; } + /// + /// Gets the using statements. + /// + public string[] Using { get; } + #region FormatName public string FormatName() @@ -120,6 +126,52 @@ public string FormatName(string generateFrom) #region Deconstruct + /// + /// Deconstruct the specified type. + /// + /// The type. + /// The attribute. + /// The symbol. + /// The kind. + /// The namespace. + /// The using statement. + public void Deconstruct(out TypeDeclarationSyntax type, + out INamedTypeSymbol symbol, + out string kind, + out string? ns, + out string[] usingStatement) + { + type = Type; + symbol = Symbol; + kind = Kind; + ns = Namespace; + usingStatement = Using; + } + + /// + /// Deconstruct the specified type. + /// + /// The type. + /// The attribute. + /// The symbol. + /// The kind. + /// The namespace. + /// The using statement. + public void Deconstruct(out TypeDeclarationSyntax type, + out AttributeSyntax att, + out INamedTypeSymbol symbol, + out string kind, + out string? ns, + out string[] usingStatement) + { + type = Type; + symbol = Symbol; + att = Att; + kind = Kind; + ns = Namespace; + usingStatement = Using; + } + /// /// Deconstruct the specified type. /// @@ -135,7 +187,8 @@ public void Deconstruct(out TypeDeclarationSyntax type, out INamedTypeSymbol symbol, out string kind, out string? ns, - out bool isProducer) + out bool isProducer, + out string[] usingStatement) { type = Type; symbol = Symbol; @@ -143,6 +196,7 @@ public void Deconstruct(out TypeDeclarationSyntax type, kind = Kind; ns = Namespace; isProducer = IsProducer; + usingStatement = Using; } /// @@ -162,7 +216,8 @@ public void Deconstruct(out TypeDeclarationSyntax type, out string kind, out string suffix, out string? ns, - out bool isProducer) + out bool isProducer, + out string[] usingStatement) { type = Type; symbol = Symbol; @@ -172,6 +227,7 @@ public void Deconstruct(out TypeDeclarationSyntax type, suffix = Suffix; ns = Namespace; isProducer = IsProducer; + usingStatement = Using; } #endregion // Deconstruct diff --git a/src-gen/EventSourcing.Backbone.SrcGen/icon.png b/src-gen/EventSourcing.Backbone.SrcGen/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/src-gen/EventSourcing.Backbone.SrcGen/icon.png differ diff --git a/src-gen/PlayGroundAbstraction/IFoo.cs b/src-gen/PlayGroundAbstraction/IFoo.cs new file mode 100644 index 00000000..0573edc2 --- /dev/null +++ b/src-gen/PlayGroundAbstraction/IFoo.cs @@ -0,0 +1,14 @@ +using EventSourcing.Backbone; + +using PlayGroundAbstraction.Common.Local; + +namespace PlayGroundAbstraction.Common; + + + +[EventsContract(EventsContractType.Producer)] +[EventsContract(EventsContractType.Consumer)] +public interface IFoo +{ + ValueTask Run(User user); +} \ No newline at end of file diff --git a/src-gen/PlayGroundAbstraction/PlayGroundAbstraction.csproj b/src-gen/PlayGroundAbstraction/PlayGroundAbstraction.csproj new file mode 100644 index 00000000..4a003241 --- /dev/null +++ b/src-gen/PlayGroundAbstraction/PlayGroundAbstraction.csproj @@ -0,0 +1,31 @@ + + + + net7.0 + enable + enable + False + + + + + + + + + + True + + + + + + + + + + + + + + diff --git a/src-gen/PlayGroundAbstraction/User.cs b/src-gen/PlayGroundAbstraction/User.cs new file mode 100644 index 00000000..1eba984c --- /dev/null +++ b/src-gen/PlayGroundAbstraction/User.cs @@ -0,0 +1,7 @@ +namespace PlayGroundAbstraction.Common.Local +{ + public record User + { + public required string Name { get; init; } + } +} \ No newline at end of file diff --git a/src-gen/PlayGroundAbstraction/icon.png b/src-gen/PlayGroundAbstraction/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/src-gen/PlayGroundAbstraction/icon.png differ diff --git a/src-gen/PlayGroundProducer/IFooY.cs b/src-gen/PlayGroundProducer/IFooY.cs new file mode 100644 index 00000000..decd71dc --- /dev/null +++ b/src-gen/PlayGroundProducer/IFooY.cs @@ -0,0 +1,7 @@ +namespace PlayGroundAbstraction; + +//[EventsContract(EventsContractType.Producer)] +public interface IFooY : IFoo +{ + +} \ No newline at end of file diff --git a/src-gen/PlayGroundProducer/PlayGroundProducer.csproj b/src-gen/PlayGroundProducer/PlayGroundProducer.csproj new file mode 100644 index 00000000..9ca3a8d7 --- /dev/null +++ b/src-gen/PlayGroundProducer/PlayGroundProducer.csproj @@ -0,0 +1,28 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + + + True + + + + + + + + + + diff --git a/src-gen/PlayGroundProducer/Program.cs b/src-gen/PlayGroundProducer/Program.cs new file mode 100644 index 00000000..3751555c --- /dev/null +++ b/src-gen/PlayGroundProducer/Program.cs @@ -0,0 +1,2 @@ +// See https://aka.ms/new-console-template for more information +Console.WriteLine("Hello, World!"); diff --git a/src-gen/PlayGroundProducer/icon.png b/src-gen/PlayGroundProducer/icon.png new file mode 100644 index 00000000..17d68338 Binary files /dev/null and b/src-gen/PlayGroundProducer/icon.png differ diff --git a/src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/ISample.cs b/src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/ISample.cs deleted file mode 100644 index 427f1192..00000000 --- a/src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/ISample.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Weknow.EventSource.Backbone.SrcGen.Playground -{ - /// - /// Some doc - /// - [GenerateEventSource(EventSourceGenType.Producer)] - [GenerateEventSource(EventSourceGenType.Consumer)] - public interface ISample - { - /// - /// Execute. - /// - /// The i. - /// The s. - /// - ValueTask ExecAsync(int i, string s); - } -} diff --git a/src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/Inheritance/IFlowA.cs b/src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/Inheritance/IFlowA.cs deleted file mode 100644 index 57c06983..00000000 --- a/src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/Inheritance/IFlowA.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Weknow.EventSource.Backbone.UnitTests.Entities; - -[GenerateEventSource(EventSourceGenType.Producer)] -[GenerateEventSource(EventSourceGenType.Consumer)] -public interface IFlowA -{ - ValueTask AAsync(int id); -} diff --git a/src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/Inheritance/IFlowAB.cs b/src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/Inheritance/IFlowAB.cs deleted file mode 100644 index d8665008..00000000 --- a/src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/Inheritance/IFlowAB.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Weknow.EventSource.Backbone.UnitTests.Entities; - -[GenerateEventSource(EventSourceGenType.Producer)] -[GenerateEventSource(EventSourceGenType.Consumer)] -public interface IFlowAB : IFlowA, IFlowB -{ -} diff --git a/src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/Inheritance/IFlowB.cs b/src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/Inheritance/IFlowB.cs deleted file mode 100644 index 33f3d379..00000000 --- a/src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/Inheritance/IFlowB.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Weknow.EventSource.Backbone.UnitTests.Entities; - -[GenerateEventSource(EventSourceGenType.Producer)] -[GenerateEventSource(EventSourceGenType.Consumer)] -public interface IFlowB -{ - ValueTask BAsync(DateTimeOffset date); -} diff --git a/src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/Weknow.EventSource.Backbone.SrcGen.Playground.csproj b/src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/Weknow.EventSource.Backbone.SrcGen.Playground.csproj deleted file mode 100644 index 73028bce..00000000 --- a/src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/Weknow.EventSource.Backbone.SrcGen.Playground.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - Exe - Debug;Release;Gen - True - - - - - - - - - - - - - - - - - - diff --git a/src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/icon.png b/src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/icon.png deleted file mode 100644 index d6811ad8..00000000 Binary files a/src-gen/Weknow.EventSource.Backbone.SrcGen.Playground/icon.png and /dev/null differ diff --git a/src-gen/Weknow.EventSource.Backbone.SrcGen/Properties/launchSettings.json b/src-gen/Weknow.EventSource.Backbone.SrcGen/Properties/launchSettings.json deleted file mode 100644 index 8a3a0d9f..00000000 --- a/src-gen/Weknow.EventSource.Backbone.SrcGen/Properties/launchSettings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "profiles": { - "Weknow.EventSource.Backbone.SrcGen": { - "commandName": "DebugRoslynComponent", - "targetProject": "..\\Weknow.EventSource.Backbone.SrcGen.Playground\\Weknow.EventSource.Backbone.SrcGen.Playground.csproj" - } - } -} \ No newline at end of file diff --git a/src-gen/Weknow.EventSource.Backbone.SrcGen/icon.png b/src-gen/Weknow.EventSource.Backbone.SrcGen/icon.png deleted file mode 100644 index d6811ad8..00000000 Binary files a/src-gen/Weknow.EventSource.Backbone.SrcGen/icon.png and /dev/null differ