diff --git a/SW.Bitween.Api/Data/BitweenDbContext.cs b/SW.Bitween.Api/Data/BitweenDbContext.cs index 86e2cf43..cadd4704 100644 --- a/SW.Bitween.Api/Data/BitweenDbContext.cs +++ b/SW.Bitween.Api/Data/BitweenDbContext.cs @@ -138,10 +138,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Restrict); bgr.HasOne(p => p.Partner).WithMany().HasForeignKey(p => p.PartnerId) .IsRequired(false).OnDelete(DeleteBehavior.Restrict); - bgr.Property(p => p.MatchExpression).HasConversion( - domainObject => - domainObject == null ? null : MatchSpecValueConverter.SerializeMatchSpec(domainObject), - dbString => dbString == null ? null : MatchSpecValueConverter.DeserializeMatchSpec(dbString)); + bgr.Property(p => p.MatchExpression).HasMatchExpressionConversion(); }); modelBuilder.Entity(gav => @@ -218,10 +215,29 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasConstraintName("FK_Subscriptions_AggFor").OnDelete(DeleteBehavior.Restrict); b.HasOne(i => i.Category).WithMany().HasForeignKey(i => i.CategoryId); b.HasOne(i => i.WorkGroup).WithMany().HasForeignKey(i => i.WorkGroupId); - b.Property(p => p.MatchExpression).HasConversion( - domainObject => - domainObject == null ? null : MatchSpecValueConverter.SerializeMatchSpec(domainObject), - dbString => dbString == null ? null : MatchSpecValueConverter.DeserializeMatchSpec(dbString)); + b.HasOne(i => i.RetryPolicy).WithMany().HasForeignKey(i => i.RetryPolicyId).IsRequired(false) + .OnDelete(DeleteBehavior.SetNull); + b.Property(p => p.CustomRetryPolicy).StoreAsJson(); + b.Property(p => p.MatchExpression).HasMatchExpressionConversion(); + }); + + modelBuilder.Entity(b => + { + b.ToTable("RetryPolicies"); + b.HasKey(p => p.Id); + b.Property(p => p.Id).ValueGeneratedOnAdd(); + b.Property(p => p.Name).IsRequired().HasMaxLength(200); + b.Property(p => p.Groups).StoreAsJson(); + }); + + modelBuilder.Entity(b => + { + b.ToTable("DelayedRetries"); + b.HasKey(p => p.Id); + b.Property(p => p.Id).IsUnicode(false).HasMaxLength(50); + b.Property(p => p.On); + b.Property(p => p.GroupAttemptCounts).StoreAsJson(); + b.HasIndex(p => p.On); }); modelBuilder.Entity(b => @@ -236,6 +252,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.HandlerId).HasMaxLength(200).IsUnicode(false); b.Property(p => p.HandlerProperties).StoreAsJson(); b.Property(p => p.MapperProperties).StoreAsJson(); + b.Property(p => p.GroupAttemptCounts).StoreAsJson(); b.Property(p => p.InputContentType).IsUnicode(false).HasMaxLength(200); b.Property(p => p.ResponseMessageTypeName).IsUnicode(false).HasMaxLength(500); diff --git a/SW.Bitween.Api/Domain/DelayedRetry.cs b/SW.Bitween.Api/Domain/DelayedRetry.cs new file mode 100644 index 00000000..c744320a --- /dev/null +++ b/SW.Bitween.Api/Domain/DelayedRetry.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Domain; +// Id should be the same for xchangeId when retry happens the record is deleted +public class DelayedRetry : BaseEntity +{ + public DateTime On { get; set; } + public Dictionary GroupAttemptCounts { get; set; } = new(); +} diff --git a/SW.Bitween.Api/Domain/Notifier.cs b/SW.Bitween.Api/Domain/Notifier.cs index dbb8b527..0aa4027e 100644 --- a/SW.Bitween.Api/Domain/Notifier.cs +++ b/SW.Bitween.Api/Domain/Notifier.cs @@ -1,46 +1,47 @@ using System.Collections.Generic; +using System.Runtime.Intrinsics.X86; using SW.PrimitiveTypes; -namespace SW.Bitween.Domain + +namespace SW.Bitween.Domain; + +public class Notifier:BaseEntity { - public class Notifier:BaseEntity - { - public Notifier(string name) - { - Name = name; - Inactive = false; - } + public Notifier(string name) + { + Name = name; + Inactive = false; + } - public string Name { get; set; } - public bool RunOnSuccessfulResult { get; set; } - public bool RunOnBadResult { get; set; } - public bool RunOnFailedResult { get; set; } - public string HandlerId { get; set; } - public bool Inactive { get; set; } - public IReadOnlyDictionary HandlerProperties { get; private set; } + public string Name { get; set; } + public bool RunOnSuccessfulResult { get; set; } + public bool RunOnBadResult { get; set; } + public bool RunOnFailedResult { get; set; } + public string HandlerId { get; set; } + public bool Inactive { get; set; } + public IReadOnlyDictionary HandlerProperties { get; private set; } - public int[] RunOnSubscriptions { get; set; } + public int[] RunOnSubscriptions { get; set; } - public void Update(string name, bool runOnSuccessfulResult, bool runOnBadResult, bool runOnFailedResult, string handlerId,bool inactive, int[] runOnSubscriptions) - { - Name = name; - RunOnSuccessfulResult = runOnSuccessfulResult; - RunOnBadResult = runOnBadResult; - RunOnFailedResult = runOnFailedResult; - HandlerId = handlerId; - Inactive = inactive; - RunOnSubscriptions = runOnSubscriptions; - } - public void SetDictionaries( - IReadOnlyDictionary handler - ) - { - HandlerProperties = handler; - } + public void Update(string name, bool runOnSuccessfulResult, bool runOnBadResult, bool runOnFailedResult, string handlerId,bool inactive, int[] runOnSubscriptions) + { + Name = name; + RunOnSuccessfulResult = runOnSuccessfulResult; + RunOnBadResult = runOnBadResult; + RunOnFailedResult = runOnFailedResult; + HandlerId = handlerId; + Inactive = inactive; + RunOnSubscriptions = runOnSubscriptions; + } + public void SetDictionaries( + IReadOnlyDictionary handler + ) + { + HandlerProperties = handler; + } - } } \ No newline at end of file diff --git a/SW.Bitween.Api/Domain/RetryPolicy.cs b/SW.Bitween.Api/Domain/RetryPolicy.cs new file mode 100644 index 00000000..c1793896 --- /dev/null +++ b/SW.Bitween.Api/Domain/RetryPolicy.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Domain; +// templates of retry policy to choose on subscriptions +public class RetryPolicy : BaseEntity, IAudited, IRetryPolicy +{ + public string Name { get; set; } + public List Groups { get; set; } = []; + public DateTime CreatedOn { get; set; } + public string CreatedBy { get; set; } + public DateTime? ModifiedOn { get; set; } + public string ModifiedBy { get; set; } +} \ No newline at end of file diff --git a/SW.Bitween.Api/Domain/Subscription/Subscription.cs b/SW.Bitween.Api/Domain/Subscription/Subscription.cs index 7d74103a..04c3caeb 100644 --- a/SW.Bitween.Api/Domain/Subscription/Subscription.cs +++ b/SW.Bitween.Api/Domain/Subscription/Subscription.cs @@ -5,169 +5,192 @@ using System.Collections.Generic; -namespace SW.Bitween.Domain +namespace SW.Bitween.Domain; + +public class Subscription : BaseEntity { - public class Subscription : BaseEntity + public Subscription() { - public Subscription() - { - } + } - //receiving - public Subscription(string name, int documentId) : this(name, documentId, SubscriptionType.Receiving, null) - { - Inactive = true; - } + //receiving + public Subscription(string name, int documentId) : this(name, documentId, SubscriptionType.Receiving, null) + { + Inactive = true; + } - //aggregation - public Subscription(string name, int aggregationFor, int partnerId) : this(name, Document.AggregationDocumentId, - SubscriptionType.Aggregation, partnerId, aggregationFor) - { - Inactive = true; - } + //aggregation + public Subscription(string name, int aggregationFor, int partnerId) : this(name, Document.AggregationDocumentId, + SubscriptionType.Aggregation, partnerId, aggregationFor) + { + Inactive = true; + } - //apiresult or filter - public Subscription(string name, int documentId, SubscriptionType type, int partnerId) : this(name, documentId, - type, partnerId, null) - { - Inactive = true; - if (!(type == SubscriptionType.ApiCall || type == SubscriptionType.Internal)) - throw new ArgumentException(); - } + //apiresult or filter + public Subscription(string name, int documentId, SubscriptionType type, int partnerId) : this(name, documentId, + type, partnerId, null) + { + Inactive = true; + if (!(type == SubscriptionType.ApiCall || type == SubscriptionType.Internal)) + throw new ArgumentException(); + } - public Subscription(string name, int documentId, SubscriptionType type) : this(name, documentId, - type, null) - { - Inactive = true; - if (type != SubscriptionType.GatewayApiCall && type != SubscriptionType.BusGateway) - throw new ArgumentException(); - } - private Subscription(string name, int documentId, SubscriptionType type, int? partnerId = null, - int? aggregationForId = null, bool temporary = false) - { - Inactive = true; - AggregationForId = aggregationForId; - PartnerId = partnerId; - Name = name ?? throw new ArgumentNullException(nameof(name)); - DocumentId = documentId; - Type = type; - _Schedules = new HashSet(); - HandlerProperties = new Dictionary(); - MapperProperties = new Dictionary(); - ReceiverProperties = new Dictionary(); - ValidatorProperties = new Dictionary(); - DocumentFilter = new Dictionary(); - Temporary = temporary; - WorkGroup = null; - } + public Subscription(string name, int documentId, SubscriptionType type) : this(name, documentId, + type, null) + { + Inactive = true; + if (type != SubscriptionType.GatewayApiCall && type != SubscriptionType.BusGateway) + throw new ArgumentException(); + } + private Subscription(string name, int documentId, SubscriptionType type, int? partnerId = null, + int? aggregationForId = null, bool temporary = false) + { + Inactive = true; + AggregationForId = aggregationForId; + PartnerId = partnerId; + Name = name ?? throw new ArgumentNullException(nameof(name)); + DocumentId = documentId; + Type = type; + _Schedules = new HashSet(); + HandlerProperties = new Dictionary(); + MapperProperties = new Dictionary(); + ReceiverProperties = new Dictionary(); + ValidatorProperties = new Dictionary(); + DocumentFilter = new Dictionary(); + Temporary = temporary; + WorkGroup = null; + } - public string Name { get; set; } - public int DocumentId { get; private set; } - public SubscriptionType Type { get; private set; } - public int? PartnerId { get; private set; } - public int? CategoryId { get; set; } - public SubscriptionCategory Category { get; set; } - public int? WorkGroupId { get; set; } - public WorkGroup WorkGroup { get; set; } - public bool Temporary { get; private set; } - public DateTime? PausedOn { get; private set; } - public string ValidatorId { get; set; } - public string HandlerId { get; set; } - public string ReceiverId { get; set; } - - public string MapperId { get; set; } - public IReadOnlyDictionary ValidatorProperties { get; private set; } - public IReadOnlyDictionary HandlerProperties { get; private set; } - public IReadOnlyDictionary MapperProperties { get; private set; } - public IReadOnlyDictionary ReceiverProperties { get; private set; } - public IReadOnlyDictionary DocumentFilter { get; private set; } - - public IPropertyMatchSpecification MatchExpression { get; private set; } - public bool IsRunning { get; set; } - public bool Inactive { get; set; } - public int? ResponseSubscriptionId { get; set; } - public string ResponseMessageTypeName { get; set; } - public int? AggregationForId { get; private set; } - public XchangeFileType AggregationTarget { get; set; } - public DateTime? AggregateOn { get; private set; } - public int ConsecutiveFailures { get; private set; } - public string LastException { get; private set; } - - readonly HashSet _Schedules; - public IReadOnlyCollection Schedules => _Schedules; - public DateTime? ReceiveOn { get; private set; } - - - public void SetAggregateNow() - { - AggregateOn = DateTime.UtcNow.AddMinutes(-1); - } + public string Name { get; set; } + public int DocumentId { get; private set; } + public SubscriptionType Type { get; private set; } + public int? PartnerId { get; private set; } + public int? CategoryId { get; set; } + public SubscriptionCategory Category { get; set; } + public int? WorkGroupId { get; set; } + public RetryPolicy RetryPolicy { get; private set; } + public int? RetryPolicyId { get; private set; } + public CustomRetryPolicy CustomRetryPolicy { get; private set; } + public WorkGroup WorkGroup { get; set; } + public bool Temporary { get; private set; } + public DateTime? PausedOn { get; private set; } + public string ValidatorId { get; set; } + public string HandlerId { get; set; } + public string ReceiverId { get; set; } + + public string MapperId { get; set; } + public IReadOnlyDictionary ValidatorProperties { get; private set; } + public IReadOnlyDictionary HandlerProperties { get; private set; } + public IReadOnlyDictionary MapperProperties { get; private set; } + public IReadOnlyDictionary ReceiverProperties { get; private set; } + public IReadOnlyDictionary DocumentFilter { get; private set; } + + public IPropertyMatchSpecification MatchExpression { get; private set; } + public bool IsRunning { get; set; } + public bool Inactive { get; set; } + public int? ResponseSubscriptionId { get; set; } + public string ResponseMessageTypeName { get; set; } + public int? AggregationForId { get; private set; } + public XchangeFileType AggregationTarget { get; set; } + public DateTime? AggregateOn { get; private set; } + public int ConsecutiveFailures { get; private set; } + public string LastException { get; private set; } + + readonly HashSet _Schedules; + public IReadOnlyCollection Schedules => _Schedules; + public DateTime? ReceiveOn { get; private set; } + + + public void SetAggregateNow() + { + AggregateOn = DateTime.UtcNow.AddMinutes(-1); + } - public void SetDictionaries( - IReadOnlyDictionary handler, - IReadOnlyDictionary mapper, - IReadOnlyDictionary receiver, - IReadOnlyDictionary document, - IReadOnlyDictionary validator - ) - { - HandlerProperties = handler; - MapperProperties = mapper; - ReceiverProperties = receiver; - ValidatorProperties = validator; - DocumentFilter = document; - } + public void SetDictionaries( + IReadOnlyDictionary handler, + IReadOnlyDictionary mapper, + IReadOnlyDictionary receiver, + IReadOnlyDictionary document, + IReadOnlyDictionary validator + ) + { + HandlerProperties = handler; + MapperProperties = mapper; + ReceiverProperties = receiver; + ValidatorProperties = validator; + DocumentFilter = document; + } - public void SetSchedules(IEnumerable schedules = null) + public void SetSchedules(IEnumerable schedules = null) + { + if (Type == SubscriptionType.Receiving) { - if (Type == SubscriptionType.Receiving) - { - if (schedules != null) _Schedules.Update(schedules); - ReceiveOn = _Schedules.Next() ?? throw new BitweenException("Invalid schedule."); - } - else if (Type == SubscriptionType.Aggregation) - { - if (schedules != null) _Schedules.Update(schedules); - AggregateOn = _Schedules.Next() ?? throw new BitweenException("Invalid schedule."); - } + if (schedules != null) _Schedules.Update(schedules); + ReceiveOn = _Schedules.Next() ?? throw new BitweenException("Invalid schedule."); } - - public void SetReceiveNow() + else if (Type == SubscriptionType.Aggregation) { - ReceiveOn = DateTime.UtcNow.AddMinutes(-1); + if (schedules != null) _Schedules.Update(schedules); + AggregateOn = _Schedules.Next() ?? throw new BitweenException("Invalid schedule."); } + } + + public void SetReceiveNow() + { + ReceiveOn = DateTime.UtcNow.AddMinutes(-1); + } - public void SetHealth(string exception = null) + public void SetHealth(string exception = null) + { + if (exception == null) { - if (exception == null) - { - ConsecutiveFailures = 0; - LastException = null; - return; - } - - ConsecutiveFailures += 1; - LastException = exception; + ConsecutiveFailures = 0; + LastException = null; + return; } - public void SetMatchExpression(IPropertyMatchSpecification matchExpression) + ConsecutiveFailures += 1; + LastException = exception; + } + + public void SetMatchExpression(IPropertyMatchSpecification matchExpression) + { + MatchExpression = matchExpression; + } + + /// + /// Assigns the subscription's retry policy. An inline + /// always takes precedence over a referenced — the two are + /// mutually exclusive, matching the resolution order used at evaluation time + /// (CustomRetryPolicy ?? RetryPolicy). Setting a custom policy clears any referenced + /// one and vice versa, so the two fields can never disagree. + /// + public void SetRetryPolicy(int? retryPolicyId, CustomRetryPolicy customRetryPolicy) + { + if (customRetryPolicy != null) { - MatchExpression = matchExpression; + CustomRetryPolicy = customRetryPolicy; + RetryPolicyId = null; } - - public void Pause() + else { - PausedOn = DateTime.UtcNow; + RetryPolicyId = retryPolicyId; + CustomRetryPolicy = null; } + } + + public void Pause() + { + PausedOn = DateTime.UtcNow; + } - public void UnPause() + public void UnPause() + { + PausedOn = null; + Events.Add(new SubscriptionUnpausedEvent { - PausedOn = null; - Events.Add(new SubscriptionUnpausedEvent - { - Id = Id - }); - } + Id = Id + }); } } \ No newline at end of file diff --git a/SW.Bitween.Api/Domain/Xchange/Xchange.cs b/SW.Bitween.Api/Domain/Xchange/Xchange.cs index cee16d5e..e9389a36 100644 --- a/SW.Bitween.Api/Domain/Xchange/Xchange.cs +++ b/SW.Bitween.Api/Domain/Xchange/Xchange.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using SW.Bitween.Model; +using System.Linq; namespace SW.Bitween.Domain { @@ -59,7 +60,7 @@ public Xchange(Subscription subscription, XchangeFile file, string[] references } //retry xchange - public Xchange(Xchange xchange, XchangeFile file, IWorkGroup workGroup) : + public Xchange(Xchange xchange, XchangeFile file, IWorkGroup workGroup, IReadOnlyDictionary groupAttemptCounts = null) : this(xchange.DocumentId, workGroup, file, xchange.References) { SubscriptionId = xchange.SubscriptionId; @@ -71,10 +72,11 @@ public Xchange(Xchange xchange, XchangeFile file, IWorkGroup workGroup) : ResponseSubscriptionId = xchange.ResponseSubscriptionId; RetryFor = xchange.Id; CorrelationId = xchange.CorrelationId; + GroupAttemptCounts = groupAttemptCounts == null ? null : new Dictionary(groupAttemptCounts); } //retry with reset subscription properties - public Xchange(Subscription subscription, Xchange xchange, XchangeFile file) : + public Xchange(Subscription subscription, Xchange xchange, XchangeFile file, IReadOnlyDictionary groupAttemptCounts = null) : this(xchange.DocumentId, subscription.WorkGroup, file, xchange.References) { SubscriptionId = xchange.SubscriptionId; @@ -86,6 +88,7 @@ public Xchange(Subscription subscription, Xchange xchange, XchangeFile file) : ResponseSubscriptionId = subscription.ResponseSubscriptionId; RetryFor = xchange.Id; CorrelationId = xchange.CorrelationId; + GroupAttemptCounts = groupAttemptCounts == null ? null : new Dictionary(groupAttemptCounts); } public int? SubscriptionId { get; private set; } @@ -106,5 +109,6 @@ public Xchange(Subscription subscription, Xchange xchange, XchangeFile file) : public string RetryFor { get; private set; } public string CorrelationId { get; set; } + public IReadOnlyDictionary GroupAttemptCounts { get; private set; } } } \ No newline at end of file diff --git a/SW.Bitween.Api/Extensions/ScheduleToCronExtension.cs b/SW.Bitween.Api/Extensions/ScheduleToCronExtension.cs new file mode 100644 index 00000000..06dce154 --- /dev/null +++ b/SW.Bitween.Api/Extensions/ScheduleToCronExtension.cs @@ -0,0 +1,23 @@ +using SW.Bitween.Domain; +using SW.Bitween.Model; + +namespace SW.Bitween; + +// Quartz cron format: seconds minutes hours day-of-month month day-of-week +// day-of-week numbering: 1=SUN, 2=MON, 3=TUE, 4=WED, 5=THU, 6=FRI, 7=SAT +internal static class ScheduleToCronExtension +{ + public static string ToCronExpression(this Schedule schedule) => schedule.Recurrence switch + { + Recurrence.Hourly => $"0 {schedule.On.Minutes} * * * ?", + Recurrence.Daily => $"0 {schedule.On.Minutes} {schedule.On.Hours} * * ?", + Recurrence.Weekly => $"0 {schedule.On.Minutes} {schedule.On.Hours} ? * {schedule.On.Days + 1}", + Recurrence.Monthly => $"0 {schedule.On.Minutes} {schedule.On.Hours} {schedule.On.Days} * ?", + _ => throw new BitweenException($"Unsupported recurrence: {schedule.Recurrence}") + }; + + // Stable, deterministic key for a (subscription, schedule) pair. + // On.Ticks is used for exact precision; Backwards flag is included to avoid collisions. + public static string ScheduleKeyFor(string prefix, int subscriptionId, Schedule schedule) + => $"{prefix}-{subscriptionId}-{(int)schedule.Recurrence}-{schedule.On.Ticks}-{(schedule.Backwards ? 1 : 0)}"; +} diff --git a/SW.Bitween.Api/Helpers/MatchSpecValueConverter.cs b/SW.Bitween.Api/Helpers/MatchSpecValueConverter.cs index 02d62277..57b6fac9 100644 --- a/SW.Bitween.Api/Helpers/MatchSpecValueConverter.cs +++ b/SW.Bitween.Api/Helpers/MatchSpecValueConverter.cs @@ -1,4 +1,5 @@ using System.IO; +using Microsoft.EntityFrameworkCore.Metadata.Builders; using Newtonsoft.Json; using SW.Bitween.JsonConverters; using SW.Bitween.Model; @@ -7,6 +8,18 @@ namespace SW.Bitween; public static class MatchSpecValueConverter { + /// + /// Applies the standard MatchExpression string<-> + /// conversion. Shared so BusGatewayRoute and Subscription can't drift from each other. + /// + public static PropertyBuilder HasMatchExpressionConversion( + this PropertyBuilder builder) + { + return builder.HasConversion( + domainObject => domainObject == null ? null : SerializeMatchSpec(domainObject), + dbString => dbString == null ? null : DeserializeMatchSpec(dbString)); + } + static readonly JsonSerializer Serializer = new JsonSerializer { Converters = diff --git a/SW.Bitween.Api/Resources/DelayedRetries/RunNow.cs b/SW.Bitween.Api/Resources/DelayedRetries/RunNow.cs new file mode 100644 index 00000000..9f287393 --- /dev/null +++ b/SW.Bitween.Api/Resources/DelayedRetries/RunNow.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.DelayedRetries +{ + [HandlerName("runnow")] + public class RunNow : ICommandHandler + { + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + private readonly XchangeService _xchangeService; + + public RunNow(BitweenDbContext dbContext, RequestContext requestContext, XchangeService xchangeService) + { + _dbContext = dbContext; + _requestContext = requestContext; + _xchangeService = xchangeService; + } + + public async Task Handle(string key, DelayedRetryRunNow request) + { + _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + + var delayedRetry = await _dbContext.Set().FirstOrDefaultAsync(d => d.Id == key); + if (delayedRetry == null) + throw new SWValidationException("NOT_FOUND", "No auto-retry is currently scheduled for this exchange."); + + if (!await _xchangeService.ExecuteDelayedRetry(delayedRetry)) + throw new SWValidationException("NOT_FOUND", "The original exchange or its subscription no longer exists."); + + await _dbContext.SaveChangesAsync(); + return null; + } + } +} diff --git a/SW.Bitween.Api/Resources/DelayedRetries/Search.cs b/SW.Bitween.Api/Resources/DelayedRetries/Search.cs new file mode 100644 index 00000000..4925351c --- /dev/null +++ b/SW.Bitween.Api/Resources/DelayedRetries/Search.cs @@ -0,0 +1,55 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.EfCoreExtensions; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.DelayedRetries +{ + public class Search : ISearchyHandler + { + private readonly BitweenDbContext _dbContext; + + public Search(BitweenDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task Handle(SearchyRequest searchyRequest, bool lookup = false, string searchPhrase = null) + { + var query = from delayedRetry in _dbContext.Set() + join xchange in _dbContext.Set() on delayedRetry.Id equals xchange.Id + join result in _dbContext.Set() on xchange.Id equals result.Id into xr + from result in xr.DefaultIfEmpty() + join document in _dbContext.Set() on xchange.DocumentId equals document.Id + join subscriber in _dbContext.Set() on xchange.SubscriptionId equals subscriber.Id into xs + from subscriber in xs.DefaultIfEmpty() + select new DelayedRetryRow + { + Id = delayedRetry.Id, + On = delayedRetry.On, + SubscriptionId = xchange.SubscriptionId, + SubscriptionName = subscriber.Name, + DocumentId = xchange.DocumentId, + DocumentName = document.Name, + Exception = result.Exception, + StartedOn = xchange.StartedOn + }; + + query = query.OrderBy(r => r.On).AsNoTracking(); + + if (lookup) + return await query.Search(searchyRequest.Conditions) + .ToDictionaryAsync(k => k.Id, v => v.DocumentName); + + return new SearchyResponse + { + TotalCount = await query.Search(searchyRequest.Conditions).CountAsync(), + Result = await query.Search(searchyRequest.Conditions, searchyRequest.Sorts, + searchyRequest.PageSize, searchyRequest.PageIndex).ToListAsync() + }; + } + } +} diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Create.cs b/SW.Bitween.Api/Resources/RetryPolicies/Create.cs new file mode 100644 index 00000000..4cb049f3 --- /dev/null +++ b/SW.Bitween.Api/Resources/RetryPolicies/Create.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.RetryPolicies; + +public class Create : ICommandHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public Create(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(RetryPolicyCreate model) + { + _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + + var entity = new RetryPolicy + { + Name = model.Name, + Groups = model.Groups ?? [] + }; + _dbContext.Add(entity); + await _dbContext.SaveChangesAsync(); + return entity.Id; + } +} diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Delete.cs b/SW.Bitween.Api/Resources/RetryPolicies/Delete.cs new file mode 100644 index 00000000..571835e4 --- /dev/null +++ b/SW.Bitween.Api/Resources/RetryPolicies/Delete.cs @@ -0,0 +1,34 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Accounts; +using SW.EfCoreExtensions; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.RetryPolicies; + +public class Delete : IDeleteHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public Delete(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(int key) + { + _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + + var inUse = await _dbContext.Set() + .AnyAsync(s => s.RetryPolicyId == key); + if (inUse) + throw new SWException("Cannot delete a retry policy that is assigned to one or more subscriptions."); + + await _dbContext.DeleteByKeyAsync(key); + return null; + } +} diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Get.cs b/SW.Bitween.Api/Resources/RetryPolicies/Get.cs new file mode 100644 index 00000000..7241dc80 --- /dev/null +++ b/SW.Bitween.Api/Resources/RetryPolicies/Get.cs @@ -0,0 +1,32 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.EfCoreExtensions; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.RetryPolicies; + +public class Get : IGetHandler +{ + private readonly BitweenDbContext _dbContext; + + public Get(BitweenDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task Handle(int key) + { + return await _dbContext.Set() + .AsNoTracking() + .Search("Id", key) + .Select(p => new RetryPolicyUpdate + { + Name = p.Name, + Groups = p.Groups + }) + .SingleOrDefaultAsync(); + } +} diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Search.cs b/SW.Bitween.Api/Resources/RetryPolicies/Search.cs new file mode 100644 index 00000000..c41261f6 --- /dev/null +++ b/SW.Bitween.Api/Resources/RetryPolicies/Search.cs @@ -0,0 +1,43 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.EfCoreExtensions; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.RetryPolicies; + +public class Search : ISearchyHandler +{ + private readonly BitweenDbContext _dbContext; + + public Search(BitweenDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task Handle(SearchyRequest searchyRequest, bool lookup = false, string searchPhrase = null) + { + var query = from policy in _dbContext.Set() + select new RetryPolicyRow + { + Id = policy.Id, + Name = policy.Name, + GroupCount = policy.Groups.Count + }; + + query = query.AsNoTracking(); + + if (lookup) + return await query.Search(searchyRequest.Conditions) + .ToDictionaryAsync(k => k.Id.ToString(), v => v.Name); + + return new SearchyResponse + { + TotalCount = await query.Search(searchyRequest.Conditions).CountAsync(), + Result = await query.Search(searchyRequest.Conditions, searchyRequest.Sorts, + searchyRequest.PageSize, searchyRequest.PageIndex).ToListAsync() + }; + } +} diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Test.cs b/SW.Bitween.Api/Resources/RetryPolicies/Test.cs new file mode 100644 index 00000000..8fbf12ec --- /dev/null +++ b/SW.Bitween.Api/Resources/RetryPolicies/Test.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.RetryPolicies; + +/// +/// Dry-runs a (possibly unsaved) set of retry groups against a single simulated failure, +/// so the management UI can show "will this retry, and when" before saving. +/// +[HandlerName("test")] +public class Test : ICommandHandler +{ + private readonly RequestContext _requestContext; + + public Test(RequestContext requestContext) + { + _requestContext = requestContext; + } + + public Task Handle(TestRetryPolicyRequest request) + { + _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + + if (request.ResultType == XchangeResultType.Success) + throw new SWValidationException("INVALID_RESULT_TYPE", + "Choose Error or Bad result — a successful result is never retried."); + + var policy = new CustomRetryPolicy { Groups = request.Groups ?? [] }; + var evaluator = new RetryPolicyEvaluator(policy); + var attemptsToSimulate = Math.Clamp(request.AttemptsToSimulate, 1, 20); + + var attempts = new List(); + for (var attemptIndex = 0; attemptIndex < attemptsToSimulate; attemptIndex++) + { + var decision = evaluator.Evaluate(request.ResultType, request.Content, attemptIndex); + + attempts.Add(new TestRetryAttemptResult + { + AttemptNumber = attemptIndex + 1, + MatchedGroupName = decision.MatchedGroupName, + ShouldRetry = decision.ShouldRetry, + DelaySeconds = decision.ShouldRetry ? decision.Delay.TotalSeconds : null, + Reason = decision.Reason + }); + + // Once blocked, every later attempt for this same message would be blocked + // for the same reason (budgets never un-consume, and a Block action or "no + // match" is structural) — no value in simulating further. + if (!decision.ShouldRetry) break; + } + + return Task.FromResult(new TestRetryPolicyResponse { Attempts = attempts }); + } +} diff --git a/SW.Bitween.Api/Resources/RetryPolicies/Update.cs b/SW.Bitween.Api/Resources/RetryPolicies/Update.cs new file mode 100644 index 00000000..5d815fd4 --- /dev/null +++ b/SW.Bitween.Api/Resources/RetryPolicies/Update.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using SW.Bitween.Domain; +using SW.Bitween.Domain.Accounts; +using SW.Bitween.Model; +using SW.PrimitiveTypes; + +namespace SW.Bitween.Resources.RetryPolicies; + +public class Update : ICommandHandler +{ + private readonly BitweenDbContext _dbContext; + private readonly RequestContext _requestContext; + + public Update(BitweenDbContext dbContext, RequestContext requestContext) + { + _dbContext = dbContext; + _requestContext = requestContext; + } + + public async Task Handle(int key, RetryPolicyUpdate model) + { + _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); + + var entity = await _dbContext.FindAsync(key); + entity.Name = model.Name; + entity.Groups = model.Groups ?? []; + await _dbContext.SaveChangesAsync(); + return null; + } +} diff --git a/SW.Bitween.Api/Resources/Subscriptions/AggregateNow.cs b/SW.Bitween.Api/Resources/Subscriptions/AggregateNow.cs index 44e0a418..653583c6 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/AggregateNow.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/AggregateNow.cs @@ -11,12 +11,13 @@ public class AggregateNow : ICommandHandler Handle(int key, SubscriptionAggregateNow request) @@ -26,6 +27,8 @@ public async Task Handle(int key, SubscriptionAggregateNow request) var entity = await _dbContext.FindAsync(key); entity.SetAggregateNow(); await _dbContext.SaveChangesAsync(); + + await _subScheduler.RunNow(entity); return null; } } diff --git a/SW.Bitween.Api/Resources/Subscriptions/ReceiveNow.cs b/SW.Bitween.Api/Resources/Subscriptions/ReceiveNow.cs index 61615f36..1dbcf31b 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/ReceiveNow.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/ReceiveNow.cs @@ -11,11 +11,13 @@ public class ReceiveNow : ICommandHandler { private readonly BitweenDbContext _dbContext; private readonly RequestContext _requestContext; + private readonly SubscriptionSchedulerService _subScheduler; - public ReceiveNow(BitweenDbContext dbContext, RequestContext requestContext) + public ReceiveNow(BitweenDbContext dbContext, RequestContext requestContext, SubscriptionSchedulerService subScheduler) { _dbContext = dbContext; _requestContext = requestContext; + _subScheduler = subScheduler; } async public Task Handle(int key, SubscriptionReceiveNow request) @@ -25,6 +27,8 @@ async public Task Handle(int key, SubscriptionReceiveNow request) var entity = await _dbContext.FindAsync(key); entity.SetReceiveNow(); await _dbContext.SaveChangesAsync(); + + await _subScheduler.RunNow(entity); return null; } } diff --git a/SW.Bitween.Api/Resources/Subscriptions/Update.cs b/SW.Bitween.Api/Resources/Subscriptions/Update.cs index 4bc8fddc..b93101b1 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/Update.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/Update.cs @@ -1,4 +1,5 @@ using FluentValidation; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using SW.EfCoreExtensions; using SW.Bitween.Domain; @@ -18,14 +19,16 @@ public class Update : ICommandHandler private readonly BitweenDbContext _dbContext; private readonly IInfolinkCache _BitweenCache; private readonly RequestContext _requestContext; + private readonly SubscriptionSchedulerService _subScheduler; private const string PrivateSentinel = "__private__"; - public Update(BitweenDbContext dbContext, IInfolinkCache BitweenCache, RequestContext requestContext) + public Update(BitweenDbContext dbContext, IInfolinkCache BitweenCache, RequestContext requestContext, SubscriptionSchedulerService subScheduler) { this._dbContext = dbContext; _BitweenCache = BitweenCache; _requestContext = requestContext; + _subScheduler = subScheduler; } public async Task Handle(int key, SubscriptionUpdate model) @@ -33,6 +36,9 @@ public async Task Handle(int key, SubscriptionUpdate model) _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); var entity = await _dbContext.FindAsync(key); + // Capture before SetSchedules replaces the collection. + var oldSchedules = entity.Schedules.ToList(); + var trail = new SubscriptionTrail(SubscriptionTrialCode.Updated, entity); _dbContext.Entry(entity).SetProperties(model); @@ -47,11 +53,21 @@ public async Task Handle(int key, SubscriptionUpdate model) ); entity.SetMatchExpression(model.MatchExpression); + if (model.CustomRetryPolicy == null && model.RetryPolicyId != null && + !await _dbContext.Set().AnyAsync(p => p.Id == model.RetryPolicyId)) + throw new SWValidationException("RETRY_POLICY_NOT_FOUND", + $"Retry policy {model.RetryPolicyId} was not found."); + + entity.SetRetryPolicy(model.RetryPolicyId, model.CustomRetryPolicy); trail.SetAfter(entity); _dbContext.Add(trail); await _dbContext.SaveChangesAsync(); await _BitweenCache.BroadcastRevoke(); + + // Sync Quartz: unschedule removed entries, schedule new/kept ones. + await _subScheduler.Sync(entity, oldSchedules); + return null; } diff --git a/SW.Bitween.Api/Resources/Xchanges/BulkRetry.cs b/SW.Bitween.Api/Resources/Xchanges/BulkRetry.cs index 7ff64e52..282549e5 100644 --- a/SW.Bitween.Api/Resources/Xchanges/BulkRetry.cs +++ b/SW.Bitween.Api/Resources/Xchanges/BulkRetry.cs @@ -23,7 +23,13 @@ public BulkRetry(BitweenDbContext dbContext, XchangeService xchangeService) public async Task Handle(XchangeBulkRetry request) { - var xchanges = await _dbContext.Set().Where(c => request.Ids.Contains(c.Id)).AsNoTracking() + var scheduledIds = await _dbContext.Set() + .Where(d => request.Ids.Contains(d.Id)) + .Select(d => d.Id) + .ToListAsync(); + + var xchanges = await _dbContext.Set() + .Where(c => request.Ids.Contains(c.Id) && !scheduledIds.Contains(c.Id)).AsNoTracking() .ToListAsync(); foreach (var xchange in xchanges) diff --git a/SW.Bitween.Api/Resources/Xchanges/Retry.cs b/SW.Bitween.Api/Resources/Xchanges/Retry.cs index a7f14abb..6b2c034b 100644 --- a/SW.Bitween.Api/Resources/Xchanges/Retry.cs +++ b/SW.Bitween.Api/Resources/Xchanges/Retry.cs @@ -20,6 +20,10 @@ public Retry(BitweenDbContext dbContext, XchangeService xchangeService) public async Task Handle(string key, XchangeRetry xchangeRetry) { + if (await dbContext.Set().AnyAsync(d => d.Id == key)) + throw new SWValidationException("AUTO_RETRY_SCHEDULED", + "An auto-retry is already scheduled for this exchange. Use \"Run Now\" to execute it immediately instead of retrying manually."); + var xchange = await dbContext.FindAsync(key); var inputFileData = await xchangeService.GetFile(xchange.Id, XchangeFileType.Input); var xchangeFile = new XchangeFile(inputFileData, xchange.InputName); diff --git a/SW.Bitween.Api/Resources/Xchanges/Search.cs b/SW.Bitween.Api/Resources/Xchanges/Search.cs index d6ed98e3..aef8e649 100644 --- a/SW.Bitween.Api/Resources/Xchanges/Search.cs +++ b/SW.Bitween.Api/Resources/Xchanges/Search.cs @@ -38,6 +38,8 @@ from promoted in xp.DefaultIfEmpty() join document in dbContext.Set() on xchange.DocumentId equals document.Id join subscriber in dbContext.Set() on xchange.SubscriptionId equals subscriber.Id into xs from subscriber in xs.DefaultIfEmpty() + join delayedRetry in dbContext.Set() on xchange.Id equals delayedRetry.Id into drGroup + from delayedRetry in drGroup.DefaultIfEmpty() select new XchangeRow { Id = xchange.Id, @@ -70,7 +72,8 @@ from subscriber in xs.DefaultIfEmpty() OutputFileName = result.OutputName, ResponseFileName = result.ResponseName, CorrelationId = xchange.CorrelationId, - PartnerId = subscriber.PartnerId + PartnerId = subscriber.PartnerId, + ScheduledRetryOn = delayedRetry != null ? delayedRetry.On : (DateTime?)null }; var condition = searchyRequest.Conditions.FirstOrDefault(); diff --git a/SW.Bitween.Api/SW.Bitween.Api.csproj b/SW.Bitween.Api/SW.Bitween.Api.csproj index 29c336e3..6c3806b1 100644 --- a/SW.Bitween.Api/SW.Bitween.Api.csproj +++ b/SW.Bitween.Api/SW.Bitween.Api.csproj @@ -26,6 +26,7 @@ + diff --git a/SW.Bitween.Api/Services/AggregationJob.cs b/SW.Bitween.Api/Services/AggregationJob.cs new file mode 100644 index 00000000..818e661f --- /dev/null +++ b/SW.Bitween.Api/Services/AggregationJob.cs @@ -0,0 +1,62 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using SW.Bitween.Domain; +using SW.Scheduler; +using System; +using System.Linq; +using System.Threading.Tasks; +using SW.PrimitiveTypes; + +namespace SW.Bitween; + +public record AggregationJobParams(int SubscriptionId, string? CronExpression); + +[ScheduleConfig(AllowConcurrentExecution = false, MisfireInstructions = MisfireInstructions.Skip)] +public class AggregationJob( + BitweenDbContext dbContext, + XchangeService xchangeService, + ILogger logger) : IScheduledJob +{ + public async Task Execute(AggregationJobParams jobParams) + { + var aggSub = await dbContext.Set() + .FirstOrDefaultAsync(s => s.Id == jobParams.SubscriptionId && !s.Inactive); + + if (aggSub == null) return; + + try + { + var xchangeQuery = + from xchange in dbContext.Set() + join result in dbContext.Set() on xchange.Id equals result.Id + join agg in dbContext.Set() on xchange.Id equals agg.Id into xa + from agg in xa.DefaultIfEmpty() + where result.Success == true && agg == null && + xchange.SubscriptionId == aggSub.AggregationForId && !aggSub.Inactive + select xchange.Id; + + var targetXchangeList = await xchangeQuery.Take(10000).ToListAsync(); + + if (targetXchangeList.Count > 0) + { + var urlList = targetXchangeList.Select(id => + xchangeService.GetFileUrl(id, aggSub.AggregationTarget)); + var xchangeAggregationFile = new XchangeFile(JsonConvert.SerializeObject(urlList)); + var aggXchange = await xchangeService.CreateXchange(aggSub, xchangeAggregationFile); + dbContext.Add(aggXchange); + targetXchangeList.ForEach(id => dbContext.Add(new XchangeAggregation(id, aggXchange.Id))); + } + + aggSub.SetSchedules(); + aggSub.SetHealth(); + } + catch (Exception ex) + { + aggSub.SetHealth(ex.ToString()); + logger.LogError(ex, "Error processing aggregation for subscription {SubscriptionId}", jobParams.SubscriptionId); + } + + await dbContext.SaveChangesAsync(); + } +} diff --git a/SW.Bitween.Api/Services/AggregationService.cs b/SW.Bitween.Api/Services/AggregationService.cs deleted file mode 100644 index 3a5a366a..00000000 --- a/SW.Bitween.Api/Services/AggregationService.cs +++ /dev/null @@ -1,91 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using SW.EfCoreExtensions; -using SW.Bitween.Domain; -using SW.PrimitiveTypes; -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace SW.Bitween -{ - public class AggregationService : BackgroundService - { - readonly ILogger logger; - readonly IServiceProvider sp; - - public AggregationService(IServiceProvider sp, ILogger logger) - { - this.sp = sp; - this.logger = logger; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); - - while (!stoppingToken.IsCancellationRequested) - { - try - { - using var scope = sp.CreateScope(); - var xchangeService = scope.ServiceProvider.GetRequiredService(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - var aggSubs = await dbContext.ListAsync(new DueAggregations()); - - foreach (var aggSub in aggSubs) - { - try - { - var xchangeQuery = from xchange in dbContext.Set() - join result in dbContext.Set() on xchange.Id equals result.Id - join agg in dbContext.Set() on xchange.Id equals agg.Id into xa - from agg in xa.DefaultIfEmpty() - where result.Success == true && agg == null && - xchange.SubscriptionId == aggSub.AggregationForId && !aggSub.Inactive - select xchange.Id; - - var targetXchangeList = - await xchangeQuery.Take(10000).ToListAsync(cancellationToken: stoppingToken); - - if (targetXchangeList.Count > 0) - { - var urlList = targetXchangeList.Select(id => - xchangeService.GetFileUrl(id, aggSub.AggregationTarget)); - var xchangeAggregationFile = new XchangeFile(JsonConvert.SerializeObject(urlList)); - - var aggXchange = await xchangeService.CreateXchange(aggSub, xchangeAggregationFile); - dbContext.Add(aggXchange); - - targetXchangeList.ForEach( - id => dbContext.Add(new XchangeAggregation(id, aggXchange.Id))); - } - - aggSub.SetSchedules(); - aggSub.SetHealth(); - } - catch (Exception ex) - { - aggSub.SetHealth(ex.ToString()); - logger.LogError(ex, - string.Concat("An error occurred while processing aggregator:", aggSub.Id)); - } - - await dbContext.SaveChangesAsync(); - } - } - catch (Exception ex) - { - logger.LogError(ex, "Service timer callback."); - } - - - await Task.Delay(TimeSpan.FromSeconds(61), stoppingToken); - } - } - } -} \ No newline at end of file diff --git a/SW.Bitween.Api/Services/BitweenOptions.cs b/SW.Bitween.Api/Services/BitweenOptions.cs index 48658e44..69eb7545 100644 --- a/SW.Bitween.Api/Services/BitweenOptions.cs +++ b/SW.Bitween.Api/Services/BitweenOptions.cs @@ -78,5 +78,12 @@ public BitweenOptions() /// Example: ["https://localhost:3000", "https://slim-dev.starlinks-me.com"] /// public string[] CorsOrigins { get; set; } = Array.Empty(); + + /// + /// Quartz cron expression that controls how often RetryJob polls for due + /// auto-retry records. Defaults to every minute. + /// Format: second minute hour dayOfMonth month dayOfWeek + /// + public string RetryJobCron { get; set; } = "0 * * * * ?"; } } \ No newline at end of file diff --git a/SW.Bitween.Api/Services/ReceivingJob.cs b/SW.Bitween.Api/Services/ReceivingJob.cs new file mode 100644 index 00000000..927888fc --- /dev/null +++ b/SW.Bitween.Api/Services/ReceivingJob.cs @@ -0,0 +1,94 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using SW.Bitween.Domain; +using SW.PrimitiveTypes; +using SW.Scheduler; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace SW.Bitween; + +public record ReceivingJobParams(int SubscriptionId, string? CronExpression); + +[ScheduleConfig(AllowConcurrentExecution = false, MisfireInstructions = MisfireInstructions.Skip)] +public class ReceivingJob( + BitweenDbContext dbContext, + RunFlagUpdater runFlagUpdater, + NativeAdapterDiscoveryService nativeAdapterDiscovery, + IServerlessService serverless, + XchangeService xchangeService, + ILogger logger) : IScheduledJob +{ + public async Task Execute(ReceivingJobParams jobParams) + { + var rec = await dbContext.Set() + .FirstOrDefaultAsync(s => s.Id == jobParams.SubscriptionId && !s.Inactive); + + if (rec == null) return; + + // Atomic DB-level guard: returns false if another execution is already running. + var isIdle = await runFlagUpdater.MarkAsRunning(rec.Id); + if (!isIdle) return; + + try + { + var startupParameters = rec.ReceiverProperties.ToDictionary(); + await RunReceiver(rec.ReceiverId, startupParameters, rec.Id); + rec.SetSchedules(); + rec.SetHealth(); + } + catch (Exception ex) + { + rec.SetHealth(ex.ToString()); + logger.LogError(ex, "Error processing receiver for subscription {SubscriptionId}", jobParams.SubscriptionId); + } + finally + { + await runFlagUpdater.MarkAsIdle(rec.Id); + } + + await dbContext.SaveChangesAsync(); + } + + private async Task RunReceiver(string serverlessId, IDictionary startupParameters, int subId) + { + if (serverlessId.StartsWith(NativeAdapterDiscoveryService.NativePrefix, StringComparison.OrdinalIgnoreCase)) + { + var receiver = nativeAdapterDiscovery.GetNativeReceiver(serverlessId, startupParameters); + await receiver.Initialize(); + var fileList = (await receiver.ListFiles()).ToList(); + + logger.LogInformation("Subscription '{SubId}' found {Count} items for retrieval.", subId, fileList.Count); + + foreach (var file in fileList) + { + var xchangeFile = await receiver.GetFile(file); + logger.LogInformation("Submitting received file for subscriber: '{SubId}'.", subId); + await xchangeService.SubmitSubscriptionXchange(subId, xchangeFile); + await receiver.DeleteFile(file); + } + + await receiver.Finalize(); + } + else + { + await serverless.StartAsync(serverlessId, null, startupParameters); + await serverless.InvokeAsync(nameof(IInfolinkReceiver.Initialize), null); + var fileList = (await serverless.InvokeAsync>(nameof(IInfolinkReceiver.ListFiles), null)).ToList(); + + logger.LogInformation("Subscription '{SubId}' found {Count} items for retrieval.", subId, fileList.Count); + + foreach (var file in fileList) + { + var xchangeFile = await serverless.InvokeAsync(nameof(IInfolinkReceiver.GetFile), file); + logger.LogInformation("Submitting received file for subscriber: '{SubId}'.", subId); + await xchangeService.SubmitSubscriptionXchange(subId, xchangeFile); + await serverless.InvokeAsync(nameof(IInfolinkReceiver.DeleteFile), file); + } + + await serverless.InvokeAsync(nameof(IInfolinkReceiver.Finalize), null); + } + } +} diff --git a/SW.Bitween.Api/Services/ReceivingService.cs b/SW.Bitween.Api/Services/ReceivingService.cs deleted file mode 100644 index 55008231..00000000 --- a/SW.Bitween.Api/Services/ReceivingService.cs +++ /dev/null @@ -1,185 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using SW.EfCoreExtensions; -using SW.PrimitiveTypes; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using SW.Bitween.Domain; - -namespace SW.Bitween -{ - public class ReceivingService : BackgroundService - { - readonly ILogger logger; - readonly IServiceProvider sp; - - - public ReceivingService(IServiceProvider sp, ILogger logger) - { - this.sp = sp; - this.logger = logger; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); - - while (!stoppingToken.IsCancellationRequested) - { - try - { - using var scope = sp.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - var rcvList = await dbContext.ListAsync(new DueReceivers()); - - var runFlagUpdater = scope.ServiceProvider.GetRequiredService(); - - foreach (var rec in rcvList) - { - try - { - var isIdle = await runFlagUpdater.MarkAsRunning(rec.Id); - if (!isIdle) continue; - - var startupParameters = rec.ReceiverProperties.ToDictionary(); - await RunReceiver(scope.ServiceProvider, rec.ReceiverId, startupParameters, rec.Id); - - rec.SetSchedules(); - rec.SetHealth(); - } - catch (Exception ex) - { - rec.SetHealth(ex.ToString()); - logger.LogError(ex, string.Concat("An error occurred while processing receiver:", rec.Id)); - } - finally - { - await runFlagUpdater.MarkAsIdle(rec.Id); - } - - await dbContext.SaveChangesAsync(stoppingToken); - } - } - catch (Exception ex) - { - logger.LogError(ex, "Service timer callback."); - } - - var options = sp.GetService(); - var delay = options.ReceiversDelayInSeconds ?? 60; - await Task.Delay(TimeSpan.FromSeconds(delay), stoppingToken); - } - } - - async Task RunReceiver(IServiceProvider serviceProvider, string serverlessId, - IDictionary startupParameters, int subId) - { - // Check if it's a native adapter - if (serverlessId.StartsWith(NativeAdapterDiscoveryService.NativePrefix, StringComparison.OrdinalIgnoreCase)) - { - var nativeAdapterDiscovery = serviceProvider.GetRequiredService(); - var receiver = nativeAdapterDiscovery.GetNativeReceiver(serverlessId, startupParameters); - - await receiver.Initialize(); - var fileList = (await receiver.ListFiles()).ToList(); - - logger.LogInformation($"Subscription:'{subId}' found {fileList.Count()} items for retrieval."); - - foreach (var file in fileList) - { - var xchangeFile = await receiver.GetFile(file); - - logger.LogInformation($"Submitting received file for subscriber: '{subId}'."); - - var xchangeService = serviceProvider.GetService(); - await xchangeService.SubmitSubscriptionXchange(subId, xchangeFile); - await receiver.DeleteFile(file); - } - - await receiver.Finalize(); - } - else - { - // Use serverless for external adapters - var serverless = serviceProvider.GetRequiredService(); - await serverless.StartAsync(serverlessId, null, startupParameters); - await serverless.InvokeAsync(nameof(IInfolinkReceiver.Initialize), null); - var fileList = - (await serverless.InvokeAsync>(nameof(IInfolinkReceiver.ListFiles), null)).ToList(); - - logger.LogInformation($"Subscription:'{subId}' found {fileList.Count()} items for retrieval."); - - foreach (var file in fileList) - { - var xchangeFile = await serverless.InvokeAsync(nameof(IInfolinkReceiver.GetFile), file); - - logger.LogInformation($"Submitting received file for subscriber: '{subId}'."); - - var xchangeService = serviceProvider.GetService(); - await xchangeService.SubmitSubscriptionXchange(subId, xchangeFile); - await serverless.InvokeAsync(nameof(IInfolinkReceiver.DeleteFile), file); - } - - await serverless.InvokeAsync(nameof(IInfolinkReceiver.Finalize), null); - } - } - - - //public void Dispose() - //{ - // timer?.Dispose(); - //} - - //public Task StartAsync(CancellationToken cancellationToken) - //{ - // logger.LogInformation("Service is starting."); - - // timer = new Timer(async state => await Run(state), null, TimeSpan.FromSeconds(5), - // TimeSpan.FromSeconds(63)); - - // return Task.CompletedTask; - //} - - //public Task StopAsync(CancellationToken cancellationToken) - //{ - // logger.LogInformation("Service is stopping."); - // timer?.Change(Timeout.Infinite, 0); - // return Task.CompletedTask; - //} - - //public async Task Run(object state) - //{ - // try - // { - // using var scope = sp.CreateScope(); - // var dbContext = scope.ServiceProvider.GetRequiredService(); - // var rcvList = await dbContext.ListAsync(new DueReceivers()); - - // foreach (var rec in rcvList) - // { - // try - // { - // var startupParameters = rec.ReceiverProperties.ToDictionary(); - // await RunReceiver(scope.ServiceProvider, rec.ReceiverId, startupParameters, rec.Id); - // rec.SetSchedules(); - // rec.SetHealth(); - // } - // catch (Exception ex) - // { - // rec.SetHealth(ex.ToString()); - // logger.LogError(ex, string.Concat("An error occurred while processing receiver:", rec.Id)); - // } - // await dbContext.SaveChangesAsync(); - // } - // } - // catch (Exception ex) - // { - // logger.LogError(ex, "Service timer callback."); - // } - //} - } -} \ No newline at end of file diff --git a/SW.Bitween.Api/Services/RetryJob.cs b/SW.Bitween.Api/Services/RetryJob.cs new file mode 100644 index 00000000..f371b4cd --- /dev/null +++ b/SW.Bitween.Api/Services/RetryJob.cs @@ -0,0 +1,35 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using SW.Scheduler; + +namespace SW.Bitween; + +/// +/// Polls for due records and re-submits the failed Xchanges. +/// Scheduled via (registered by SchedulerSeedService). +/// +[ScheduleConfig(AllowConcurrentExecution = false, MisfireInstructions = MisfireInstructions.Skip)] +public class RetryJob(BitweenDbContext dbContext, XchangeService xchangeService) : IScheduledJob +{ + private const int BatchSize = 100; + + public async Task Execute() + { + var ready = await dbContext.Set() + .Where(r => r.On <= DateTime.UtcNow) + .Take(BatchSize) + .ToListAsync(); + + foreach (var delayedRetry in ready) + { + await xchangeService.ExecuteDelayedRetry(delayedRetry); + } + + await dbContext.SaveChangesAsync(); + } +} diff --git a/SW.Bitween.Api/Services/SchedulerSeedService.cs b/SW.Bitween.Api/Services/SchedulerSeedService.cs new file mode 100644 index 00000000..056d42e7 --- /dev/null +++ b/SW.Bitween.Api/Services/SchedulerSeedService.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.Scheduler; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace SW.Bitween; + +// Registers all active Receiving and Aggregation subscriptions with Quartz on startup. +// Uses ScheduleIfNotExists so restarts are idempotent against a persistent Quartz store. +public class SchedulerSeedService(IServiceProvider sp, ILogger logger) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + using var scope = sp.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + var subScheduler = scope.ServiceProvider.GetRequiredService(); + var scheduleRepo = scope.ServiceProvider.GetRequiredService(); + var options = scope.ServiceProvider.GetRequiredService(); + + await scheduleRepo.Schedule(options.RetryJobCron); + + var subscriptions = await dbContext.Set() + .Where(s => + (s.Type == SubscriptionType.Receiving || s.Type == SubscriptionType.Aggregation) && + !s.Inactive && + s.Schedules.Any()) + .ToListAsync(stoppingToken); + + foreach (var sub in subscriptions) + { + try + { + await subScheduler.ScheduleAll(sub); + logger.LogInformation( + "Seeded Quartz schedules for subscription {Id} ({Type})", sub.Id, sub.Type); + } + catch (Exception ex) + { + logger.LogError(ex, + "Failed to seed Quartz schedule for subscription {Id}", sub.Id); + } + } + } +} diff --git a/SW.Bitween.Api/Services/SubscriptionSchedulerService.cs b/SW.Bitween.Api/Services/SubscriptionSchedulerService.cs new file mode 100644 index 00000000..4e12aa9f --- /dev/null +++ b/SW.Bitween.Api/Services/SubscriptionSchedulerService.cs @@ -0,0 +1,90 @@ +using SW.Bitween.Domain; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using SW.Scheduler; +using System; +using System.Threading.Tasks; + +namespace SW.Bitween; + +// Wraps IScheduleRepository with subscription-specific scheduling logic. +// Inject this (scoped) in command handlers and the seed service. +public class SubscriptionSchedulerService(IScheduleRepository scheduleRepo) +{ + // Unschedules all old schedule entries then schedules all new ones (if not inactive). + // Capture entity.Schedules.ToList() BEFORE calling SetSchedules() to get the old set. + public async Task Sync(Subscription sub, System.Collections.Generic.IReadOnlyCollection oldSchedules) + { + if (sub.Type != SubscriptionType.Receiving && sub.Type != SubscriptionType.Aggregation) + return; + + foreach (var s in oldSchedules) + await TryUnschedule(sub.Type, sub.Id, s); + + if (!sub.Inactive) + foreach (var s in sub.Schedules) + await Schedule(sub.Type, sub.Id, s); + } + + // Registers all schedules for a subscription (no-op if already registered or inactive). + public async Task ScheduleAll(Subscription sub) + { + if (sub.Type != SubscriptionType.Receiving && sub.Type != SubscriptionType.Aggregation) + return; + if (sub.Inactive) + return; + + foreach (var s in sub.Schedules) + await Schedule(sub.Type, sub.Id, s); + } + + // Triggers a one-time immediate execution outside the normal cron cadence. + public async Task RunNow(Subscription sub) + { + if (sub.Type == SubscriptionType.Receiving) + await scheduleRepo.ScheduleOnce( + new ReceivingJobParams(sub.Id, null)); + else if (sub.Type == SubscriptionType.Aggregation) + await scheduleRepo.ScheduleOnce( + new AggregationJobParams(sub.Id, null)); + } + + private async Task Schedule(SubscriptionType type, int subId, Schedule schedule) + { + var cron = schedule.ToCronExpression(); + + if (type == SubscriptionType.Receiving) + { + var key = ScheduleToCronExtension.ScheduleKeyFor("receiver", subId, schedule); + await scheduleRepo.ScheduleIfNotExists( + new ReceivingJobParams(subId, cron), cron, key); + } + else + { + var key = ScheduleToCronExtension.ScheduleKeyFor("aggregator", subId, schedule); + await scheduleRepo.ScheduleIfNotExists( + new AggregationJobParams(subId, cron), cron, key); + } + } + + private async Task TryUnschedule(SubscriptionType type, int subId, Schedule schedule) + { + try + { + if (type == SubscriptionType.Receiving) + { + var key = ScheduleToCronExtension.ScheduleKeyFor("receiver", subId, schedule); + await scheduleRepo.UnscheduleJob(key); + } + else + { + var key = ScheduleToCronExtension.ScheduleKeyFor("aggregator", subId, schedule); + await scheduleRepo.UnscheduleJob(key); + } + } + catch (SWValidationException) + { + // Schedule didn't exist in Quartz — nothing to remove. + } + } +} diff --git a/SW.Bitween.Api/Services/XchangeService.cs b/SW.Bitween.Api/Services/XchangeService.cs index 41ca6fbc..31f6bb7f 100644 --- a/SW.Bitween.Api/Services/XchangeService.cs +++ b/SW.Bitween.Api/Services/XchangeService.cs @@ -85,17 +85,17 @@ public async Task SubmitFilterXchange(int documentId, XchangeFile file, string[] await _dbContext.SaveChangesAsync(); } - public async Task CreateXchange(Xchange xchange, XchangeFile file, WorkGroup workGroup) + public async Task CreateXchange(Xchange xchange, XchangeFile file, WorkGroup workGroup, Dictionary groupAttemptCounts = null) { - var newXchange = new Xchange(xchange, file, workGroup); + var newXchange = new Xchange(xchange, file, workGroup, groupAttemptCounts); await AddFile(newXchange.Id, XchangeFileType.Input, file); _dbContext.Add(newXchange); } public async Task CreateXchange(Subscription subscription, Xchange xchange, XchangeFile file, - string[] references = null) + string[] references = null, Dictionary groupAttemptCounts = null) { - var newXchange = new Xchange(subscription, xchange, file); + var newXchange = new Xchange(subscription, xchange, file, groupAttemptCounts); await AddFile(newXchange.Id, XchangeFileType.Input, file); _dbContext.Add(newXchange); } @@ -121,6 +121,37 @@ public async Task CreateXchange(Subscription subscription, XchangeFile return xchange; } + /// + /// Executes a due or manually-triggered : resubmits the original + /// failed Xchange and removes the DelayedRetry record. Used by both RetryJob (scheduled) + /// and the DelayedRetries/RunNow endpoint (immediate). + /// + /// false if the original Xchange or its Subscription no longer exist (the + /// DelayedRetry record is removed as an orphan in that case); true on success. + public async Task ExecuteDelayedRetry(DelayedRetry delayedRetry) + { + var xchange = await _dbContext.FindAsync(delayedRetry.Id); + if (xchange == null) + { + _dbContext.Remove(delayedRetry); + return false; + } + + var subscription = await _dbContext.Set() + .FirstOrDefaultAsync(s => s.Id == xchange.SubscriptionId); + if (subscription == null) + { + _dbContext.Remove(delayedRetry); + return false; + } + + var inputFileData = await GetFile(xchange.Id, XchangeFileType.Input); + var inputFile = new XchangeFile(inputFileData, xchange.InputName); + await CreateXchange(subscription, xchange, inputFile, groupAttemptCounts: delayedRetry.GroupAttemptCounts); + _dbContext.Remove(delayedRetry); + return true; + } + private Task CreateOnHoldXchange(Subscription subscription, XchangeFile file, string[] references = null) { var xchange = new OnHoldXchange(subscription, file.Data, file.Filename, file.BadData, references); @@ -388,16 +419,78 @@ private async Task Process(XchangeMessage message) } _dbContext.Add(new XchangeResult(xchange.Id, workGroup, outputFile, responseFile, responseXchange?.Id)); + if (responseFile?.BadData == true) + await TryScheduleAutoRetry(xchange, XchangeResultType.BadResult, responseFile.Data); await _dbContext.SaveChangesAsync(); } catch (Exception ex) { _dbContext.Add(new XchangeResult(xchange.Id, workGroup, outputFile, responseFile, responseXchange?.Id, ex.ToString())); + await TryScheduleAutoRetry(xchange, XchangeResultType.Error, ex.ToString()); await _dbContext.SaveChangesAsync(); } } + private async Task TryScheduleAutoRetry(Xchange xchange, XchangeResultType resultType, string content) + { + if (xchange.SubscriptionId == null) return; + + var subscription = await _dbContext.Set() + .Include(s => s.RetryPolicy) + .FirstOrDefaultAsync(s => s.Id == xchange.SubscriptionId.Value); + + IRetryPolicy policy = subscription?.CustomRetryPolicy ?? (IRetryPolicy)subscription?.RetryPolicy; + if (policy?.Groups == null || policy.Groups.Count == 0) return; + + var evaluator = new RetryPolicyEvaluator(policy); + evaluator.RestoreGroupAttemptCounts(xchange.GroupAttemptCounts == null + ? new Dictionary() + : new Dictionary(xchange.GroupAttemptCounts)); + + var attemptIndex = await CountRetryChainDepth(xchange); + var decision = evaluator.Evaluate(resultType, content, attemptIndex); + + if (decision.ShouldRetry) + { + // Guard against duplicate scheduling (e.g. an at-least-once redelivery + // reprocessing the same xchange) — DelayedRetry.Id is xchange.Id, so a + // blind Add would violate the PK and fail the whole SaveChangesAsync. + var existing = await _dbContext.Set().FindAsync(xchange.Id); + if (existing != null) + { + existing.On = DateTime.UtcNow + decision.Delay; + existing.GroupAttemptCounts = evaluator.GetGroupAttemptCounts(); + } + else + { + _dbContext.Add(new DelayedRetry + { + Id = xchange.Id, + On = DateTime.UtcNow + decision.Delay, + GroupAttemptCounts = evaluator.GetGroupAttemptCounts() + }); + } + } + } + + private async Task CountRetryChainDepth(Xchange xchange) + { + var depth = 0; + var retryFor = xchange.RetryFor; + while (retryFor != null) + { + depth++; + var parent = await _dbContext.Set() + .AsNoTracking() + .Where(x => x.Id == retryFor) + .Select(x => x.RetryFor) + .FirstOrDefaultAsync(); + retryFor = parent; + } + return depth; + } + async Task CreateXchangesForHits(Xchange xchange, FilterResult result, XchangeFile inputFile) { diff --git a/SW.Bitween.IntegrationTests/Adapters/NativeTestReceiver.cs b/SW.Bitween.IntegrationTests/Adapters/NativeTestReceiver.cs new file mode 100644 index 00000000..a30a987a --- /dev/null +++ b/SW.Bitween.IntegrationTests/Adapters/NativeTestReceiver.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using SW.Bitween.NativeAdapters; +using SW.PrimitiveTypes; + +namespace SW.Bitween.IntegrationTests.Adapters; + +public class NativeTestReceiver : INativeInfolinkReceiver +{ + public string Name => nameof(NativeTestReceiver); + public Type StartupValuesType => typeof(object); + + public void InitializeStartupValues(IDictionary settings) { } + + public Task Initialize() => Task.CompletedTask; + + public Task> ListFiles() => + Task.FromResult>(new[] { "test-file-1", "test-file-2" }); + + public Task GetFile(string fileId) => + Task.FromResult(new XchangeFile($"{{\"fileId\":\"{fileId}\"}}")); + + public Task DeleteFile(string fileId) => Task.CompletedTask; + + public Task Finalize() => Task.CompletedTask; +} diff --git a/SW.Bitween.IntegrationTests/Fixtures/AdapterInstaller.cs b/SW.Bitween.IntegrationTests/Fixtures/AdapterInstaller.cs new file mode 100644 index 00000000..c322d88a --- /dev/null +++ b/SW.Bitween.IntegrationTests/Fixtures/AdapterInstaller.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Reflection; +using System.Security.Cryptography; +using System.Threading.Tasks; +using SW.PrimitiveTypes; + +namespace SW.Bitween.IntegrationTests.Fixtures; + +internal static class AdapterInstaller +{ + private static string AdaptersRoot => + Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "test-adapters"); + + public static async Task InstallAsync(ICloudFilesService cloudFiles, + string projectName, string adapterId, string entryAssembly) + { + var publishDir = Path.Combine(AdaptersRoot, projectName); + + using var zipStream = new MemoryStream(); + using (var archive = new ZipArchive(zipStream, ZipArchiveMode.Create, leaveOpen: true)) + { + foreach (var file in Directory.GetFiles(publishDir, "*", SearchOption.AllDirectories)) + { + var ext = Path.GetExtension(file).ToLowerInvariant(); + if (ext is ".pdb" or ".xml" or ".http") continue; + var entryName = Path.GetRelativePath(publishDir, file); + var entry = archive.CreateEntry(entryName); + await using var entryStream = entry.Open(); + await using var fileStream = File.OpenRead(file); + await fileStream.CopyToAsync(entryStream); + } + } + + var bytes = zipStream.ToArray(); + var hash = Convert.ToHexString(SHA256.HashData(bytes)).ToLower()[..16]; + + using var uploadStream = new MemoryStream(bytes); + await cloudFiles.WriteAsync(uploadStream, new WriteFileSettings + { + Key = $"adapters/{adapterId}".ToLower(), + ContentType = "application/zip", + Metadata = new Dictionary + { + { "EntryAssembly", entryAssembly }, + { "Hash", hash } + } + }); + } +} diff --git a/SW.Bitween.IntegrationTests/Fixtures/BitweenFixture.cs b/SW.Bitween.IntegrationTests/Fixtures/BitweenFixture.cs new file mode 100644 index 00000000..33824ad2 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Fixtures/BitweenFixture.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Npgsql; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Adapters; +using SW.Bitween.NativeAdapters; +using SW.Bitween.PgSql; +using SW.Bus; +using SW.CloudFiles.Extensions; +using SW.CloudFiles.LocalTests; +using SW.PrimitiveTypes; +using SW.Serverless; +using Testcontainers.PostgreSql; +using Testcontainers.RabbitMq; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Fixtures; + +/// +/// Collection-scoped fixture that starts a PostgreSQL container and a RabbitMQ container, +/// applies EF migrations, installs serverless adapters to local cloud storage, and builds +/// a fully wired service provider. +/// +public sealed class BitweenFixture : IAsyncLifetime +{ + private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder().Build(); + private readonly RabbitMqContainer _rabbitMq = new RabbitMqBuilder().Build(); + + public IHost App { get; private set; } = null!; + + private ExceptionDispatchInfo? _initError; + + public async Task InitializeAsync() + { + try + { + await Task.WhenAll(_postgres.StartAsync(), _rabbitMq.StartAsync()); + + var dataSourceBuilder = new NpgsqlDataSourceBuilder(_postgres.GetConnectionString()); + dataSourceBuilder.EnableDynamicJson(); + var dataSource = dataSourceBuilder.Build(); + + App = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration(cfg => cfg.AddInMemoryCollection(new Dictionary + { + ["ConnectionStrings:RabbitMQ"] = _rabbitMq.GetConnectionString(), + })) + .ConfigureServices((ctx, services) => + { + services.AddSingleton(new BitweenOptions + { + QueuePrefix = "bitween-test", + StorageProvider = "LocalTests", + DatabaseType = "PgSql", + BusDefaultQueuePrefetch = 10, + }); + + services.AddMemoryCache(); + services.AddScoped(); + + services.AddDbContext(c => + c.UseSnakeCaseNamingConvention() + .UseNpgsql(dataSource, b => + { + b.MigrationsHistoryTable("_ef_migrations_history", PgSql.BitweenDbContext.Schema); + b.MigrationsAssembly(typeof(PgSql.DbType).Assembly.FullName); + })); + + services.AddBus(cfg => + { + cfg.ApplicationName = "bitween-test"; + cfg.DefaultQueuePrefetch = 10; + }); + services.AddBusPublish(); + + // Real local filesystem cloud files provider + services.AddLocalTestsCloudFiles(); + + // Real serverless service pointing to local adapter extraction path + services.AddServerless(opts => + { + opts.AdapterRemotePath = "adapters"; + opts.AdapterLocalPath = Path.Combine(Path.GetTempPath(), "bitween-test-serverless"); + }); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + }) + .Build(); + + await using (var scope = App.Services.CreateAsyncScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.MigrateAsync(); + } + + // Install serverless adapters into local cloud storage + await using (var scope = App.Services.CreateAsyncScope()) + { + var cloudFiles = scope.ServiceProvider.GetRequiredService(); + await AdapterInstaller.InstallAsync(cloudFiles, + "SW.Bitween.SampleHandler", "sw.bitween.samplehandler", "SW.Bitween.SampleHandler.dll"); + await AdapterInstaller.InstallAsync(cloudFiles, + "SW.Bitween.SampleConfigurableAdapter", "sw.bitween.sampleconfigurableadapter", "SW.Bitween.SampleConfigurableAdapter.dll"); + } + + await App.StartAsync(); + } + catch (Exception ex) + { + _initError = ExceptionDispatchInfo.Capture(ex); + } + } + + /// Creates a new DI scope. Caller is responsible for disposal. + public AsyncServiceScope CreateScope() + { + _initError?.Throw(); + return App.Services.CreateAsyncScope(); + } + + public async Task DisposeAsync() + { + if (App is not null) + { + App.Services.GetRequiredService().Cleanup(); + await App.StopAsync(); + } + await _postgres.DisposeAsync(); + await _rabbitMq.DisposeAsync(); + } +} + +[CollectionDefinition("Bitween")] +public class BitweenCollection : ICollectionFixture; diff --git a/SW.Bitween.IntegrationTests/SW.Bitween.IntegrationTests.csproj b/SW.Bitween.IntegrationTests/SW.Bitween.IntegrationTests.csproj new file mode 100644 index 00000000..4cb60b01 --- /dev/null +++ b/SW.Bitween.IntegrationTests/SW.Bitween.IntegrationTests.csproj @@ -0,0 +1,49 @@ + + + + net8.0 + enable + false + SW.Bitween.IntegrationTests + + + + + PreserveNewest + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SW.Bitween.IntegrationTests/Tests/AggregationTests.cs b/SW.Bitween.IntegrationTests/Tests/AggregationTests.cs new file mode 100644 index 00000000..f40b6cf7 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/AggregationTests.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +[Collection("Bitween")] +public class AggregationTests +{ + private readonly BitweenFixture _fixture; + + public AggregationTests(BitweenFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task Aggregation_job_creates_one_xchange_from_successful_source_xchanges() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var job = scope.ServiceProvider.GetRequiredService(); + var cache = _fixture.App.Services.GetRequiredService(); + + // Source subscription whose Xchanges will be aggregated + var sourceDoc = new Document(6003, "Agg Source Doc"); + db.Set().Add(sourceDoc); + var sourceSub = new Subscription("Agg Source", sourceDoc.Id); + sourceSub.Inactive = false; + db.Set().Add(sourceSub); + await db.SaveChangesAsync(); + + // Create 3 source Xchanges with successful results + var xchangeIds = new List(); + for (var i = 0; i < 3; i++) + { + var xchange = new Xchange(sourceSub, new XchangeFile($"{{\"i\":{i}}}")); + db.Set().Add(xchange); + await db.SaveChangesAsync(); + + db.Set().Add(new XchangeResult(xchange.Id, null, null)); + await db.SaveChangesAsync(); + + xchangeIds.Add(xchange.Id); + } + + // Aggregation subscription pointing at the source + var aggSub = new Subscription("Agg Test", sourceSub.Id, Partner.SystemId); + aggSub.Inactive = false; + aggSub.AggregationTarget = XchangeFileType.Input; + db.Set().Add(aggSub); + await db.SaveChangesAsync(); + + cache.Revoke(); + + await job.Execute(new AggregationJobParams(aggSub.Id, null)); + + // One aggregation Xchange should have been created + var aggXchange = await db.Set().FirstOrDefaultAsync(x => x.SubscriptionId == aggSub.Id); + Assert.NotNull(aggXchange); + + // All 3 source Xchanges should now be marked as aggregated + var aggLinks = await db.Set() + .Where(a => xchangeIds.Contains(a.Id)) + .ToListAsync(); + Assert.Equal(3, aggLinks.Count); + Assert.All(aggLinks, a => Assert.Equal(aggXchange.Id, a.AggregationXchangeId)); + } + + [Fact] + public async Task Aggregation_job_skips_already_aggregated_xchanges() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var job = scope.ServiceProvider.GetRequiredService(); + var cache = _fixture.App.Services.GetRequiredService(); + + var sourceDoc = new Document(6004, "Agg Source Doc 2"); + db.Set().Add(sourceDoc); + var sourceSub = new Subscription("Agg Source 2", sourceDoc.Id); + sourceSub.Inactive = false; + db.Set().Add(sourceSub); + await db.SaveChangesAsync(); + + // Create 2 source Xchanges — one will be pre-aggregated, one will not + var x1 = new Xchange(sourceSub, new XchangeFile("{\"seq\":1}")); + var x2 = new Xchange(sourceSub, new XchangeFile("{\"seq\":2}")); + db.Set().Add(x1); + db.Set().Add(x2); + await db.SaveChangesAsync(); + + db.Set().Add(new XchangeResult(x1.Id, null, null)); + db.Set().Add(new XchangeResult(x2.Id, null, null)); + await db.SaveChangesAsync(); + + // Mark x1 as already aggregated + var priorAggXchange = new Xchange(sourceSub, new XchangeFile("{\"prior\":true}")); + db.Set().Add(priorAggXchange); + await db.SaveChangesAsync(); + db.Set().Add(new XchangeAggregation(x1.Id, priorAggXchange.Id)); + await db.SaveChangesAsync(); + + var aggSub = new Subscription("Agg Test 2", sourceSub.Id, Partner.SystemId); + aggSub.Inactive = false; + aggSub.AggregationTarget = XchangeFileType.Input; + db.Set().Add(aggSub); + await db.SaveChangesAsync(); + + cache.Revoke(); + + await job.Execute(new AggregationJobParams(aggSub.Id, null)); + + // Only x2 was eligible → one aggregation Xchange created + var aggXchanges = await db.Set() + .Where(x => x.SubscriptionId == aggSub.Id) + .ToListAsync(); + Assert.Single(aggXchanges); + + // x2 now has an aggregation link; x1 still points to the prior one + var x2Link = await db.Set().FindAsync(x2.Id); + Assert.NotNull(x2Link); + Assert.Equal(aggXchanges[0].Id, x2Link.AggregationXchangeId); + } + + [Fact] + public async Task Aggregation_job_does_nothing_for_inactive_subscription() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var job = scope.ServiceProvider.GetRequiredService(); + + var sourceDoc = new Document(6005, "Inactive Agg Source Doc"); + db.Set().Add(sourceDoc); + var sourceSub = new Subscription("Inactive Agg Source", sourceDoc.Id); + sourceSub.Inactive = false; + db.Set().Add(sourceSub); + await db.SaveChangesAsync(); + + var aggSub = new Subscription("Inactive Agg", sourceSub.Id, Partner.SystemId); + // Inactive = true by default + db.Set().Add(aggSub); + await db.SaveChangesAsync(); + + await job.Execute(new AggregationJobParams(aggSub.Id, null)); + + var count = await db.Set().CountAsync(x => x.SubscriptionId == aggSub.Id); + Assert.Equal(0, count); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/BusTests.cs b/SW.Bitween.IntegrationTests/Tests/BusTests.cs new file mode 100644 index 00000000..22021fa5 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/BusTests.cs @@ -0,0 +1,59 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// Verifies basic RabbitMQ connectivity using the real Testcontainer broker. +/// IPublish.Publish(routingKey, json) is the low-level API used throughout the codebase. +/// These tests confirm the AMQP channel is open and messages are accepted. +/// +[Collection("Bitween")] +public class BusTests +{ + private readonly BitweenFixture _fixture; + + public BusTests(BitweenFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task IPublish_is_resolvable_from_di() + { + await using var scope = _fixture.CreateScope(); + var publish = scope.ServiceProvider.GetRequiredService(); + + Assert.NotNull(publish); + } + + [Fact] + public async Task Can_publish_message_to_broker() + { + await using var scope = _fixture.CreateScope(); + var publish = scope.ServiceProvider.GetRequiredService(); + + // Publish a simple JSON payload. The routing key mirrors the pattern used + // by the Bitween SaveChangesAsync domain-event dispatch. + var ex = await Record.ExceptionAsync(async () => + await publish.Publish("TestEvent", "{\"id\":\"integration-test\"}")); + + Assert.Null(ex); + } + + [Fact] + public async Task Can_publish_multiple_messages_in_sequence() + { + await using var scope = _fixture.CreateScope(); + var publish = scope.ServiceProvider.GetRequiredService(); + + for (var i = 0; i < 5; i++) + { + await publish.Publish("TestSequenceEvent", $"{{\"seq\":{i}}}"); + } + // Passes if no exception is thrown — confirms the channel stays open + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/DelayedRetriesTests.cs b/SW.Bitween.IntegrationTests/Tests/DelayedRetriesTests.cs new file mode 100644 index 00000000..a410e084 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/DelayedRetriesTests.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +[Collection("Bitween")] +public class DelayedRetriesTests +{ + private readonly BitweenFixture _fixture; + + public DelayedRetriesTests(BitweenFixture fixture) + { + _fixture = fixture; + } + + private static SearchyRequest EmptySearch() => new() + { + PageSize = 50, + PageIndex = 0 + }; + + private async Task<(Document doc, Subscription sub, Xchange xchange)> CreateSubscriptionWithXchange( + BitweenDbContext db, XchangeService xs, int docId, string name) + { + var doc = new Document(docId, name); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var sub = new Subscription(name, doc.Id); + sub.Inactive = false; + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var xchange = await xs.CreateXchange(sub, new XchangeFile("{}")); + await db.SaveChangesAsync(); + + return (doc, sub, xchange); + } + + // ─── Manual retry guard ───────────────────────────────────────────────── + + [Fact] + public async Task Retry_throws_when_auto_retry_already_scheduled() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + var (_, _, xchange) = await CreateSubscriptionWithXchange(db, xs, 9001, "Retry Guard Doc"); + + db.Set().Add(new DelayedRetry { Id = xchange.Id, On = DateTime.UtcNow.AddMinutes(5) }); + await db.SaveChangesAsync(); + + var retry = new SW.Bitween.Resources.Xchanges.Retry(db, xs); + + await Assert.ThrowsAsync(() => + retry.Handle(xchange.Id, new XchangeRetry { Reset = false })); + } + + [Fact] + public async Task Retry_succeeds_when_no_auto_retry_scheduled() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + var (_, _, xchange) = await CreateSubscriptionWithXchange(db, xs, 9002, "Retry OK Doc"); + + var retry = new SW.Bitween.Resources.Xchanges.Retry(db, xs); + await retry.Handle(xchange.Id, new XchangeRetry { Reset = false }); + + var retryXchange = await db.Set().FirstOrDefaultAsync(x => x.RetryFor == xchange.Id); + Assert.NotNull(retryXchange); + } + + [Fact] + public async Task BulkRetry_skips_ids_with_scheduled_auto_retry_and_processes_others() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + var (_, _, xchangeScheduled) = await CreateSubscriptionWithXchange(db, xs, 9003, "Bulk Scheduled Doc"); + var (_, _, xchangeFree) = await CreateSubscriptionWithXchange(db, xs, 9004, "Bulk Free Doc"); + + db.Set().Add(new DelayedRetry { Id = xchangeScheduled.Id, On = DateTime.UtcNow.AddMinutes(5) }); + await db.SaveChangesAsync(); + + var bulkRetry = new SW.Bitween.Resources.Xchanges.BulkRetry(db, xs); + await bulkRetry.Handle(new XchangeBulkRetry + { + Reset = false, + Ids = [xchangeScheduled.Id, xchangeFree.Id] + }); + + var retriedScheduled = await db.Set().AnyAsync(x => x.RetryFor == xchangeScheduled.Id); + var retriedFree = await db.Set().AnyAsync(x => x.RetryFor == xchangeFree.Id); + + Assert.False(retriedScheduled, "An xchange with a scheduled auto-retry must be skipped by bulk retry."); + Assert.True(retriedFree, "An xchange without a scheduled auto-retry must still be retried by bulk retry."); + } + + // ─── DelayedRetries search ────────────────────────────────────────────── + + [Fact] + public async Task DelayedRetries_Search_returns_expected_row() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + var (doc, sub, xchange) = await CreateSubscriptionWithXchange(db, xs, 9005, "Search Row Doc"); + + var scheduledOn = DateTime.UtcNow.AddMinutes(10); + db.Set().Add(new DelayedRetry { Id = xchange.Id, On = scheduledOn }); + await db.SaveChangesAsync(); + + var search = new SW.Bitween.Resources.DelayedRetries.Search(db); + var response = (SearchyResponse)await search.Handle(EmptySearch()); + + var row = response.Result.FirstOrDefault(r => r.Id == xchange.Id); + Assert.NotNull(row); + Assert.Equal(sub.Id, row.SubscriptionId); + Assert.Equal(sub.Name, row.SubscriptionName); + Assert.Equal(doc.Id, row.DocumentId); + Assert.Equal(doc.Name, row.DocumentName); + Assert.Equal(scheduledOn, row.On, TimeSpan.FromSeconds(1)); + } + + // ─── Run now ──────────────────────────────────────────────────────────── + + [Fact] + public async Task RunNow_executes_immediately_even_when_not_yet_due_and_removes_record() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.ServiceProvider.GetRequiredService(); + var (_, _, xchange) = await CreateSubscriptionWithXchange(db, xs, 9006, "Run Now Doc"); + + // Scheduled an hour from now — RunNow must still execute it immediately. + db.Set().Add(new DelayedRetry { Id = xchange.Id, On = DateTime.UtcNow.AddHours(1) }); + await db.SaveChangesAsync(); + + var runNow = new SW.Bitween.Resources.DelayedRetries.RunNow(db, ctx, xs); + await runNow.Handle(xchange.Id, new DelayedRetryRunNow()); + + var stillScheduled = await db.Set().AnyAsync(d => d.Id == xchange.Id); + Assert.False(stillScheduled, "RunNow must remove the DelayedRetry record."); + + var retryXchange = await db.Set().FirstOrDefaultAsync(x => x.RetryFor == xchange.Id); + Assert.NotNull(retryXchange); + } + + [Fact] + public async Task RunNow_throws_when_nothing_is_scheduled() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.ServiceProvider.GetRequiredService(); + + var runNow = new SW.Bitween.Resources.DelayedRetries.RunNow(db, ctx, xs); + + await Assert.ThrowsAsync(() => + runNow.Handle("rjt-nonexistent-" + Guid.NewGuid().ToString("N")[..8], new DelayedRetryRunNow())); + } + + // ─── Xchanges search surfacing ────────────────────────────────────────── + + [Fact] + public async Task Xchanges_Search_includes_ScheduledRetryOn_when_delayed_retry_exists() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + var (_, _, xchange) = await CreateSubscriptionWithXchange(db, xs, 9007, "Xchange Search Scheduled Doc"); + + var scheduledOn = DateTime.UtcNow.AddMinutes(15); + db.Set().Add(new DelayedRetry { Id = xchange.Id, On = scheduledOn }); + await db.SaveChangesAsync(); + + var search = new SW.Bitween.Resources.Xchanges.Search(db, xs); + var response = (SearchyResponse)await search.Handle(EmptySearch()); + + var row = response.Result.FirstOrDefault(r => r.Id == xchange.Id); + Assert.NotNull(row); + Assert.NotNull(row.ScheduledRetryOn); + Assert.Equal(scheduledOn, row.ScheduledRetryOn!.Value, TimeSpan.FromSeconds(1)); + } + + [Fact] + public async Task Xchanges_Search_has_null_ScheduledRetryOn_when_no_delayed_retry_exists() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + var (_, _, xchange) = await CreateSubscriptionWithXchange(db, xs, 9008, "Xchange Search Unscheduled Doc"); + + var search = new SW.Bitween.Resources.Xchanges.Search(db, xs); + var response = (SearchyResponse)await search.Handle(EmptySearch()); + + var row = response.Result.FirstOrDefault(r => r.Id == xchange.Id); + Assert.NotNull(row); + Assert.Null(row.ScheduledRetryOn); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/EntityTests.cs b/SW.Bitween.IntegrationTests/Tests/EntityTests.cs new file mode 100644 index 00000000..a6a94023 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/EntityTests.cs @@ -0,0 +1,100 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +/// +/// Verifies that EF Core migrations applied correctly and that domain entities +/// can be persisted and retrieved from the real PostgreSQL container. +/// +[Collection("Bitween")] +public class EntityTests +{ + private readonly BitweenFixture _fixture; + + public EntityTests(BitweenFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task Can_create_and_read_document() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + // Use high IDs to avoid PK conflicts with seeded data (AggregationDocumentId = 10001) + var document = new Document(5001, "Integration Test Doc"); + db.Set().Add(document); + await db.SaveChangesAsync(); + + var loaded = await db.Set().FirstOrDefaultAsync(d => d.Id == 5001); + + Assert.NotNull(loaded); + Assert.Equal("Integration Test Doc", loaded.Name); + } + + [Fact] + public async Task Can_create_partner() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + // Partner.Id is auto-generated (ValueGeneratedOnAdd) + var partner = new Partner("Test Partner"); + db.Set().Add(partner); + await db.SaveChangesAsync(); + + Assert.True(partner.Id > 0, "Id should be assigned by the database"); + + var loaded = await db.Set().FindAsync(partner.Id); + + Assert.NotNull(loaded); + Assert.Equal("Test Partner", loaded.Name); + } + + [Fact] + public async Task Can_create_receiving_subscription() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + // Create a document for the subscription to reference + var document = new Document(5002, "Sub Test Doc"); + db.Set().Add(document); + await db.SaveChangesAsync(); + + var subscription = new Subscription("My Receiver", document.Id); + db.Set().Add(subscription); + await db.SaveChangesAsync(); + + Assert.True(subscription.Id > 0); + + var loaded = await db.Set().FindAsync(subscription.Id); + + Assert.NotNull(loaded); + Assert.Equal("My Receiver", loaded.Name); + Assert.Equal(SubscriptionType.Receiving, loaded.Type); + Assert.True(loaded.Inactive, "New receiving subscriptions start inactive"); + } + + [Fact] + public async Task Seed_data_exists_after_migration() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var systemPartner = await db.Set().FindAsync(Partner.SystemId); + var aggregationDoc = await db.Set().FindAsync(Document.AggregationDocumentId); + + Assert.NotNull(systemPartner); + Assert.Equal("SYSTEM", systemPartner.Name); + Assert.NotNull(aggregationDoc); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/ReceivingTests.cs b/SW.Bitween.IntegrationTests/Tests/ReceivingTests.cs new file mode 100644 index 00000000..7fdbeb18 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/ReceivingTests.cs @@ -0,0 +1,71 @@ +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Adapters; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +[Collection("Bitween")] +public class ReceivingTests +{ + private readonly BitweenFixture _fixture; + + public ReceivingTests(BitweenFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task Receiving_job_creates_one_xchange_per_received_file() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var job = scope.ServiceProvider.GetRequiredService(); + var cache = _fixture.App.Services.GetRequiredService(); + + var document = new Document(6001, "Receiving Test Doc"); + db.Set().Add(document); + + // NativeTestReceiver is matched by class name, which starts with "Native" (case-insensitive "native" prefix) + var subscription = new Subscription("Receive Test", document.Id); + subscription.ReceiverId = nameof(NativeTestReceiver); + subscription.Inactive = false; + db.Set().Add(subscription); + await db.SaveChangesAsync(); + + // Invalidate cache so XchangeService can find the newly created subscription + cache.Revoke(); + + await job.Execute(new ReceivingJobParams(subscription.Id, null)); + + // NativeTestReceiver.ListFiles() returns 2 files → 2 Xchanges expected + var count = await db.Set().CountAsync(x => x.SubscriptionId == subscription.Id); + Assert.Equal(2, count); + } + + [Fact] + public async Task Receiving_job_does_nothing_for_inactive_subscription() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var job = scope.ServiceProvider.GetRequiredService(); + + var document = new Document(6002, "Inactive Receiving Doc"); + db.Set().Add(document); + + var subscription = new Subscription("Inactive Receiver", document.Id); + subscription.ReceiverId = nameof(NativeTestReceiver); + // Inactive = true by default — job should skip it + db.Set().Add(subscription); + await db.SaveChangesAsync(); + + await job.Execute(new ReceivingJobParams(subscription.Id, null)); + + var count = await db.Set().CountAsync(x => x.SubscriptionId == subscription.Id); + Assert.Equal(0, count); + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/RetryJobTests.cs b/SW.Bitween.IntegrationTests/Tests/RetryJobTests.cs new file mode 100644 index 00000000..5bf58c4d --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/RetryJobTests.cs @@ -0,0 +1,255 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +[Collection("Bitween")] +public class RetryJobTests +{ + private readonly BitweenFixture _fixture; + + public RetryJobTests(BitweenFixture fixture) + { + _fixture = fixture; + } + + // ─── Helpers ────────────────────────────────────────────────────────────── + + private RetryJob BuildJob(BitweenDbContext db, XchangeService xchangeService) => + new(db, xchangeService); + + // ─── Batch query ────────────────────────────────────────────────────────── + + [Fact] + public async Task RetryJob_does_not_process_future_delayed_retry() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + var delayedRetry = new DelayedRetry + { + Id = "rjt-future-" + Guid.NewGuid().ToString("N")[..8], + On = DateTime.UtcNow.AddHours(1) + }; + db.Set().Add(delayedRetry); + await db.SaveChangesAsync(); + + await BuildJob(db, xs).Execute(); + + var stillExists = await db.Set().AnyAsync(r => r.Id == delayedRetry.Id); + Assert.True(stillExists, "A future DelayedRetry must not be processed before its due time."); + } + + // ─── Orphan cleanup ─────────────────────────────────────────────────────── + + [Fact] + public async Task RetryJob_removes_delayed_retry_when_xchange_is_missing() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + // ID that has no matching Xchange row + var delayedRetry = new DelayedRetry + { + Id = "rjt-orphan-" + Guid.NewGuid().ToString("N")[..8], + On = DateTime.UtcNow.AddMinutes(-1) + }; + db.Set().Add(delayedRetry); + await db.SaveChangesAsync(); + + await BuildJob(db, xs).Execute(); + + var gone = !await db.Set().AnyAsync(r => r.Id == delayedRetry.Id); + Assert.True(gone, "A DelayedRetry whose Xchange no longer exists must be removed."); + } + + [Fact] + public async Task RetryJob_removes_delayed_retry_when_subscription_is_missing() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + // Create an Xchange with no subscription (SubscriptionId remains null) + var doc = new Document(8003, "RetryJob Orphan Sub Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + // The base Xchange constructor leaves SubscriptionId null + var xchange = new Xchange(doc.Id, null, new XchangeFile("{}")); + db.Set().Add(xchange); + await db.SaveChangesAsync(); + + var delayedRetry = new DelayedRetry { Id = xchange.Id, On = DateTime.UtcNow.AddMinutes(-1) }; + db.Set().Add(delayedRetry); + await db.SaveChangesAsync(); + + await BuildJob(db, xs).Execute(); + + var gone = !await db.Set().AnyAsync(r => r.Id == delayedRetry.Id); + Assert.True(gone, + "A DelayedRetry whose Subscription no longer exists must be removed without creating a retry Xchange."); + } + + // ─── Full execution path ────────────────────────────────────────────────── + + [Fact] + public async Task RetryJob_processes_due_delayed_retry_and_creates_retry_xchange() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(8001, "RetryJob Due Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var sub = new Subscription("RetryJob Sub", doc.Id); + sub.Inactive = false; + db.Set().Add(sub); + await db.SaveChangesAsync(); + + // Use CreateXchange so the input file is uploaded to real cloud storage + var originalXchange = await xs.CreateXchange(sub, new XchangeFile("{}")); + + var groupCounts = new System.Collections.Generic.Dictionary + { + [Guid.NewGuid().ToString()] = 1 + }; + var delayedRetry = new DelayedRetry + { + Id = originalXchange.Id, + On = DateTime.UtcNow.AddMinutes(-1), + GroupAttemptCounts = groupCounts + }; + db.Set().Add(delayedRetry); + await db.SaveChangesAsync(); + + await BuildJob(db, xs).Execute(); + + // DelayedRetry must be gone + var retryGone = !await db.Set().AnyAsync(r => r.Id == originalXchange.Id); + Assert.True(retryGone, "The processed DelayedRetry record must be deleted."); + + // A new Xchange with RetryFor pointing to the original must exist + var retryXchange = await db.Set() + .FirstOrDefaultAsync(x => x.RetryFor == originalXchange.Id); + Assert.NotNull(retryXchange); + Assert.Equal(originalXchange.Id, retryXchange.RetryFor); + Assert.Equal(sub.Id, retryXchange.SubscriptionId); + } + + [Fact] + public async Task RetryJob_carries_group_attempt_counts_onto_retry_xchange() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(8002, "RetryJob GroupCounts Doc"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var sub = new Subscription("RetryJob GroupCounts Sub", doc.Id); + sub.Inactive = false; + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var originalXchange = await xs.CreateXchange(sub, new XchangeFile("{}")); + + var groupId = Guid.NewGuid().ToString(); + var delayedRetry = new DelayedRetry + { + Id = originalXchange.Id, + On = DateTime.UtcNow.AddMinutes(-1), + GroupAttemptCounts = new System.Collections.Generic.Dictionary + { + [groupId] = 2 + } + }; + db.Set().Add(delayedRetry); + await db.SaveChangesAsync(); + + await BuildJob(db, xs).Execute(); + + var retryXchange = await db.Set() + .FirstOrDefaultAsync(x => x.RetryFor == originalXchange.Id); + Assert.NotNull(retryXchange); + Assert.NotNull(retryXchange.GroupAttemptCounts); + Assert.True(retryXchange.GroupAttemptCounts.TryGetValue(groupId, out var count)); + Assert.Equal(2, count); + } + + [Fact] + public async Task RetryJob_processes_multiple_due_records_in_one_invocation() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var xs = scope.ServiceProvider.GetRequiredService(); + + // Create 3 subscriptions / xchanges + var docs = new[] { 8005, 8006, 8007 }; + var originalIds = new string[3]; + + for (var i = 0; i < 3; i++) + { + var doc = new Document(docs[i], $"RetryJob Batch Doc {i}"); + db.Set().Add(doc); + await db.SaveChangesAsync(); + + var sub = new Subscription($"RetryJob Batch Sub {i}", doc.Id); + sub.Inactive = false; + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var xchange = await xs.CreateXchange(sub, new XchangeFile("{}")); + originalIds[i] = xchange.Id; + + db.Set().Add(new DelayedRetry + { + Id = xchange.Id, + On = DateTime.UtcNow.AddMinutes(-1) + }); + } + + // Also add a future record that must not be processed + var futureId = "rjt-batch-future-" + Guid.NewGuid().ToString("N")[..8]; + db.Set().Add(new DelayedRetry + { + Id = futureId, + On = DateTime.UtcNow.AddHours(1) + }); + await db.SaveChangesAsync(); + + await BuildJob(db, xs).Execute(); + + // All 3 due records removed + foreach (var id in originalIds) + { + var removed = !await db.Set().AnyAsync(r => r.Id == id); + Assert.True(removed, $"Due DelayedRetry {id} must have been processed and removed."); + } + + // Future record untouched + var futureIntact = await db.Set().AnyAsync(r => r.Id == futureId); + Assert.True(futureIntact, "The future DelayedRetry must not be processed."); + + // 3 retry Xchanges created + foreach (var originalId in originalIds) + { + var retryXchange = await db.Set() + .FirstOrDefaultAsync(x => x.RetryFor == originalId); + Assert.NotNull(retryXchange); + } + } +} diff --git a/SW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs b/SW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs new file mode 100644 index 00000000..45f975f2 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs @@ -0,0 +1,407 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.Domain; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.Bitween.Model; +using SW.Bitween.Resources.RetryPolicies; +using SW.PrimitiveTypes; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +[Collection("Bitween")] +public class RetryPolicyTests +{ + private readonly BitweenFixture _fixture; + + public RetryPolicyTests(BitweenFixture fixture) + { + _fixture = fixture; + } + + // ─── Helpers ────────────────────────────────────────────────────────────── + + private static (Create create, Get get, Update update, Delete delete) + Handlers(BitweenDbContext db, RequestContext ctx) => ( + new Create(db, ctx), + new Get(db), + new Update(db, ctx), + new Delete(db, ctx)); + + private static RetryPolicyCreate SimplePolicy(string name) => new() + { + Name = name, + Groups = + [ + new RetryGroup + { + Name = "Timeout", + Priority = 10, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "timeout" }], + Budget = new RetryBudget + { + MaxAttemptsPerError = 3, + MaxAttemptsTotal = 10, + DelayStrategy = new FixedDelayStrategy { DelayMs = 5_000 } + } + } + ] + }; + + // ─── CRUD ────────────────────────────────────────────────────────────────── + + [Fact] + public async Task Can_create_and_get_retry_policy() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.ServiceProvider.GetRequiredService(); + var (create, get, _, _) = Handlers(db, ctx); + + var id = (int)await create.Handle(SimplePolicy("Round-trip Policy")); + + var result = (RetryPolicyUpdate)await get.Handle(id); + + Assert.NotNull(result); + Assert.Equal("Round-trip Policy", result.Name); + Assert.Single(result.Groups); + Assert.Equal("Timeout", result.Groups[0].Name); + } + + [Fact] + public async Task Create_policy_with_complex_groups_round_trips_json_correctly() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.ServiceProvider.GetRequiredService(); + var (create, _, _, _) = Handlers(db, ctx); + + var policy = new RetryPolicyCreate + { + Name = "Complex JSON Policy", + Groups = + [ + new RetryGroup + { + Name = "Exception Group", + Priority = 10, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ExceptionTypeMatcher { Value = "System.TimeoutException" }], + Budget = new RetryBudget + { + MaxAttemptsPerError = 2, + MaxAttemptsTotal = 5, + DelayStrategy = new ExponentialDelayStrategy { InitialDelayMs = 1_000, Multiplier = 2, MaxDelayMs = 60_000 } + } + }, + new RetryGroup + { + Name = "Block Group", + Priority = 20, + AppliesTo = [XchangeResultType.BadResult], + Action = RetryAction.Block, + Matchers = [new JsonPathMatcher { Path = "$.error.code", Op = JsonPathOp.Eq, Value = "500" }] + } + ] + }; + + var id = (int)await create.Handle(policy); + + var reloaded = await db.Set().AsNoTracking().SingleAsync(p => p.Id == id); + + Assert.Equal(2, reloaded.Groups.Count); + + var exGrp = reloaded.Groups.First(g => g.Name == "Exception Group"); + Assert.IsType(exGrp.Matchers[0]); + Assert.IsType(exGrp.Budget!.DelayStrategy); + + var blockGrp = reloaded.Groups.First(g => g.Name == "Block Group"); + Assert.Equal(RetryAction.Block, blockGrp.Action); + Assert.IsType(blockGrp.Matchers[0]); + } + + [Fact] + public async Task Can_update_retry_policy_name_and_groups() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.ServiceProvider.GetRequiredService(); + var (create, _, update, _) = Handlers(db, ctx); + + var id = (int)await create.Handle(SimplePolicy("Before Update")); + + await update.Handle(id, new RetryPolicyUpdate + { + Name = "After Update", + Groups = + [ + new RetryGroup + { + Name = "New Group", + Priority = 5, + AppliesTo = [XchangeResultType.Error], + Matchers = [new RegexMatcher { Pattern = "connect" }], + Budget = new RetryBudget + { + MaxAttemptsPerError = 1, + MaxAttemptsTotal = 5, + DelayStrategy = new LinearDelayStrategy { InitialDelayMs = 1_000, IncrementMs = 500 } + } + } + ] + }); + + var reloaded = await db.Set().AsNoTracking().SingleAsync(p => p.Id == id); + + Assert.Equal("After Update", reloaded.Name); + Assert.Single(reloaded.Groups); + Assert.Equal("New Group", reloaded.Groups[0].Name); + Assert.IsType(reloaded.Groups[0].Budget!.DelayStrategy); + } + + [Fact] + public async Task Can_delete_retry_policy_not_assigned_to_any_subscription() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.ServiceProvider.GetRequiredService(); + var (create, _, _, delete) = Handlers(db, ctx); + + var id = (int)await create.Handle(SimplePolicy("Deletable Policy")); + + await delete.Handle(id); + + var exists = await db.Set().AnyAsync(p => p.Id == id); + Assert.False(exists); + } + + // ─── Delete guard ───────────────────────────────────────────────────────── + + [Fact] + public async Task Cannot_delete_retry_policy_that_is_assigned_to_a_subscription() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.ServiceProvider.GetRequiredService(); + var (create, _, _, delete) = Handlers(db, ctx); + + var doc = new Document(7001, "Delete Guard Doc"); + db.Set().Add(doc); + var sub = new Subscription("Delete Guard Sub", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var policyId = (int)await create.Handle(SimplePolicy("In-Use Policy")); + + sub.SetRetryPolicy(policyId, null); + await db.SaveChangesAsync(); + + await Assert.ThrowsAsync(() => delete.Handle(policyId)); + + // Policy must still exist after the blocked delete + var stillExists = await db.Set().AnyAsync(p => p.Id == policyId); + Assert.True(stillExists); + } + + // ─── Payload validation ─────────────────────────────────────────────────── + + [Fact] + public async Task Creating_policy_with_null_name_violates_not_null_db_constraint() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + db.Set().Add(new RetryPolicy { Name = null!, Groups = [] }); + + await Assert.ThrowsAsync(() => db.SaveChangesAsync()); + } + + [Fact] + public async Task Creating_policy_with_name_over_200_chars_violates_db_constraint() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + db.Set().Add(new RetryPolicy { Name = new string('X', 201), Groups = [] }); + + await Assert.ThrowsAsync(() => db.SaveChangesAsync()); + } + + // ─── Subscription retry fields ──────────────────────────────────────────── + + [Fact] + public async Task Subscription_retry_policy_id_is_persisted_and_fk_resolves() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var ctx = scope.ServiceProvider.GetRequiredService(); + var (create, _, _, _) = Handlers(db, ctx); + + var doc = new Document(7002, "Sub FK Doc"); + db.Set().Add(doc); + var sub = new Subscription("Sub with Policy", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var policyId = (int)await create.Handle(SimplePolicy("FK Test Policy")); + + // Mirror what Subscriptions/Update.cs does: set the FK field and save + sub.SetRetryPolicy(policyId, null); + await db.SaveChangesAsync(); + + var reloaded = await db.Set() + .Include(s => s.RetryPolicy) + .AsNoTracking() + .SingleAsync(s => s.Id == sub.Id); + + Assert.Equal(policyId, reloaded.RetryPolicyId); + Assert.NotNull(reloaded.RetryPolicy); + Assert.Equal("FK Test Policy", reloaded.RetryPolicy.Name); + } + + [Fact] + public async Task Subscription_custom_retry_policy_json_is_persisted_and_reloads_with_polymorphic_types() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(7003, "Sub Custom Policy Doc"); + db.Set().Add(doc); + var sub = new Subscription("Sub with Custom Policy", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + // Mirror what Subscriptions/Update.cs does: set the inline policy and save + sub.SetRetryPolicy(null, new CustomRetryPolicy + { + Groups = + [ + new RetryGroup + { + Name = "Custom Timeout", + Priority = 10, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "timeout" }], + Budget = new RetryBudget + { + MaxAttemptsPerError = 3, + MaxAttemptsTotal = 10, + DelayStrategy = new LinearDelayStrategy { InitialDelayMs = 1_000, IncrementMs = 500 } + } + } + ] + }); + await db.SaveChangesAsync(); + + var reloaded = await db.Set().AsNoTracking().SingleAsync(s => s.Id == sub.Id); + + Assert.NotNull(reloaded.CustomRetryPolicy); + Assert.Single(reloaded.CustomRetryPolicy.Groups); + Assert.Equal("Custom Timeout", reloaded.CustomRetryPolicy.Groups[0].Name); + Assert.IsType(reloaded.CustomRetryPolicy.Groups[0].Matchers[0]); + Assert.IsType(reloaded.CustomRetryPolicy.Groups[0].Budget!.DelayStrategy); + } + + [Fact] + public async Task Removing_retry_policy_nullifies_subscription_fk_via_set_null_cascade() + { + await using var scope = _fixture.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var doc = new Document(7004, "Sub SetNull Doc"); + db.Set().Add(doc); + var sub = new Subscription("Sub SetNull Test", doc.Id); + db.Set().Add(sub); + await db.SaveChangesAsync(); + + var policy = new RetryPolicy { Name = "SetNull Policy", Groups = [] }; + db.Set().Add(policy); + await db.SaveChangesAsync(); + + sub.SetRetryPolicy(policy.Id, null); + await db.SaveChangesAsync(); + + // Remove the policy directly (bypassing the handler's guard) to test the ON DELETE SET NULL cascade + db.Set().Remove(policy); + await db.SaveChangesAsync(); + + var reloaded = await db.Set().AsNoTracking().SingleAsync(s => s.Id == sub.Id); + Assert.Null(reloaded.RetryPolicyId); + } + + // ─── Test / dry-run endpoint ──────────────────────────────────────────────── + + [Fact] + public async Task Test_simulates_consecutive_attempts_and_stops_once_blocked() + { + await using var scope = _fixture.CreateScope(); + var ctx = scope.ServiceProvider.GetRequiredService(); + var handler = new Resources.RetryPolicies.Test(ctx); + + var request = new TestRetryPolicyRequest + { + Groups = SimplePolicy("Dry-run Policy").Groups, + ResultType = XchangeResultType.Error, + Content = "System.TimeoutException: contains timeout", + AttemptsToSimulate = 5 + }; + + var response = (TestRetryPolicyResponse)await handler.Handle(request); + + // Budget is MaxAttemptsPerError = 3, so attempts 1-3 retry and attempt 4 is blocked; + // simulation stops there rather than continuing to the requested 5. + Assert.Equal(4, response.Attempts.Count); + Assert.All(response.Attempts.Take(3), a => + { + Assert.True(a.ShouldRetry); + Assert.Equal("Timeout", a.MatchedGroupName); + Assert.Equal(5, a.DelaySeconds); + }); + Assert.False(response.Attempts[3].ShouldRetry); + Assert.Null(response.Attempts[3].MatchedGroupName); + } + + [Fact] + public async Task Test_rejects_success_result_type() + { + await using var scope = _fixture.CreateScope(); + var ctx = scope.ServiceProvider.GetRequiredService(); + var handler = new Resources.RetryPolicies.Test(ctx); + + var request = new TestRetryPolicyRequest + { + Groups = [], + ResultType = XchangeResultType.Success, + Content = "n/a" + }; + + await Assert.ThrowsAsync(() => handler.Handle(request)); + } + + [Fact] + public async Task Test_reports_no_match_when_no_group_applies() + { + await using var scope = _fixture.CreateScope(); + var ctx = scope.ServiceProvider.GetRequiredService(); + var handler = new Resources.RetryPolicies.Test(ctx); + + var request = new TestRetryPolicyRequest + { + Groups = SimplePolicy("Dry-run Policy").Groups, + ResultType = XchangeResultType.Error, + Content = "System.NullReferenceException: unrelated failure", + AttemptsToSimulate = 3 + }; + + var response = (TestRetryPolicyResponse)await handler.Handle(request); + + Assert.Single(response.Attempts); + Assert.False(response.Attempts[0].ShouldRetry); + Assert.Null(response.Attempts[0].MatchedGroupName); + } + +} diff --git a/SW.Bitween.IntegrationTests/Tests/ServerlessAdapterTests.cs b/SW.Bitween.IntegrationTests/Tests/ServerlessAdapterTests.cs new file mode 100644 index 00000000..bdd4fd31 --- /dev/null +++ b/SW.Bitween.IntegrationTests/Tests/ServerlessAdapterTests.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using SW.Bitween.IntegrationTests.Fixtures; +using SW.PrimitiveTypes; +using SW.Serverless; +using Xunit; + +namespace SW.Bitween.IntegrationTests.Tests; + +[Collection("Bitween")] +public class ServerlessAdapterTests +{ + private readonly BitweenFixture _fixture; + + public ServerlessAdapterTests(BitweenFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task SampleHandler_echo_returns_input_unchanged() + { + await using var scope = _fixture.CreateScope(); + var serverless = scope.ServiceProvider.GetRequiredService(); + + var correlationId = Guid.NewGuid().ToString(); + await serverless.StartAsync("sw.bitween.samplehandler", correlationId, + new Dictionary { ["ContentType"] = "text/plain" }); + + var input = new XchangeFile("hello from integration test"); + var result = await serverless.InvokeAsync("Handle", input); + + Assert.NotNull(result); + Assert.Equal(input.Data, result.Data); + } + + [Fact] + public async Task ConfigurableAdapter_with_output_data_overrides_response() + { + await using var scope = _fixture.CreateScope(); + var serverless = scope.ServiceProvider.GetRequiredService(); + + var correlationId = Guid.NewGuid().ToString(); + await serverless.StartAsync("sw.bitween.sampleconfigurableadapter", correlationId, + new Dictionary { ["OutputData"] = "overridden output" }); + + var result = await serverless.InvokeAsync("Handle", new XchangeFile("{}")); + + Assert.NotNull(result); + Assert.Equal("overridden output", result.Data); + } + + [Fact] + public async Task ConfigurableAdapter_simulate_error_throws_on_invoke() + { + await using var scope = _fixture.CreateScope(); + var serverless = scope.ServiceProvider.GetRequiredService(); + + var correlationId = Guid.NewGuid().ToString(); + await serverless.StartAsync("sw.bitween.sampleconfigurableadapter", correlationId, + new Dictionary + { + ["SimulateError"] = "true", + ["ErrorMessage"] = "test failure from adapter" + }); + + await Assert.ThrowsAnyAsync(() => + serverless.InvokeAsync("Handle", new XchangeFile("{}"))); + } + + [Fact] + public async Task ConfigurableAdapter_delay_completes_within_tolerance() + { + await using var scope = _fixture.CreateScope(); + var serverless = scope.ServiceProvider.GetRequiredService(); + + var correlationId = Guid.NewGuid().ToString(); + await serverless.StartAsync("sw.bitween.sampleconfigurableadapter", correlationId, + new Dictionary { ["DelayMs"] = "300" }); + + var sw = Stopwatch.StartNew(); + var result = await serverless.InvokeAsync("Handle", new XchangeFile("{}")); + sw.Stop(); + + Assert.NotNull(result); + Assert.True(sw.ElapsedMilliseconds >= 300, + $"Expected at least 300ms delay, actual: {sw.ElapsedMilliseconds}ms"); + } +} diff --git a/SW.Bitween.IntegrationTests/xunit.runner.json b/SW.Bitween.IntegrationTests/xunit.runner.json new file mode 100644 index 00000000..2a4dfae0 --- /dev/null +++ b/SW.Bitween.IntegrationTests/xunit.runner.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "parallelizeAssembly": false, + "parallelizeTestCollections": false, + "diagnosticMessages": true, + "longRunningTestSeconds": 300 +} diff --git a/SW.Bitween.MsSql/BitweenDbContext.cs b/SW.Bitween.MsSql/BitweenDbContext.cs new file mode 100644 index 00000000..6088c1ef --- /dev/null +++ b/SW.Bitween.MsSql/BitweenDbContext.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using SW.PrimitiveTypes; +using SW.Scheduler.SqlServer; + +namespace SW.Bitween.MsSql +{ + public class BitweenDbContext : Bitween.BitweenDbContext + { + public BitweenDbContext(DbContextOptions options, RequestContext requestContext, IPublish publish) + : base(options, requestContext, publish) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.UseSchedulerSqlServer(); + } + } +} diff --git a/SW.Bitween.MsSql/BitweenDbContextFactory.cs b/SW.Bitween.MsSql/BitweenDbContextFactory.cs new file mode 100644 index 00000000..58ca6ce6 --- /dev/null +++ b/SW.Bitween.MsSql/BitweenDbContextFactory.cs @@ -0,0 +1,23 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace SW.Bitween.MsSql +{ + public class BitweenDbContextFactory : IDesignTimeDbContextFactory + { + public BitweenDbContext CreateDbContext(string[] args) + { + var connStr = Environment.GetEnvironmentVariable("ConnectionStrings__BitweenDb") + ?? "Server=localhost,1433;Database=bitween;User Id=sa;Password=Pass@word123;TrustServerCertificate=True"; + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlServer(connStr, b => + { + b.MigrationsAssembly(typeof(DbType).Assembly.FullName); + }); + + return new BitweenDbContext(optionsBuilder.Options, null!, null!); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260706071041_QuartzAndAutoRetry.Designer.cs b/SW.Bitween.MsSql/Migrations/20260706071041_QuartzAndAutoRetry.Designer.cs new file mode 100644 index 00000000..86d6cfbf --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260706071041_QuartzAndAutoRetry.Designer.cs @@ -0,0 +1,1777 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260706071041_QuartzAndAutoRetry")] + partial class QuartzAndAutoRetry + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("GroupAttemptCounts") + .HasColumnType("nvarchar(max)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("GroupAttemptCounts") + .HasColumnType("nvarchar(max)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260706071041_QuartzAndAutoRetry.cs b/SW.Bitween.MsSql/Migrations/20260706071041_QuartzAndAutoRetry.cs new file mode 100644 index 00000000..d484a657 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260706071041_QuartzAndAutoRetry.cs @@ -0,0 +1,522 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class QuartzAndAutoRetry : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "dbo"); + + migrationBuilder.AddColumn( + name: "GroupAttemptCounts", + table: "Xchanges", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "CustomRetryPolicy", + table: "Subscriptions", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "RetryPolicyId", + table: "Subscriptions", + type: "int", + nullable: true); + + migrationBuilder.CreateTable( + name: "DelayedRetries", + columns: table => new + { + Id = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false), + On = table.Column(type: "datetime2", nullable: false), + GroupAttemptCounts = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_DelayedRetries", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "job_executions", + schema: "dbo", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + job_name = table.Column(type: "nvarchar(450)", nullable: false), + job_group = table.Column(type: "nvarchar(450)", nullable: false), + job_type_name = table.Column(type: "nvarchar(450)", nullable: false), + fire_instance_id = table.Column(type: "nvarchar(450)", nullable: false), + start_time_utc = table.Column(type: "datetime2", nullable: false), + end_time_utc = table.Column(type: "datetime2", nullable: true), + duration_ms = table.Column(type: "bigint", nullable: true), + success = table.Column(type: "bit", nullable: true), + error = table.Column(type: "nvarchar(max)", nullable: true), + node = table.Column(type: "nvarchar(450)", nullable: false), + context = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_job_executions", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "QRTZ_calendars", + schema: "dbo", + columns: table => new + { + sched_name = table.Column(type: "nvarchar(450)", nullable: false), + calendar_name = table.Column(type: "nvarchar(450)", nullable: false), + calendar = table.Column(type: "varbinary(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_calendars", x => new { x.sched_name, x.calendar_name }); + }); + + migrationBuilder.CreateTable( + name: "QRTZ_fired_triggers", + schema: "dbo", + columns: table => new + { + sched_name = table.Column(type: "nvarchar(450)", nullable: false), + entry_id = table.Column(type: "nvarchar(450)", nullable: false), + trigger_name = table.Column(type: "nvarchar(450)", nullable: false), + trigger_group = table.Column(type: "nvarchar(450)", nullable: false), + instance_name = table.Column(type: "nvarchar(450)", nullable: false), + fired_time = table.Column(type: "bigint", nullable: false), + sched_time = table.Column(type: "bigint", nullable: false), + priority = table.Column(type: "int", nullable: false), + state = table.Column(type: "nvarchar(450)", nullable: false), + job_name = table.Column(type: "nvarchar(450)", nullable: true), + job_group = table.Column(type: "nvarchar(450)", nullable: true), + is_nonconcurrent = table.Column(type: "bit", nullable: false), + requests_recovery = table.Column(type: "bit", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_fired_triggers", x => new { x.sched_name, x.entry_id }); + }); + + migrationBuilder.CreateTable( + name: "QRTZ_job_details", + schema: "dbo", + columns: table => new + { + sched_name = table.Column(type: "nvarchar(450)", nullable: false), + job_name = table.Column(type: "nvarchar(450)", nullable: false), + job_group = table.Column(type: "nvarchar(450)", nullable: false), + description = table.Column(type: "nvarchar(450)", nullable: true), + job_class_name = table.Column(type: "nvarchar(450)", nullable: false), + is_durable = table.Column(type: "bit", nullable: false), + is_nonconcurrent = table.Column(type: "bit", nullable: false), + is_update_data = table.Column(type: "bit", nullable: false), + requests_recovery = table.Column(type: "bit", nullable: false), + job_data = table.Column(type: "varbinary(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_job_details", x => new { x.sched_name, x.job_name, x.job_group }); + }); + + migrationBuilder.CreateTable( + name: "QRTZ_locks", + schema: "dbo", + columns: table => new + { + sched_name = table.Column(type: "nvarchar(450)", nullable: false), + lock_name = table.Column(type: "nvarchar(450)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_locks", x => new { x.sched_name, x.lock_name }); + }); + + migrationBuilder.CreateTable( + name: "QRTZ_paused_trigger_grps", + schema: "dbo", + columns: table => new + { + sched_name = table.Column(type: "nvarchar(450)", nullable: false), + trigger_group = table.Column(type: "nvarchar(450)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_paused_trigger_grps", x => new { x.sched_name, x.trigger_group }); + }); + + migrationBuilder.CreateTable( + name: "QRTZ_scheduler_state", + schema: "dbo", + columns: table => new + { + sched_name = table.Column(type: "nvarchar(450)", nullable: false), + instance_name = table.Column(type: "nvarchar(450)", nullable: false), + last_checkin_time = table.Column(type: "bigint", nullable: false), + checkin_interval = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_scheduler_state", x => new { x.sched_name, x.instance_name }); + }); + + migrationBuilder.CreateTable( + name: "RetryPolicies", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + Groups = table.Column(type: "nvarchar(max)", nullable: true), + UpdatedBy = table.Column(type: "nvarchar(max)", nullable: true), + UpdatedAt = table.Column(type: "datetimeoffset", nullable: false), + CreatedOn = table.Column(type: "datetime2", nullable: false), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), + ModifiedOn = table.Column(type: "datetime2", nullable: true), + ModifiedBy = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RetryPolicies", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "QRTZ_triggers", + schema: "dbo", + columns: table => new + { + sched_name = table.Column(type: "nvarchar(450)", nullable: false), + trigger_name = table.Column(type: "nvarchar(450)", nullable: false), + trigger_group = table.Column(type: "nvarchar(450)", nullable: false), + job_name = table.Column(type: "nvarchar(450)", nullable: false), + job_group = table.Column(type: "nvarchar(450)", nullable: false), + description = table.Column(type: "nvarchar(450)", nullable: true), + next_fire_time = table.Column(type: "bigint", nullable: true), + prev_fire_time = table.Column(type: "bigint", nullable: true), + priority = table.Column(type: "int", nullable: true), + trigger_state = table.Column(type: "nvarchar(450)", nullable: false), + trigger_type = table.Column(type: "nvarchar(450)", nullable: false), + start_time = table.Column(type: "bigint", nullable: false), + end_time = table.Column(type: "bigint", nullable: true), + calendar_name = table.Column(type: "nvarchar(450)", nullable: true), + misfire_instr = table.Column(type: "int", nullable: true), + job_data = table.Column(type: "varbinary(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_triggers", x => new { x.sched_name, x.trigger_name, x.trigger_group }); + table.ForeignKey( + name: "FK_QRTZ_triggers_QRTZ_job_details_sched_name_job_name_job_group", + columns: x => new { x.sched_name, x.job_name, x.job_group }, + principalSchema: "dbo", + principalTable: "QRTZ_job_details", + principalColumns: new[] { "sched_name", "job_name", "job_group" }, + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "QRTZ_blob_triggers", + schema: "dbo", + columns: table => new + { + sched_name = table.Column(type: "nvarchar(450)", nullable: false), + trigger_name = table.Column(type: "nvarchar(450)", nullable: false), + trigger_group = table.Column(type: "nvarchar(450)", nullable: false), + blob_data = table.Column(type: "varbinary(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_blob_triggers", x => new { x.sched_name, x.trigger_name, x.trigger_group }); + table.ForeignKey( + name: "FK_QRTZ_blob_triggers_QRTZ_triggers_sched_name_trigger_name_trigger_group", + columns: x => new { x.sched_name, x.trigger_name, x.trigger_group }, + principalSchema: "dbo", + principalTable: "QRTZ_triggers", + principalColumns: new[] { "sched_name", "trigger_name", "trigger_group" }, + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "QRTZ_cron_triggers", + schema: "dbo", + columns: table => new + { + sched_name = table.Column(type: "nvarchar(450)", nullable: false), + trigger_name = table.Column(type: "nvarchar(450)", nullable: false), + trigger_group = table.Column(type: "nvarchar(450)", nullable: false), + cron_expression = table.Column(type: "nvarchar(450)", nullable: false), + time_zone_id = table.Column(type: "nvarchar(450)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_cron_triggers", x => new { x.sched_name, x.trigger_name, x.trigger_group }); + table.ForeignKey( + name: "FK_QRTZ_cron_triggers_QRTZ_triggers_sched_name_trigger_name_trigger_group", + columns: x => new { x.sched_name, x.trigger_name, x.trigger_group }, + principalSchema: "dbo", + principalTable: "QRTZ_triggers", + principalColumns: new[] { "sched_name", "trigger_name", "trigger_group" }, + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "QRTZ_simple_triggers", + schema: "dbo", + columns: table => new + { + sched_name = table.Column(type: "nvarchar(450)", nullable: false), + trigger_name = table.Column(type: "nvarchar(450)", nullable: false), + trigger_group = table.Column(type: "nvarchar(450)", nullable: false), + repeat_count = table.Column(type: "bigint", nullable: false), + repeat_interval = table.Column(type: "bigint", nullable: false), + times_triggered = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_simple_triggers", x => new { x.sched_name, x.trigger_name, x.trigger_group }); + table.ForeignKey( + name: "FK_QRTZ_simple_triggers_QRTZ_triggers_sched_name_trigger_name_trigger_group", + columns: x => new { x.sched_name, x.trigger_name, x.trigger_group }, + principalSchema: "dbo", + principalTable: "QRTZ_triggers", + principalColumns: new[] { "sched_name", "trigger_name", "trigger_group" }, + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "QRTZ_simprop_triggers", + schema: "dbo", + columns: table => new + { + sched_name = table.Column(type: "nvarchar(450)", nullable: false), + trigger_name = table.Column(type: "nvarchar(450)", nullable: false), + trigger_group = table.Column(type: "nvarchar(450)", nullable: false), + str_prop_1 = table.Column(type: "nvarchar(450)", nullable: true), + str_prop_2 = table.Column(type: "nvarchar(450)", nullable: true), + str_prop_3 = table.Column(type: "nvarchar(450)", nullable: true), + int_prop_1 = table.Column(type: "int", nullable: true), + int_prop_2 = table.Column(type: "int", nullable: true), + long_prop_1 = table.Column(type: "bigint", nullable: true), + long_prop_2 = table.Column(type: "bigint", nullable: true), + dec_prop_1 = table.Column(type: "numeric(18,0)", nullable: true), + dec_prop_2 = table.Column(type: "numeric(18,0)", nullable: true), + bool_prop_1 = table.Column(type: "bit", nullable: true), + bool_prop_2 = table.Column(type: "bit", nullable: true), + time_zone_id = table.Column(type: "nvarchar(450)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_simprop_triggers", x => new { x.sched_name, x.trigger_name, x.trigger_group }); + table.ForeignKey( + name: "FK_QRTZ_simprop_triggers_QRTZ_triggers_sched_name_trigger_name_trigger_group", + columns: x => new { x.sched_name, x.trigger_name, x.trigger_group }, + principalSchema: "dbo", + principalTable: "QRTZ_triggers", + principalColumns: new[] { "sched_name", "trigger_name", "trigger_group" }, + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Subscriptions_RetryPolicyId", + table: "Subscriptions", + column: "RetryPolicyId"); + + migrationBuilder.CreateIndex( + name: "IX_DelayedRetries_On", + table: "DelayedRetries", + column: "On"); + + migrationBuilder.CreateIndex( + name: "idx_je_fire_instance_id", + schema: "dbo", + table: "job_executions", + column: "fire_instance_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "idx_je_group_name_start", + schema: "dbo", + table: "job_executions", + columns: new[] { "job_group", "job_name", "start_time_utc" }); + + migrationBuilder.CreateIndex( + name: "idx_je_start_time", + schema: "dbo", + table: "job_executions", + column: "start_time_utc"); + + migrationBuilder.CreateIndex( + name: "idx_je_success", + schema: "dbo", + table: "job_executions", + column: "success"); + + migrationBuilder.CreateIndex( + name: "idx_QRTZ_ft_job_group", + schema: "dbo", + table: "QRTZ_fired_triggers", + column: "job_group"); + + migrationBuilder.CreateIndex( + name: "idx_QRTZ_ft_job_name", + schema: "dbo", + table: "QRTZ_fired_triggers", + column: "job_name"); + + migrationBuilder.CreateIndex( + name: "idx_QRTZ_ft_job_req_recovery", + schema: "dbo", + table: "QRTZ_fired_triggers", + column: "requests_recovery"); + + migrationBuilder.CreateIndex( + name: "idx_QRTZ_ft_trig_group", + schema: "dbo", + table: "QRTZ_fired_triggers", + column: "trigger_group"); + + migrationBuilder.CreateIndex( + name: "idx_QRTZ_ft_trig_inst_name", + schema: "dbo", + table: "QRTZ_fired_triggers", + column: "instance_name"); + + migrationBuilder.CreateIndex( + name: "idx_QRTZ_ft_trig_name", + schema: "dbo", + table: "QRTZ_fired_triggers", + column: "trigger_name"); + + migrationBuilder.CreateIndex( + name: "idx_QRTZ_ft_trig_nm_gp", + schema: "dbo", + table: "QRTZ_fired_triggers", + columns: new[] { "sched_name", "trigger_name", "trigger_group" }); + + migrationBuilder.CreateIndex( + name: "idx_j_req_recovery", + schema: "dbo", + table: "QRTZ_job_details", + column: "requests_recovery"); + + migrationBuilder.CreateIndex( + name: "idx_t_next_fire_time", + schema: "dbo", + table: "QRTZ_triggers", + column: "next_fire_time"); + + migrationBuilder.CreateIndex( + name: "idx_t_nft_st", + schema: "dbo", + table: "QRTZ_triggers", + columns: new[] { "next_fire_time", "trigger_state" }); + + migrationBuilder.CreateIndex( + name: "idx_t_state", + schema: "dbo", + table: "QRTZ_triggers", + column: "trigger_state"); + + migrationBuilder.CreateIndex( + name: "IX_QRTZ_triggers_sched_name_job_name_job_group", + schema: "dbo", + table: "QRTZ_triggers", + columns: new[] { "sched_name", "job_name", "job_group" }); + + migrationBuilder.AddForeignKey( + name: "FK_Subscriptions_RetryPolicies_RetryPolicyId", + table: "Subscriptions", + column: "RetryPolicyId", + principalTable: "RetryPolicies", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Subscriptions_RetryPolicies_RetryPolicyId", + table: "Subscriptions"); + + migrationBuilder.DropTable( + name: "DelayedRetries"); + + migrationBuilder.DropTable( + name: "job_executions", + schema: "dbo"); + + migrationBuilder.DropTable( + name: "QRTZ_blob_triggers", + schema: "dbo"); + + migrationBuilder.DropTable( + name: "QRTZ_calendars", + schema: "dbo"); + + migrationBuilder.DropTable( + name: "QRTZ_cron_triggers", + schema: "dbo"); + + migrationBuilder.DropTable( + name: "QRTZ_fired_triggers", + schema: "dbo"); + + migrationBuilder.DropTable( + name: "QRTZ_locks", + schema: "dbo"); + + migrationBuilder.DropTable( + name: "QRTZ_paused_trigger_grps", + schema: "dbo"); + + migrationBuilder.DropTable( + name: "QRTZ_scheduler_state", + schema: "dbo"); + + migrationBuilder.DropTable( + name: "QRTZ_simple_triggers", + schema: "dbo"); + + migrationBuilder.DropTable( + name: "QRTZ_simprop_triggers", + schema: "dbo"); + + migrationBuilder.DropTable( + name: "RetryPolicies"); + + migrationBuilder.DropTable( + name: "QRTZ_triggers", + schema: "dbo"); + + migrationBuilder.DropTable( + name: "QRTZ_job_details", + schema: "dbo"); + + migrationBuilder.DropIndex( + name: "IX_Subscriptions_RetryPolicyId", + table: "Subscriptions"); + + migrationBuilder.DropColumn( + name: "GroupAttemptCounts", + table: "Xchanges"); + + migrationBuilder.DropColumn( + name: "CustomRetryPolicy", + table: "Subscriptions"); + + migrationBuilder.DropColumn( + name: "RetryPolicyId", + table: "Subscriptions"); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260707135042_DropRetryPolicyUpdatedFields.Designer.cs b/SW.Bitween.MsSql/Migrations/20260707135042_DropRetryPolicyUpdatedFields.Designer.cs new file mode 100644 index 00000000..1613f9c9 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260707135042_DropRetryPolicyUpdatedFields.Designer.cs @@ -0,0 +1,1889 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MsSql; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260707135042_DropRetryPolicyUpdatedFields")] + partial class DropRetryPolicyUpdatedFields + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Disabled") + .HasColumnType("bit"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint"); + + b.Property("LoginMethods") + .HasColumnType("tinyint"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasFilter("[Email] IS NOT NULL"); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("LoginMethod") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("GroupAttemptCounts") + .HasColumnType("nvarchar(max)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("bit"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("bit"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasFilter("[BusMessageTypeName] IS NOT NULL"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Values") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("bit"); + + b.Property("RunOnFailedResult") + .HasColumnType("bit"); + + b.Property("RunOnSubscriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("bit"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime2"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentFilter") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Inactive") + .HasColumnType("bit"); + + b.Property("IsRunning") + .HasColumnType("bit"); + + b.Property("LastException") + .HasColumnType("nvarchar(max)"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("MatchExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime2"); + + b.Property("ReceiveOn") + .HasColumnType("datetime2"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("bit"); + + b.Property("Type") + .HasColumnType("tinyint"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[Code] IS NOT NULL"); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("StateAfter") + .HasColumnType("nvarchar(max)"); + + b.Property("StateBefore") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("GroupAttemptCounts") + .HasColumnType("nvarchar(max)"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime2"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime2"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("PropertiesRaw") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("FinishedOn") + .HasColumnType("datetime2"); + + b.Property("OutputBad") + .HasColumnType("bit"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("bit"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("nvarchar(max)"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("bit"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("bit"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/20260707135042_DropRetryPolicyUpdatedFields.cs b/SW.Bitween.MsSql/Migrations/20260707135042_DropRetryPolicyUpdatedFields.cs new file mode 100644 index 00000000..180786e3 --- /dev/null +++ b/SW.Bitween.MsSql/Migrations/20260707135042_DropRetryPolicyUpdatedFields.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MsSql.Migrations +{ + /// + public partial class DropRetryPolicyUpdatedFields : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UpdatedAt", + table: "RetryPolicies"); + + migrationBuilder.DropColumn( + name: "UpdatedBy", + table: "RetryPolicies"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UpdatedAt", + table: "RetryPolicies", + type: "datetimeoffset", + nullable: false, + defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0))); + + migrationBuilder.AddColumn( + name: "UpdatedBy", + table: "RetryPolicies", + type: "nvarchar(max)", + nullable: true); + } + } +} diff --git a/SW.Bitween.MsSql/Migrations/BitweenDbContextModelSnapshot.cs b/SW.Bitween.MsSql/Migrations/BitweenDbContextModelSnapshot.cs index 7c943b66..43e9d519 100644 --- a/SW.Bitween.MsSql/Migrations/BitweenDbContextModelSnapshot.cs +++ b/SW.Bitween.MsSql/Migrations/BitweenDbContextModelSnapshot.cs @@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using SW.Bitween; +using SW.Bitween.MsSql; #nullable disable @@ -124,6 +124,26 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("RefreshTokens", (string)null); }); + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("GroupAttemptCounts") + .HasColumnType("nvarchar(max)"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + modelBuilder.Entity("SW.Bitween.Domain.Document", b => { b.Property("Id") @@ -478,6 +498,39 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Groups") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedOn") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => { b.Property("Id") @@ -501,6 +554,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ConsecutiveFailures") .HasColumnType("int"); + b.Property("CustomRetryPolicy") + .HasColumnType("nvarchar(max)"); + b.Property("DocumentFilter") .HasColumnType("nvarchar(max)"); @@ -565,6 +621,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ResponseSubscriptionId") .HasColumnType("int"); + b.Property("RetryPolicyId") + .HasColumnType("int"); + b.Property("Temporary") .HasColumnType("bit"); @@ -594,6 +653,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ResponseSubscriptionId"); + b.HasIndex("RetryPolicyId"); + b.HasIndex("WorkGroupId"); b.ToTable("Subscriptions", (string)null); @@ -705,6 +766,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DocumentId") .HasColumnType("int"); + b.Property("GroupAttemptCounts") + .HasColumnType("nvarchar(max)"); + b.Property("HandlerId") .HasMaxLength(200) .IsUnicode(false) @@ -952,6 +1016,525 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToView(null, (string)null); }); + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("nvarchar(max)") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("nvarchar(max)") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("bit") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("varbinary(max)") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("varbinary(max)") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("nvarchar(450)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bit") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bit") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bit") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bit") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("nvarchar(450)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("nvarchar(450)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bit") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bit") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("nvarchar(450)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(450)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", "dbo"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("nvarchar(450)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("nvarchar(450)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("varbinary(max)") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("nvarchar(450)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", "dbo"); + }); + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => { b.HasOne("SW.Bitween.Domain.Accounts.Account", null) @@ -1110,6 +1693,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Restrict) .HasConstraintName("FK_Subscriptions_RespSub"); + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") .WithMany() .HasForeignKey("WorkGroupId"); @@ -1144,6 +1732,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Category"); + b.Navigation("RetryPolicy"); + b.Navigation("Schedules"); b.Navigation("WorkGroup"); @@ -1205,6 +1795,61 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => { b.Navigation("Partners"); @@ -1219,6 +1864,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Navigation("Subscriptions"); }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); #pragma warning restore 612, 618 } } diff --git a/SW.Bitween.MsSql/SW.Bitween.MsSql.csproj b/SW.Bitween.MsSql/SW.Bitween.MsSql.csproj index d13d5790..a8b69a30 100644 --- a/SW.Bitween.MsSql/SW.Bitween.MsSql.csproj +++ b/SW.Bitween.MsSql/SW.Bitween.MsSql.csproj @@ -7,10 +7,15 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/SW.Bitween.MySql/BitweenDbContext.cs b/SW.Bitween.MySql/BitweenDbContext.cs new file mode 100644 index 00000000..27dc7ddc --- /dev/null +++ b/SW.Bitween.MySql/BitweenDbContext.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using SW.PrimitiveTypes; +using SW.Scheduler.MySql; + +namespace SW.Bitween.MySql +{ + public class BitweenDbContext : Bitween.BitweenDbContext + { + public BitweenDbContext(DbContextOptions options, RequestContext requestContext, IPublish publish) + : base(options, requestContext, publish) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.UseSchedulerMySql(); + } + } +} diff --git a/SW.Bitween.MySql/BitweenDbContextFactory.cs b/SW.Bitween.MySql/BitweenDbContextFactory.cs new file mode 100644 index 00000000..58596d1e --- /dev/null +++ b/SW.Bitween.MySql/BitweenDbContextFactory.cs @@ -0,0 +1,23 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace SW.Bitween.MySql +{ + public class BitweenDbContextFactory : IDesignTimeDbContextFactory + { + public BitweenDbContext CreateDbContext(string[] args) + { + var connStr = Environment.GetEnvironmentVariable("ConnectionStrings__BitweenDb") + ?? "Server=localhost;Port=3307;Database=bitween;User=root;Password=mysql"; + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseMySql(connStr, new MySqlServerVersion(new Version(8, 0, 18)), b => + { + b.MigrationsAssembly(typeof(DbType).Assembly.FullName); + }); + + return new BitweenDbContext(optionsBuilder.Options, null!, null!); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260706071032_QuartzAndAutoRetry.Designer.cs b/SW.Bitween.MySql/Migrations/20260706071032_QuartzAndAutoRetry.Designer.cs new file mode 100644 index 00000000..8658c025 --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260706071032_QuartzAndAutoRetry.Designer.cs @@ -0,0 +1,1774 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260706071032_QuartzAndAutoRetry")] + partial class QuartzAndAutoRetry + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("GroupAttemptCounts") + .HasColumnType("longtext"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("GroupAttemptCounts") + .HasColumnType("longtext"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260706071032_QuartzAndAutoRetry.cs b/SW.Bitween.MySql/Migrations/20260706071032_QuartzAndAutoRetry.cs new file mode 100644 index 00000000..b68c815e --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260706071032_QuartzAndAutoRetry.cs @@ -0,0 +1,553 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class QuartzAndAutoRetry : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "GroupAttemptCounts", + table: "Xchanges", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "CustomRetryPolicy", + table: "Subscriptions", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "RetryPolicyId", + table: "Subscriptions", + type: "int", + nullable: true); + + migrationBuilder.CreateTable( + name: "DelayedRetries", + columns: table => new + { + Id = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + On = table.Column(type: "datetime(6)", nullable: false), + GroupAttemptCounts = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_DelayedRetries", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "job_executions", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + job_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + job_group = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + job_type_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + fire_instance_id = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + start_time_utc = table.Column(type: "datetime(6)", nullable: false), + end_time_utc = table.Column(type: "datetime(6)", nullable: true), + duration_ms = table.Column(type: "bigint", nullable: true), + success = table.Column(type: "tinyint(1)", nullable: true), + error = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + node = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + context = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_job_executions", x => x.id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "QRTZ_calendars", + columns: table => new + { + sched_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + calendar_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + calendar = table.Column(type: "longblob", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_calendars", x => new { x.sched_name, x.calendar_name }); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "QRTZ_fired_triggers", + columns: table => new + { + sched_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + entry_id = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + trigger_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + trigger_group = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + instance_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + fired_time = table.Column(type: "bigint", nullable: false), + sched_time = table.Column(type: "bigint", nullable: false), + priority = table.Column(type: "int", nullable: false), + state = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + job_name = table.Column(type: "varchar(200)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + job_group = table.Column(type: "varchar(200)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + is_nonconcurrent = table.Column(type: "tinyint(1)", nullable: false), + requests_recovery = table.Column(type: "tinyint(1)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_fired_triggers", x => new { x.sched_name, x.entry_id }); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "QRTZ_job_details", + columns: table => new + { + sched_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + job_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + job_group = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + description = table.Column(type: "varchar(200)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + job_class_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + is_durable = table.Column(type: "tinyint(1)", nullable: false), + is_nonconcurrent = table.Column(type: "tinyint(1)", nullable: false), + is_update_data = table.Column(type: "tinyint(1)", nullable: false), + requests_recovery = table.Column(type: "tinyint(1)", nullable: false), + job_data = table.Column(type: "longblob", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_job_details", x => new { x.sched_name, x.job_name, x.job_group }); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "QRTZ_locks", + columns: table => new + { + sched_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + lock_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_locks", x => new { x.sched_name, x.lock_name }); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "QRTZ_paused_trigger_grps", + columns: table => new + { + sched_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + trigger_group = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_paused_trigger_grps", x => new { x.sched_name, x.trigger_group }); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "QRTZ_scheduler_state", + columns: table => new + { + sched_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + instance_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + last_checkin_time = table.Column(type: "bigint", nullable: false), + checkin_interval = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_scheduler_state", x => new { x.sched_name, x.instance_name }); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "RetryPolicies", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Name = table.Column(type: "varchar(200)", maxLength: 200, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Groups = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + UpdatedBy = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false), + CreatedOn = table.Column(type: "datetime(6)", nullable: false), + CreatedBy = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ModifiedOn = table.Column(type: "datetime(6)", nullable: true), + ModifiedBy = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_RetryPolicies", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "QRTZ_triggers", + columns: table => new + { + sched_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + trigger_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + trigger_group = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + job_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + job_group = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + description = table.Column(type: "varchar(200)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + next_fire_time = table.Column(type: "bigint", nullable: true), + prev_fire_time = table.Column(type: "bigint", nullable: true), + priority = table.Column(type: "int", nullable: true), + trigger_state = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + trigger_type = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + start_time = table.Column(type: "bigint", nullable: false), + end_time = table.Column(type: "bigint", nullable: true), + calendar_name = table.Column(type: "varchar(200)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + misfire_instr = table.Column(type: "int", nullable: true), + job_data = table.Column(type: "longblob", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_triggers", x => new { x.sched_name, x.trigger_name, x.trigger_group }); + table.ForeignKey( + name: "FK_QRTZ_triggers_QRTZ_job_details_sched_name_job_name_job_group", + columns: x => new { x.sched_name, x.job_name, x.job_group }, + principalTable: "QRTZ_job_details", + principalColumns: new[] { "sched_name", "job_name", "job_group" }, + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "QRTZ_blob_triggers", + columns: table => new + { + sched_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + trigger_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + trigger_group = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + blob_data = table.Column(type: "longblob", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_blob_triggers", x => new { x.sched_name, x.trigger_name, x.trigger_group }); + table.ForeignKey( + name: "FK_QRTZ_blob_triggers_QRTZ_triggers_sched_name_trigger_name_tri~", + columns: x => new { x.sched_name, x.trigger_name, x.trigger_group }, + principalTable: "QRTZ_triggers", + principalColumns: new[] { "sched_name", "trigger_name", "trigger_group" }, + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "QRTZ_cron_triggers", + columns: table => new + { + sched_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + trigger_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + trigger_group = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + cron_expression = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + time_zone_id = table.Column(type: "varchar(200)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_cron_triggers", x => new { x.sched_name, x.trigger_name, x.trigger_group }); + table.ForeignKey( + name: "FK_QRTZ_cron_triggers_QRTZ_triggers_sched_name_trigger_name_tri~", + columns: x => new { x.sched_name, x.trigger_name, x.trigger_group }, + principalTable: "QRTZ_triggers", + principalColumns: new[] { "sched_name", "trigger_name", "trigger_group" }, + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "QRTZ_simple_triggers", + columns: table => new + { + sched_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + trigger_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + trigger_group = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + repeat_count = table.Column(type: "bigint", nullable: false), + repeat_interval = table.Column(type: "bigint", nullable: false), + times_triggered = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_simple_triggers", x => new { x.sched_name, x.trigger_name, x.trigger_group }); + table.ForeignKey( + name: "FK_QRTZ_simple_triggers_QRTZ_triggers_sched_name_trigger_name_t~", + columns: x => new { x.sched_name, x.trigger_name, x.trigger_group }, + principalTable: "QRTZ_triggers", + principalColumns: new[] { "sched_name", "trigger_name", "trigger_group" }, + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "QRTZ_simprop_triggers", + columns: table => new + { + sched_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + trigger_name = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + trigger_group = table.Column(type: "varchar(200)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + str_prop_1 = table.Column(type: "varchar(200)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + str_prop_2 = table.Column(type: "varchar(200)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + str_prop_3 = table.Column(type: "varchar(200)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + int_prop_1 = table.Column(type: "int", nullable: true), + int_prop_2 = table.Column(type: "int", nullable: true), + long_prop_1 = table.Column(type: "bigint", nullable: true), + long_prop_2 = table.Column(type: "bigint", nullable: true), + dec_prop_1 = table.Column(type: "numeric(65,30)", nullable: true), + dec_prop_2 = table.Column(type: "numeric(65,30)", nullable: true), + bool_prop_1 = table.Column(type: "tinyint(1)", nullable: true), + bool_prop_2 = table.Column(type: "tinyint(1)", nullable: true), + time_zone_id = table.Column(type: "varchar(200)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_QRTZ_simprop_triggers", x => new { x.sched_name, x.trigger_name, x.trigger_group }); + table.ForeignKey( + name: "FK_QRTZ_simprop_triggers_QRTZ_triggers_sched_name_trigger_name_~", + columns: x => new { x.sched_name, x.trigger_name, x.trigger_group }, + principalTable: "QRTZ_triggers", + principalColumns: new[] { "sched_name", "trigger_name", "trigger_group" }, + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_Subscriptions_RetryPolicyId", + table: "Subscriptions", + column: "RetryPolicyId"); + + migrationBuilder.CreateIndex( + name: "IX_DelayedRetries_On", + table: "DelayedRetries", + column: "On"); + + migrationBuilder.CreateIndex( + name: "idx_je_fire_instance_id", + table: "job_executions", + column: "fire_instance_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "idx_je_group_name_start", + table: "job_executions", + columns: new[] { "job_group", "job_name", "start_time_utc" }); + + migrationBuilder.CreateIndex( + name: "idx_je_start_time", + table: "job_executions", + column: "start_time_utc"); + + migrationBuilder.CreateIndex( + name: "idx_je_success", + table: "job_executions", + column: "success"); + + migrationBuilder.CreateIndex( + name: "idx_QRTZ_ft_job_group", + table: "QRTZ_fired_triggers", + column: "job_group"); + + migrationBuilder.CreateIndex( + name: "idx_QRTZ_ft_job_name", + table: "QRTZ_fired_triggers", + column: "job_name"); + + migrationBuilder.CreateIndex( + name: "idx_QRTZ_ft_job_req_recovery", + table: "QRTZ_fired_triggers", + column: "requests_recovery"); + + migrationBuilder.CreateIndex( + name: "idx_QRTZ_ft_trig_group", + table: "QRTZ_fired_triggers", + column: "trigger_group"); + + migrationBuilder.CreateIndex( + name: "idx_QRTZ_ft_trig_inst_name", + table: "QRTZ_fired_triggers", + column: "instance_name"); + + migrationBuilder.CreateIndex( + name: "idx_QRTZ_ft_trig_name", + table: "QRTZ_fired_triggers", + column: "trigger_name"); + + migrationBuilder.CreateIndex( + name: "idx_QRTZ_ft_trig_nm_gp", + table: "QRTZ_fired_triggers", + columns: new[] { "sched_name", "trigger_name", "trigger_group" }); + + migrationBuilder.CreateIndex( + name: "idx_j_req_recovery", + table: "QRTZ_job_details", + column: "requests_recovery"); + + migrationBuilder.CreateIndex( + name: "idx_t_next_fire_time", + table: "QRTZ_triggers", + column: "next_fire_time"); + + migrationBuilder.CreateIndex( + name: "idx_t_nft_st", + table: "QRTZ_triggers", + columns: new[] { "next_fire_time", "trigger_state" }); + + migrationBuilder.CreateIndex( + name: "idx_t_state", + table: "QRTZ_triggers", + column: "trigger_state"); + + migrationBuilder.CreateIndex( + name: "IX_QRTZ_triggers_sched_name_job_name_job_group", + table: "QRTZ_triggers", + columns: new[] { "sched_name", "job_name", "job_group" }); + + migrationBuilder.AddForeignKey( + name: "FK_Subscriptions_RetryPolicies_RetryPolicyId", + table: "Subscriptions", + column: "RetryPolicyId", + principalTable: "RetryPolicies", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Subscriptions_RetryPolicies_RetryPolicyId", + table: "Subscriptions"); + + migrationBuilder.DropTable( + name: "DelayedRetries"); + + migrationBuilder.DropTable( + name: "job_executions"); + + migrationBuilder.DropTable( + name: "QRTZ_blob_triggers"); + + migrationBuilder.DropTable( + name: "QRTZ_calendars"); + + migrationBuilder.DropTable( + name: "QRTZ_cron_triggers"); + + migrationBuilder.DropTable( + name: "QRTZ_fired_triggers"); + + migrationBuilder.DropTable( + name: "QRTZ_locks"); + + migrationBuilder.DropTable( + name: "QRTZ_paused_trigger_grps"); + + migrationBuilder.DropTable( + name: "QRTZ_scheduler_state"); + + migrationBuilder.DropTable( + name: "QRTZ_simple_triggers"); + + migrationBuilder.DropTable( + name: "QRTZ_simprop_triggers"); + + migrationBuilder.DropTable( + name: "RetryPolicies"); + + migrationBuilder.DropTable( + name: "QRTZ_triggers"); + + migrationBuilder.DropTable( + name: "QRTZ_job_details"); + + migrationBuilder.DropIndex( + name: "IX_Subscriptions_RetryPolicyId", + table: "Subscriptions"); + + migrationBuilder.DropColumn( + name: "GroupAttemptCounts", + table: "Xchanges"); + + migrationBuilder.DropColumn( + name: "CustomRetryPolicy", + table: "Subscriptions"); + + migrationBuilder.DropColumn( + name: "RetryPolicyId", + table: "Subscriptions"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260707135024_DropRetryPolicyUpdatedFields.Designer.cs b/SW.Bitween.MySql/Migrations/20260707135024_DropRetryPolicyUpdatedFields.Designer.cs new file mode 100644 index 00000000..d14d992f --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260707135024_DropRetryPolicyUpdatedFields.Designer.cs @@ -0,0 +1,1886 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SW.Bitween.MySql; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260707135024_DropRetryPolicyUpdatedFields")] + partial class DropRetryPolicyUpdatedFields + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("EmailProvider") + .HasColumnType("tinyint unsigned"); + + b.Property("LoginMethods") + .HasColumnType("tinyint unsigned"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("varchar(20)"); + + b.Property("Role") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Accounts", (string)null); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AccountId") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("LoginMethod") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("RefreshTokens", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("GroupAttemptCounts") + .HasColumnType("longtext"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("BusEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("tinyint(1)"); + + b.Property("DocumentFormat") + .HasColumnType("int"); + + b.Property("DuplicateInterval") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("PromotedProperties") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BusMessageTypeName") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Documents", (string)null); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("UrlName") + .IsUnique(); + + b.ToTable("ApiGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("int"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("ApiGatewayPartners", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("BusGateways", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BusGatewayId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("BusGatewayRoutes", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Values") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GlobalAdapterValuesSets", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RunOnBadResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnFailedResult") + .HasColumnType("tinyint(1)"); + + b.Property("RunOnSubscriptions") + .HasColumnType("longtext"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Notifiers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("tinyint(1)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("OnHoldXchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AdapterProperties") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Partners", (string)null); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationForId") + .HasColumnType("int"); + + b.Property("AggregationTarget") + .HasColumnType("tinyint unsigned"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ConsecutiveFailures") + .HasColumnType("int"); + + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + + b.Property("DocumentFilter") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("Inactive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.Property("LastException") + .HasColumnType("longtext"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("MatchExpression") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("PausedOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiveOn") + .HasColumnType("datetime(6)"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ReceiverProperties") + .HasColumnType("longtext"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryPolicyId") + .HasColumnType("int"); + + b.Property("Temporary") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ValidatorProperties") + .HasColumnType("longtext"); + + b.Property("WorkGroupId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregationForId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("PartnerId"); + + b.HasIndex("ResponseSubscriptionId"); + + b.HasIndex("RetryPolicyId"); + + b.HasIndex("WorkGroupId"); + + b.ToTable("Subscriptions", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("varchar(255)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SubscriptionCategory"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("StateAfter") + .HasColumnType("longtext"); + + b.Property("StateBefore") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("SubscriptionTrail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Options") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("int"); + + b.Property("GroupAttemptCounts") + .HasColumnType("longtext"); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("HandlerProperties") + .HasColumnType("longtext"); + + b.Property("InputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("InputSize") + .HasColumnType("int"); + + b.Property("MapperId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("MapperProperties") + .HasColumnType("longtext"); + + b.Property("PartnerId") + .HasColumnType("int"); + + b.Property("References") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("int"); + + b.Property("RetryFor") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("StartedOn") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InputHash"); + + b.HasIndex("RetryFor"); + + b.HasIndex("StartedOn"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("Xchanges", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("AggregatedOn") + .HasColumnType("datetime(6)"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AggregationXchangeId"); + + b.ToTable("XchangeAggregations", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("DeliveredOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("DeliveredOn"); + + b.ToTable("XchangeDeliveries", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("NotifierId") + .HasColumnType("int"); + + b.Property("NotifierName") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("XchangeNotifications", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Hits") + .HasMaxLength(2000) + .IsUnicode(false) + .HasColumnType("varchar(2000)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("PropertiesRaw") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PropertiesRaw"); + + b.ToTable("XchangePromotedProperties", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Exception") + .HasColumnType("longtext"); + + b.Property("FinishedOn") + .HasColumnType("datetime(6)"); + + b.Property("OutputBad") + .HasColumnType("tinyint(1)"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("OutputHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OutputSize") + .HasColumnType("int"); + + b.Property("ResponseBad") + .HasColumnType("tinyint(1)"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("varchar(200)"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ResponseSize") + .HasColumnType("int"); + + b.Property("ResponseXchangeId") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("XchangeResults", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("tinyint(1)"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("varchar(500)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b1.HasKey("PartnerId", "Id"); + + b1.HasIndex("Key") + .IsUnique(); + + b1.ToTable("PartnerApiCredentials", (string)null); + + b1.WithOwner() + .HasForeignKey("PartnerId"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_AggFor"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_Subscriptions_RespSub"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("int"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("tinyint(1)"); + + b1.Property("On") + .HasColumnType("bigint"); + + b1.Property("Recurrence") + .HasColumnType("tinyint unsigned"); + + b1.HasKey("SubscriptionId", "Id"); + + b1.ToTable("SubscriptionSchedules", (string)null); + + b1.WithOwner() + .HasForeignKey("SubscriptionId"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.MySql/Migrations/20260707135024_DropRetryPolicyUpdatedFields.cs b/SW.Bitween.MySql/Migrations/20260707135024_DropRetryPolicyUpdatedFields.cs new file mode 100644 index 00000000..12a81fda --- /dev/null +++ b/SW.Bitween.MySql/Migrations/20260707135024_DropRetryPolicyUpdatedFields.cs @@ -0,0 +1,41 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.MySql.Migrations +{ + /// + public partial class DropRetryPolicyUpdatedFields : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UpdatedAt", + table: "RetryPolicies"); + + migrationBuilder.DropColumn( + name: "UpdatedBy", + table: "RetryPolicies"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UpdatedAt", + table: "RetryPolicies", + type: "datetime(6)", + nullable: false, + defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0))); + + migrationBuilder.AddColumn( + name: "UpdatedBy", + table: "RetryPolicies", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + } + } +} diff --git a/SW.Bitween.MySql/Migrations/BitweenDbContextModelSnapshot.cs b/SW.Bitween.MySql/Migrations/BitweenDbContextModelSnapshot.cs index f662d75c..ee14e415 100644 --- a/SW.Bitween.MySql/Migrations/BitweenDbContextModelSnapshot.cs +++ b/SW.Bitween.MySql/Migrations/BitweenDbContextModelSnapshot.cs @@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using SW.Bitween; +using SW.Bitween.MySql; #nullable disable @@ -123,6 +123,26 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("RefreshTokens", (string)null); }); + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("GroupAttemptCounts") + .HasColumnType("longtext"); + + b.Property("On") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("On"); + + b.ToTable("DelayedRetries", (string)null); + }); + modelBuilder.Entity("SW.Bitween.Domain.Document", b => { b.Property("Id") @@ -476,6 +496,39 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Groups") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("longtext"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.ToTable("RetryPolicies", (string)null); + }); + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => { b.Property("Id") @@ -499,6 +552,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ConsecutiveFailures") .HasColumnType("int"); + b.Property("CustomRetryPolicy") + .HasColumnType("longtext"); + b.Property("DocumentFilter") .HasColumnType("longtext"); @@ -563,6 +619,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ResponseSubscriptionId") .HasColumnType("int"); + b.Property("RetryPolicyId") + .HasColumnType("int"); + b.Property("Temporary") .HasColumnType("tinyint(1)"); @@ -592,6 +651,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ResponseSubscriptionId"); + b.HasIndex("RetryPolicyId"); + b.HasIndex("WorkGroupId"); b.ToTable("Subscriptions", (string)null); @@ -702,6 +763,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DocumentId") .HasColumnType("int"); + b.Property("GroupAttemptCounts") + .HasColumnType("longtext"); + b.Property("HandlerId") .HasMaxLength(200) .IsUnicode(false) @@ -949,6 +1013,525 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToView(null, (string)null); }); + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("longtext") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("longtext") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("tinyint(1)") + .HasColumnName("success"); + + b.HasKey("Id"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("longblob") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_blob_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("QRTZ_calendars", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_cron_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("varchar(200)") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_QRTZ_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_QRTZ_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_QRTZ_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_QRTZ_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_QRTZ_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_QRTZ_ft_trig_nm_gp"); + + b.ToTable("QRTZ_fired_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("tinyint(1)") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("tinyint(1)") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("tinyint(1)") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("tinyint(1)") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("QRTZ_job_details", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("varchar(200)") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("QRTZ_locks", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("QRTZ_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("varchar(200)") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("QRTZ_scheduler_state", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("tinyint(1)") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("int") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("int") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("varchar(200)") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("varchar(200)") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("QRTZ_simple_triggers", (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("varchar(200)") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("varchar(200)") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("varchar(200)") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("varchar(200)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("longblob") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("int") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("int") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("varchar(200)") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("QRTZ_triggers", (string)null); + }); + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => { b.HasOne("SW.Bitween.Domain.Accounts.Account", null) @@ -1107,6 +1690,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Restrict) .HasConstraintName("FK_Subscriptions_RespSub"); + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull); + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") .WithMany() .HasForeignKey("WorkGroupId"); @@ -1141,6 +1729,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Category"); + b.Navigation("RetryPolicy"); + b.Navigation("Schedules"); b.Navigation("WorkGroup"); @@ -1202,6 +1792,61 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => { b.Navigation("Partners"); @@ -1216,6 +1861,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Navigation("Subscriptions"); }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); #pragma warning restore 612, 618 } } diff --git a/SW.Bitween.MySql/SW.Bitween.MySql.csproj b/SW.Bitween.MySql/SW.Bitween.MySql.csproj index a19cf9f6..cd0fcec6 100644 --- a/SW.Bitween.MySql/SW.Bitween.MySql.csproj +++ b/SW.Bitween.MySql/SW.Bitween.MySql.csproj @@ -7,10 +7,15 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/SW.Bitween.PgSql/BitweenDbContext.cs b/SW.Bitween.PgSql/BitweenDbContext.cs index d52e93a7..b159f6ce 100644 --- a/SW.Bitween.PgSql/BitweenDbContext.cs +++ b/SW.Bitween.PgSql/BitweenDbContext.cs @@ -1,23 +1,30 @@ -using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using SW.EfCoreExtensions; using SW.Bitween.Domain; +using SW.Bitween.Model; using SW.PrimitiveTypes; using System.Linq; using System.Threading; using System.Threading.Tasks; using SW.Bitween.Domain.Accounts; using SW.Bitween.Domain.Gateway; +using SW.Scheduler.PgSql; namespace SW.Bitween.PgSql { public class BitweenDbContext : Bitween.BitweenDbContext { - //private readonly RequestContext requestContext; - //private readonly IPublish publish; - public const string Schema = "infolink"; + private static readonly JsonSerializerOptions _polymorphicOpts = new() + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver() + }; + public BitweenDbContext(DbContextOptions options, RequestContext requestContext, IPublish publish) : base( options, requestContext, publish) { @@ -346,6 +353,47 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(p => p.AccountId); b.Property(p => p.LoginMethod).HasConversion(); }); + + modelBuilder.Entity(b => + { + b.HasKey(p => p.Id); + b.Property(p => p.Id).ValueGeneratedOnAdd(); + b.Property(p => p.Name).IsRequired().HasMaxLength(200); + b.Property(p => p.Groups).HasConversion( + groups => JsonSerializer.Serialize(groups, _polymorphicOpts), + json => JsonSerializer.Deserialize>(json, _polymorphicOpts)!, + new Microsoft.EntityFrameworkCore.ChangeTracking.ValueComparer>( + (a, b) => JsonSerializer.Serialize(a, _polymorphicOpts) == JsonSerializer.Serialize(b, _polymorphicOpts), + v => JsonSerializer.Serialize(v, _polymorphicOpts).GetHashCode(), + v => JsonSerializer.Deserialize>(JsonSerializer.Serialize(v, _polymorphicOpts), _polymorphicOpts)! + ) + ); + }); + + modelBuilder.Entity(b => + { + b.HasOne(s => s.RetryPolicy).WithMany().HasForeignKey(s => s.RetryPolicyId).IsRequired(false) + .OnDelete(DeleteBehavior.SetNull); + b.Property(s => s.CustomRetryPolicy).HasConversion( + cp => cp == null ? null : JsonSerializer.Serialize(cp, _polymorphicOpts), + json => json == null ? null : JsonSerializer.Deserialize(json, _polymorphicOpts) + ); + }); + + modelBuilder.Entity(b => + { + b.HasKey(p => p.Id); + b.Property(p => p.Id).HasMaxLength(50); + b.Property(p => p.GroupAttemptCounts).HasColumnType("jsonb"); + b.HasIndex(p => p.On); + }); + + modelBuilder.Entity(b => + { + b.Property(p => p.GroupAttemptCounts).HasColumnType("jsonb"); + }); + + modelBuilder.UseSchedulerPostgreSql(Schema); } diff --git a/SW.Bitween.PgSql/BitweenDbContextFactory.cs b/SW.Bitween.PgSql/BitweenDbContextFactory.cs new file mode 100644 index 00000000..ac157810 --- /dev/null +++ b/SW.Bitween.PgSql/BitweenDbContextFactory.cs @@ -0,0 +1,31 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Npgsql; + +namespace SW.Bitween.PgSql +{ + public class BitweenDbContextFactory : IDesignTimeDbContextFactory + { + public BitweenDbContext CreateDbContext(string[] args) + { + var connStr = Environment.GetEnvironmentVariable("ConnectionStrings__BitweenDb") + ?? "Host=localhost;Port=5432;Database=bitween;Username=postgres;Password=postgres"; + + var dataSourceBuilder = new NpgsqlDataSourceBuilder(connStr); + dataSourceBuilder.EnableDynamicJson(); + var dataSource = dataSourceBuilder.Build(); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder + .UseSnakeCaseNamingConvention() + .UseNpgsql(dataSource, b => + { + b.MigrationsHistoryTable("_ef_migrations_history", BitweenDbContext.Schema); + b.MigrationsAssembly(typeof(DbType).Assembly.FullName); + }); + + return new BitweenDbContext(optionsBuilder.Options, null!, null!); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260706071017_QuartzAndAutoRetry.Designer.cs b/SW.Bitween.PgSql/Migrations/20260706071017_QuartzAndAutoRetry.Designer.cs new file mode 100644 index 00000000..6d22f1a8 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260706071017_QuartzAndAutoRetry.Designer.cs @@ -0,0 +1,2023 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260706071017_QuartzAndAutoRetry")] + partial class QuartzAndAutoRetry + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("phone"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property>("GroupAttemptCounts") + .HasColumnType("jsonb") + .HasColumnName("group_attempt_counts"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("jsonb") + .HasColumnName("promoted_properties"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.HasKey("Id") + .HasName("pk_document_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_document_trail_created_on"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_document_trail_document_id"); + + b.ToTable("document_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property>("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.Property("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property>("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedBy") + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property>("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property>("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_subscription_trail_created_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_subscription_trail_subscription_id"); + + b.ToTable("subscription_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property>("GroupAttemptCounts") + .HasColumnType("jsonb") + .HasColumnName("group_attempt_counts"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.HasIndex("PropertiesRaw") + .HasDatabaseName("ix_xchange_promoted_properties_properties_raw"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_document_trail_document_document_id"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_trail_subscription_subscription_id"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260706071017_QuartzAndAutoRetry.cs b/SW.Bitween.PgSql/Migrations/20260706071017_QuartzAndAutoRetry.cs new file mode 100644 index 00000000..c957c580 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260706071017_QuartzAndAutoRetry.cs @@ -0,0 +1,537 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class QuartzAndAutoRetry : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn>( + name: "group_attempt_counts", + schema: "infolink", + table: "xchange", + type: "jsonb", + nullable: true); + + migrationBuilder.AddColumn( + name: "custom_retry_policy", + schema: "infolink", + table: "subscription", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "retry_policy_id", + schema: "infolink", + table: "subscription", + type: "integer", + nullable: true); + + migrationBuilder.CreateTable( + name: "delayed_retry", + schema: "infolink", + columns: table => new + { + id = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + on = table.Column(type: "timestamp with time zone", nullable: false), + group_attempt_counts = table.Column>(type: "jsonb", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_delayed_retry", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "job_executions", + schema: "infolink", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + job_name = table.Column(type: "text", nullable: false), + job_group = table.Column(type: "text", nullable: false), + job_type_name = table.Column(type: "text", nullable: false), + fire_instance_id = table.Column(type: "text", nullable: false), + start_time_utc = table.Column(type: "timestamp with time zone", nullable: false), + end_time_utc = table.Column(type: "timestamp with time zone", nullable: true), + duration_ms = table.Column(type: "bigint", nullable: true), + success = table.Column(type: "boolean", nullable: true), + error = table.Column(type: "text", nullable: true), + node = table.Column(type: "text", nullable: false), + context = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_job_executions", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "qrtz_calendars", + schema: "infolink", + columns: table => new + { + sched_name = table.Column(type: "text", nullable: false), + calendar_name = table.Column(type: "text", nullable: false), + calendar = table.Column(type: "bytea", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_qrtz_calendars", x => new { x.sched_name, x.calendar_name }); + }); + + migrationBuilder.CreateTable( + name: "qrtz_fired_triggers", + schema: "infolink", + columns: table => new + { + sched_name = table.Column(type: "text", nullable: false), + entry_id = table.Column(type: "text", nullable: false), + trigger_name = table.Column(type: "text", nullable: false), + trigger_group = table.Column(type: "text", nullable: false), + instance_name = table.Column(type: "text", nullable: false), + fired_time = table.Column(type: "bigint", nullable: false), + sched_time = table.Column(type: "bigint", nullable: false), + priority = table.Column(type: "integer", nullable: false), + state = table.Column(type: "text", nullable: false), + job_name = table.Column(type: "text", nullable: true), + job_group = table.Column(type: "text", nullable: true), + is_nonconcurrent = table.Column(type: "bool", nullable: false), + requests_recovery = table.Column(type: "bool", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_qrtz_fired_triggers", x => new { x.sched_name, x.entry_id }); + }); + + migrationBuilder.CreateTable( + name: "qrtz_job_details", + schema: "infolink", + columns: table => new + { + sched_name = table.Column(type: "text", nullable: false), + job_name = table.Column(type: "text", nullable: false), + job_group = table.Column(type: "text", nullable: false), + description = table.Column(type: "text", nullable: true), + job_class_name = table.Column(type: "text", nullable: false), + is_durable = table.Column(type: "bool", nullable: false), + is_nonconcurrent = table.Column(type: "bool", nullable: false), + is_update_data = table.Column(type: "bool", nullable: false), + requests_recovery = table.Column(type: "bool", nullable: false), + job_data = table.Column(type: "bytea", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_qrtz_job_details", x => new { x.sched_name, x.job_name, x.job_group }); + }); + + migrationBuilder.CreateTable( + name: "qrtz_locks", + schema: "infolink", + columns: table => new + { + sched_name = table.Column(type: "text", nullable: false), + lock_name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_qrtz_locks", x => new { x.sched_name, x.lock_name }); + }); + + migrationBuilder.CreateTable( + name: "qrtz_paused_trigger_grps", + schema: "infolink", + columns: table => new + { + sched_name = table.Column(type: "text", nullable: false), + trigger_group = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_qrtz_paused_trigger_grps", x => new { x.sched_name, x.trigger_group }); + }); + + migrationBuilder.CreateTable( + name: "qrtz_scheduler_state", + schema: "infolink", + columns: table => new + { + sched_name = table.Column(type: "text", nullable: false), + instance_name = table.Column(type: "text", nullable: false), + last_checkin_time = table.Column(type: "bigint", nullable: false), + checkin_interval = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_qrtz_scheduler_state", x => new { x.sched_name, x.instance_name }); + }); + + migrationBuilder.CreateTable( + name: "retry_policy", + schema: "infolink", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + groups = table.Column(type: "text", nullable: true), + updated_by = table.Column(type: "text", nullable: true), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + created_on = table.Column(type: "timestamp with time zone", nullable: false), + created_by = table.Column(type: "text", nullable: true), + modified_on = table.Column(type: "timestamp with time zone", nullable: true), + modified_by = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_retry_policy", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "qrtz_triggers", + schema: "infolink", + columns: table => new + { + sched_name = table.Column(type: "text", nullable: false), + trigger_name = table.Column(type: "text", nullable: false), + trigger_group = table.Column(type: "text", nullable: false), + job_name = table.Column(type: "text", nullable: false), + job_group = table.Column(type: "text", nullable: false), + description = table.Column(type: "text", nullable: true), + next_fire_time = table.Column(type: "bigint", nullable: true), + prev_fire_time = table.Column(type: "bigint", nullable: true), + priority = table.Column(type: "integer", nullable: true), + trigger_state = table.Column(type: "text", nullable: false), + trigger_type = table.Column(type: "text", nullable: false), + start_time = table.Column(type: "bigint", nullable: false), + end_time = table.Column(type: "bigint", nullable: true), + calendar_name = table.Column(type: "text", nullable: true), + misfire_instr = table.Column(type: "integer", nullable: true), + job_data = table.Column(type: "bytea", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_qrtz_triggers", x => new { x.sched_name, x.trigger_name, x.trigger_group }); + table.ForeignKey( + name: "fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group", + columns: x => new { x.sched_name, x.job_name, x.job_group }, + principalSchema: "infolink", + principalTable: "qrtz_job_details", + principalColumns: new[] { "sched_name", "job_name", "job_group" }, + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "qrtz_blob_triggers", + schema: "infolink", + columns: table => new + { + sched_name = table.Column(type: "text", nullable: false), + trigger_name = table.Column(type: "text", nullable: false), + trigger_group = table.Column(type: "text", nullable: false), + blob_data = table.Column(type: "bytea", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_qrtz_blob_triggers", x => new { x.sched_name, x.trigger_name, x.trigger_group }); + table.ForeignKey( + name: "fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr", + columns: x => new { x.sched_name, x.trigger_name, x.trigger_group }, + principalSchema: "infolink", + principalTable: "qrtz_triggers", + principalColumns: new[] { "sched_name", "trigger_name", "trigger_group" }, + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "qrtz_cron_triggers", + schema: "infolink", + columns: table => new + { + sched_name = table.Column(type: "text", nullable: false), + trigger_name = table.Column(type: "text", nullable: false), + trigger_group = table.Column(type: "text", nullable: false), + cron_expression = table.Column(type: "text", nullable: false), + time_zone_id = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_qrtz_cron_triggers", x => new { x.sched_name, x.trigger_name, x.trigger_group }); + table.ForeignKey( + name: "fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr", + columns: x => new { x.sched_name, x.trigger_name, x.trigger_group }, + principalSchema: "infolink", + principalTable: "qrtz_triggers", + principalColumns: new[] { "sched_name", "trigger_name", "trigger_group" }, + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "qrtz_simple_triggers", + schema: "infolink", + columns: table => new + { + sched_name = table.Column(type: "text", nullable: false), + trigger_name = table.Column(type: "text", nullable: false), + trigger_group = table.Column(type: "text", nullable: false), + repeat_count = table.Column(type: "bigint", nullable: false), + repeat_interval = table.Column(type: "bigint", nullable: false), + times_triggered = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_qrtz_simple_triggers", x => new { x.sched_name, x.trigger_name, x.trigger_group }); + table.ForeignKey( + name: "fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_", + columns: x => new { x.sched_name, x.trigger_name, x.trigger_group }, + principalSchema: "infolink", + principalTable: "qrtz_triggers", + principalColumns: new[] { "sched_name", "trigger_name", "trigger_group" }, + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "qrtz_simprop_triggers", + schema: "infolink", + columns: table => new + { + sched_name = table.Column(type: "text", nullable: false), + trigger_name = table.Column(type: "text", nullable: false), + trigger_group = table.Column(type: "text", nullable: false), + str_prop_1 = table.Column(type: "text", nullable: true), + str_prop_2 = table.Column(type: "text", nullable: true), + str_prop_3 = table.Column(type: "text", nullable: true), + int_prop_1 = table.Column(type: "integer", nullable: true), + int_prop_2 = table.Column(type: "integer", nullable: true), + long_prop_1 = table.Column(type: "bigint", nullable: true), + long_prop_2 = table.Column(type: "bigint", nullable: true), + dec_prop_1 = table.Column(type: "numeric", nullable: true), + dec_prop_2 = table.Column(type: "numeric", nullable: true), + bool_prop_1 = table.Column(type: "bool", nullable: true), + bool_prop_2 = table.Column(type: "bool", nullable: true), + time_zone_id = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_qrtz_simprop_triggers", x => new { x.sched_name, x.trigger_name, x.trigger_group }); + table.ForeignKey( + name: "fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name", + columns: x => new { x.sched_name, x.trigger_name, x.trigger_group }, + principalSchema: "infolink", + principalTable: "qrtz_triggers", + principalColumns: new[] { "sched_name", "trigger_name", "trigger_group" }, + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_subscription_retry_policy_id", + schema: "infolink", + table: "subscription", + column: "retry_policy_id"); + + migrationBuilder.CreateIndex( + name: "ix_delayed_retry_on", + schema: "infolink", + table: "delayed_retry", + column: "on"); + + migrationBuilder.CreateIndex( + name: "idx_je_fire_instance_id", + schema: "infolink", + table: "job_executions", + column: "fire_instance_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "idx_je_group_name_start", + schema: "infolink", + table: "job_executions", + columns: new[] { "job_group", "job_name", "start_time_utc" }); + + migrationBuilder.CreateIndex( + name: "idx_je_start_time", + schema: "infolink", + table: "job_executions", + column: "start_time_utc"); + + migrationBuilder.CreateIndex( + name: "idx_je_success", + schema: "infolink", + table: "job_executions", + column: "success"); + + migrationBuilder.CreateIndex( + name: "idx_qrtz_ft_job_group", + schema: "infolink", + table: "qrtz_fired_triggers", + column: "job_group"); + + migrationBuilder.CreateIndex( + name: "idx_qrtz_ft_job_name", + schema: "infolink", + table: "qrtz_fired_triggers", + column: "job_name"); + + migrationBuilder.CreateIndex( + name: "idx_qrtz_ft_job_req_recovery", + schema: "infolink", + table: "qrtz_fired_triggers", + column: "requests_recovery"); + + migrationBuilder.CreateIndex( + name: "idx_qrtz_ft_trig_group", + schema: "infolink", + table: "qrtz_fired_triggers", + column: "trigger_group"); + + migrationBuilder.CreateIndex( + name: "idx_qrtz_ft_trig_inst_name", + schema: "infolink", + table: "qrtz_fired_triggers", + column: "instance_name"); + + migrationBuilder.CreateIndex( + name: "idx_qrtz_ft_trig_name", + schema: "infolink", + table: "qrtz_fired_triggers", + column: "trigger_name"); + + migrationBuilder.CreateIndex( + name: "idx_qrtz_ft_trig_nm_gp", + schema: "infolink", + table: "qrtz_fired_triggers", + columns: new[] { "sched_name", "trigger_name", "trigger_group" }); + + migrationBuilder.CreateIndex( + name: "idx_j_req_recovery", + schema: "infolink", + table: "qrtz_job_details", + column: "requests_recovery"); + + migrationBuilder.CreateIndex( + name: "idx_t_next_fire_time", + schema: "infolink", + table: "qrtz_triggers", + column: "next_fire_time"); + + migrationBuilder.CreateIndex( + name: "idx_t_nft_st", + schema: "infolink", + table: "qrtz_triggers", + columns: new[] { "next_fire_time", "trigger_state" }); + + migrationBuilder.CreateIndex( + name: "idx_t_state", + schema: "infolink", + table: "qrtz_triggers", + column: "trigger_state"); + + migrationBuilder.CreateIndex( + name: "ix_qrtz_triggers_sched_name_job_name_job_group", + schema: "infolink", + table: "qrtz_triggers", + columns: new[] { "sched_name", "job_name", "job_group" }); + + migrationBuilder.AddForeignKey( + name: "fk_subscription_retry_policy_retry_policy_id", + schema: "infolink", + table: "subscription", + column: "retry_policy_id", + principalSchema: "infolink", + principalTable: "retry_policy", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_subscription_retry_policy_retry_policy_id", + schema: "infolink", + table: "subscription"); + + migrationBuilder.DropTable( + name: "delayed_retry", + schema: "infolink"); + + migrationBuilder.DropTable( + name: "job_executions", + schema: "infolink"); + + migrationBuilder.DropTable( + name: "qrtz_blob_triggers", + schema: "infolink"); + + migrationBuilder.DropTable( + name: "qrtz_calendars", + schema: "infolink"); + + migrationBuilder.DropTable( + name: "qrtz_cron_triggers", + schema: "infolink"); + + migrationBuilder.DropTable( + name: "qrtz_fired_triggers", + schema: "infolink"); + + migrationBuilder.DropTable( + name: "qrtz_locks", + schema: "infolink"); + + migrationBuilder.DropTable( + name: "qrtz_paused_trigger_grps", + schema: "infolink"); + + migrationBuilder.DropTable( + name: "qrtz_scheduler_state", + schema: "infolink"); + + migrationBuilder.DropTable( + name: "qrtz_simple_triggers", + schema: "infolink"); + + migrationBuilder.DropTable( + name: "qrtz_simprop_triggers", + schema: "infolink"); + + migrationBuilder.DropTable( + name: "retry_policy", + schema: "infolink"); + + migrationBuilder.DropTable( + name: "qrtz_triggers", + schema: "infolink"); + + migrationBuilder.DropTable( + name: "qrtz_job_details", + schema: "infolink"); + + migrationBuilder.DropIndex( + name: "ix_subscription_retry_policy_id", + schema: "infolink", + table: "subscription"); + + migrationBuilder.DropColumn( + name: "group_attempt_counts", + schema: "infolink", + table: "xchange"); + + migrationBuilder.DropColumn( + name: "custom_retry_policy", + schema: "infolink", + table: "subscription"); + + migrationBuilder.DropColumn( + name: "retry_policy_id", + schema: "infolink", + table: "subscription"); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260707134908_DropRetryPolicyUpdatedFields.Designer.cs b/SW.Bitween.PgSql/Migrations/20260707134908_DropRetryPolicyUpdatedFields.Designer.cs new file mode 100644 index 00000000..55c76b36 --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260707134908_DropRetryPolicyUpdatedFields.Designer.cs @@ -0,0 +1,2159 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SW.Bitween.Model; +using SW.Bitween.PgSql; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + [DbContext(typeof(BitweenDbContext))] + [Migration("20260707134908_DropRetryPolicyUpdatedFields")] + partial class DropRetryPolicyUpdatedFields + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("infolink") + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Disabled") + .HasColumnType("boolean") + .HasColumnName("disabled"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display_name"); + + b.Property("Email") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("email"); + + b.Property("EmailProvider") + .HasColumnType("smallint") + .HasColumnName("email_provider"); + + b.Property("LoginMethods") + .HasColumnType("smallint") + .HasColumnName("login_methods"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Password") + .HasMaxLength(500) + .IsUnicode(false) + .HasColumnType("character varying(500)") + .HasColumnName("password"); + + b.Property("Phone") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character varying(20)") + .HasColumnName("phone"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.ToTable("Accounts", "infolink"); + + b.HasData( + new + { + Id = 9999, + CreatedOn = new DateTime(2021, 12, 31, 22, 0, 0, 0, DateTimeKind.Utc), + Deleted = false, + Disabled = false, + DisplayName = "Admin", + Email = "admin@Bitween.systems", + EmailProvider = (byte)0, + LoginMethods = (byte)2, + Password = "$SWHASH$V1$10000$VQCi48eitH4Ml5juvBMOFZrMdQwBbhuIQVXe6RR7qJdDF2bJ", + Role = 0 + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.Property("Id") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("integer") + .HasColumnName("account_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("LoginMethod") + .HasColumnType("smallint") + .HasColumnName("login_method"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_refresh_tokens_account_id"); + + b.ToTable("RefreshTokens", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property>("GroupAttemptCounts") + .HasColumnType("jsonb") + .HasColumnName("group_attempt_counts"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Document", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("BusEnabled") + .HasColumnType("boolean") + .HasColumnName("bus_enabled"); + + b.Property("BusMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("bus_message_type_name"); + + b.Property("DisregardsUnfilteredMessages") + .HasColumnType("boolean") + .HasColumnName("disregards_unfiltered_messages"); + + b.Property("DocumentFormat") + .HasColumnType("integer") + .HasColumnName("document_format"); + + b.Property("DuplicateInterval") + .HasColumnType("integer") + .HasColumnName("duplicate_interval"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PromotedProperties") + .HasColumnType("jsonb") + .HasColumnName("promoted_properties"); + + b.HasKey("Id") + .HasName("pk_document"); + + b.HasIndex("BusMessageTypeName") + .IsUnique() + .HasDatabaseName("ix_document_bus_message_type_name"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_document_name"); + + b.ToTable("document", "infolink"); + + b.HasData( + new + { + Id = 10001, + BusEnabled = false, + DocumentFormat = 0, + DuplicateInterval = 0, + Name = "Aggregation Document", + PromotedProperties = "{}" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.HasKey("Id") + .HasName("pk_document_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_document_trail_created_on"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_document_trail_document_id"); + + b.ToTable("document_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UrlName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("url_name"); + + b.HasKey("Id") + .HasName("pk_api_gateway"); + + b.HasIndex("UrlName") + .IsUnique() + .HasDatabaseName("ix_api_gateway_url_name"); + + b.ToTable("api_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.Property("ApiGatewayId") + .HasColumnType("integer") + .HasColumnName("api_gateway_id"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("ApiGatewayId", "PartnerId", "SubscriptionId") + .HasName("pk_api_gateway_partner"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_api_gateway_partner_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_api_gateway_partner_subscription_id"); + + b.ToTable("api_gateway_partner", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_bus_gateway"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_bus_gateway_document_id"); + + b.ToTable("bus_gateway", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusGatewayId") + .HasColumnType("integer") + .HasColumnName("bus_gateway_id"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_bus_gateway_route"); + + b.HasIndex("BusGatewayId") + .HasDatabaseName("ix_bus_gateway_route_bus_gateway_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_bus_gateway_route_partner_id"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_bus_gateway_route_subscription_id"); + + b.ToTable("bus_gateway_route", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.GlobalAdapterValuesSet", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property>("Values") + .HasColumnType("jsonb") + .HasColumnName("values"); + + b.HasKey("Id") + .HasName("pk_global_adapter_values_set"); + + b.ToTable("global_adapter_values_set", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Notifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("HandlerId") + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property("HandlerProperties") + .HasColumnType("text") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("RunOnBadResult") + .HasColumnType("boolean") + .HasColumnName("run_on_bad_result"); + + b.Property("RunOnFailedResult") + .HasColumnType("boolean") + .HasColumnName("run_on_failed_result"); + + b.Property("RunOnSubscriptions") + .HasColumnType("integer[]") + .HasColumnName("run_on_subscriptions"); + + b.Property("RunOnSuccessfulResult") + .HasColumnType("boolean") + .HasColumnName("run_on_successful_result"); + + b.HasKey("Id") + .HasName("pk_notifier"); + + b.ToTable("notifier", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.OnHoldXchange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BadData") + .HasColumnType("boolean") + .HasColumnName("bad_data"); + + b.Property("Data") + .HasColumnType("text") + .HasColumnName("data"); + + b.Property("FileName") + .HasColumnType("text") + .HasColumnName("file_name"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_on_hold_xchange"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_on_hold_xchange_subscription_id"); + + b.ToTable("on_hold_xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property>("AdapterProperties") + .HasColumnType("jsonb") + .HasColumnName("adapter_properties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_partner"); + + b.ToTable("partner", "infolink"); + + b.HasData( + new + { + Id = 1, + Name = "SYSTEM" + }); + }); + + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregate_on"); + + b.Property("AggregationForId") + .HasColumnType("integer") + .HasColumnName("aggregation_for_id"); + + b.Property("AggregationTarget") + .HasColumnType("smallint") + .HasColumnName("aggregation_target"); + + b.Property("CategoryId") + .HasColumnType("integer") + .HasColumnName("category_id"); + + b.Property("ConsecutiveFailures") + .HasColumnType("integer") + .HasColumnName("consecutive_failures"); + + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + + b.Property>("DocumentFilter") + .HasColumnType("jsonb") + .HasColumnName("document_filter"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("Inactive") + .HasColumnType("boolean") + .HasColumnName("inactive"); + + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.Property("LastException") + .HasColumnType("text") + .HasColumnName("last_exception"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("MatchExpression") + .HasColumnType("text") + .HasColumnName("match_expression"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("PausedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("paused_on"); + + b.Property("ReceiveOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("receive_on"); + + b.Property("ReceiverId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("receiver_id"); + + b.Property>("ReceiverProperties") + .HasColumnType("jsonb") + .HasColumnName("receiver_properties"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + + b.Property("Temporary") + .HasColumnType("boolean") + .HasColumnName("temporary"); + + b.Property("Type") + .HasColumnType("smallint") + .HasColumnName("type"); + + b.Property("ValidatorId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("validator_id"); + + b.Property>("ValidatorProperties") + .HasColumnType("jsonb") + .HasColumnName("validator_properties"); + + b.Property("WorkGroupId") + .HasColumnType("integer") + .HasColumnName("work_group_id"); + + b.HasKey("Id") + .HasName("pk_subscription"); + + b.HasIndex("AggregationForId") + .HasDatabaseName("ix_subscription_aggregation_for_id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_subscription_category_id"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_subscription_document_id"); + + b.HasIndex("PartnerId") + .HasDatabaseName("ix_subscription_partner_id"); + + b.HasIndex("ResponseSubscriptionId") + .HasDatabaseName("ix_subscription_response_subscription_id"); + + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + + b.HasIndex("WorkGroupId") + .HasDatabaseName("ix_subscription_work_group_id"); + + b.ToTable("subscription", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("text") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.HasKey("Id") + .HasName("pk_subscription_category"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_subscription_category_code"); + + b.ToTable("subscription_category", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Code") + .HasColumnType("integer") + .HasColumnName("code"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("StateAfter") + .HasColumnType("text") + .HasColumnName("state_after"); + + b.Property("StateBefore") + .HasColumnType("text") + .HasColumnName("state_before"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_subscription_trail"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_subscription_trail_created_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_subscription_trail_subscription_id"); + + b.ToTable("subscription_trail", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.WorkGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BusMessageName") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)") + .HasColumnName("bus_message_name"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Options") + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.HasKey("Id") + .HasName("pk_work_group"); + + b.ToTable("work_group", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("CorrelationId") + .HasColumnType("text") + .HasColumnName("correlation_id"); + + b.Property("DocumentId") + .HasColumnType("integer") + .HasColumnName("document_id"); + + b.Property>("GroupAttemptCounts") + .HasColumnType("jsonb") + .HasColumnName("group_attempt_counts"); + + b.Property("HandlerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("handler_id"); + + b.Property>("HandlerProperties") + .HasColumnType("jsonb") + .HasColumnName("handler_properties"); + + b.Property("InputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_content_type"); + + b.Property("InputHash") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("input_hash"); + + b.Property("InputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("input_name"); + + b.Property("InputSize") + .HasColumnType("integer") + .HasColumnName("input_size"); + + b.Property("MapperId") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("mapper_id"); + + b.Property>("MapperProperties") + .HasColumnType("jsonb") + .HasColumnName("mapper_properties"); + + b.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b.Property("References") + .HasColumnType("text[]") + .HasColumnName("references"); + + b.Property("ResponseMessageTypeName") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("response_message_type_name"); + + b.Property("ResponseSubscriptionId") + .HasColumnType("integer") + .HasColumnName("response_subscription_id"); + + b.Property("RetryFor") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("retry_for"); + + b.Property("StartedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_on"); + + b.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b.HasKey("Id") + .HasName("pk_xchange"); + + b.HasIndex("DocumentId") + .HasDatabaseName("ix_xchange_document_id"); + + b.HasIndex("InputHash") + .HasDatabaseName("ix_xchange_input_hash"); + + b.HasIndex("RetryFor") + .HasDatabaseName("ix_xchange_retry_for"); + + b.HasIndex("StartedOn") + .HasDatabaseName("ix_xchange_started_on"); + + b.HasIndex("SubscriptionId") + .HasDatabaseName("ix_xchange_subscription_id"); + + b.ToTable("xchange", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("AggregatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("aggregated_on"); + + b.Property("AggregationXchangeId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("aggregation_xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_aggregation"); + + b.HasIndex("AggregationXchangeId") + .HasDatabaseName("ix_xchange_aggregation_aggregation_xchange_id"); + + b.ToTable("xchange_aggregation", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("DeliveredOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("delivered_on"); + + b.HasKey("Id") + .HasName("pk_xchange_delivery"); + + b.HasIndex("DeliveredOn") + .HasDatabaseName("ix_xchange_delivery_delivered_on"); + + b.ToTable("xchange_delivery", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("NotifierId") + .HasColumnType("integer") + .HasColumnName("notifier_id"); + + b.Property("NotifierName") + .HasColumnType("text") + .HasColumnName("notifier_name"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.Property("XchangeId") + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("character varying(50)") + .HasColumnName("xchange_id"); + + b.HasKey("Id") + .HasName("pk_xchange_notification"); + + b.ToTable("xchange_notification", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Hits") + .HasColumnType("integer[]") + .HasColumnName("hits"); + + b.Property>("Properties") + .HasColumnType("jsonb") + .HasColumnName("properties"); + + b.Property("PropertiesRaw") + .HasColumnType("text") + .HasColumnName("properties_raw"); + + b.HasKey("Id") + .HasName("pk_xchange_promoted_properties"); + + b.HasIndex("PropertiesRaw") + .HasDatabaseName("ix_xchange_promoted_properties_properties_raw"); + + b.ToTable("xchange_promoted_properties", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property("Exception") + .HasColumnType("text") + .HasColumnName("exception"); + + b.Property("FinishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_on"); + + b.Property("OutputBad") + .HasColumnType("boolean") + .HasColumnName("output_bad"); + + b.Property("OutputContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_content_type"); + + b.Property("OutputHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("output_hash"); + + b.Property("OutputName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("output_name"); + + b.Property("OutputSize") + .HasColumnType("integer") + .HasColumnName("output_size"); + + b.Property("ResponseBad") + .HasColumnType("boolean") + .HasColumnName("response_bad"); + + b.Property("ResponseContentType") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_content_type"); + + b.Property("ResponseHash") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("response_hash"); + + b.Property("ResponseName") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("response_name"); + + b.Property("ResponseSize") + .HasColumnType("integer") + .HasColumnName("response_size"); + + b.Property("ResponseXchangeId") + .HasColumnType("text") + .HasColumnName("response_xchange_id"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_xchange_result"); + + b.ToTable("xchange_result", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.RunFlagUpdater+RunningResult", b => + { + b.Property("IsRunning") + .HasColumnType("boolean") + .HasColumnName("is_running"); + + b.ToTable("running_result", "infolink"); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => + { + b.HasOne("SW.Bitween.Domain.Accounts.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_refresh_tokens_accounts_account_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.DocumentTrail", b => + { + b.HasOne("SW.Bitween.Domain.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_document_trail_document_document_id"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGatewayPartner", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.ApiGateway", "ApiGateway") + .WithMany("Partners") + .HasForeignKey("ApiGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_api_gateway_api_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_api_gateway_partner_subscription_subscription_id"); + + b.Navigation("ApiGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGatewayRoute", b => + { + b.HasOne("SW.Bitween.Domain.Gateway.BusGateway", "BusGateway") + .WithMany("Routes") + .HasForeignKey("BusGatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_bus_gateway_bus_gateway_id"); + + b.HasOne("SW.Bitween.Domain.Partner", "Partner") + .WithMany() + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_bus_gateway_route_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bus_gateway_route_subscription_subscription_id"); + + b.Navigation("BusGateway"); + + b.Navigation("Partner"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.OwnsMany("SW.Bitween.Domain.ApiCredential", "ApiCredentials", b1 => + { + b1.Property("PartnerId") + .HasColumnType("integer") + .HasColumnName("partner_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("key"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("name"); + + b1.HasKey("PartnerId", "Id") + .HasName("pk_partner_api_credential"); + + b1.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_partner_api_credential_key"); + + b1.ToTable("partner_api_credential", "infolink"); + + b1.WithOwner() + .HasForeignKey("PartnerId") + .HasConstraintName("fk_partner_api_credential_partner_partner_id"); + + b1.HasData( + new + { + PartnerId = 1, + Id = 1, + Key = "7facc758283844b49cc4ffd26a75b1de", + Name = "default" + }); + }); + + b.Navigation("ApiCredentials"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("AggregationForId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_aggregation_for"); + + b.HasOne("SW.Bitween.Domain.SubscriptionCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_subscription_subscription_category_category_id"); + + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_subscription_document_document_id"); + + b.HasOne("SW.Bitween.Domain.Partner", null) + .WithMany("Subscriptions") + .HasForeignKey("PartnerId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_partner_partner_id"); + + b.HasOne("SW.Bitween.Domain.Subscription", null) + .WithMany() + .HasForeignKey("ResponseSubscriptionId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_subscription_response_subscriber"); + + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") + .WithMany() + .HasForeignKey("WorkGroupId") + .HasConstraintName("fk_subscription_work_group_work_group_id"); + + b.OwnsMany("SW.Bitween.Domain.Schedule", "Schedules", b1 => + { + b1.Property("SubscriptionId") + .HasColumnType("integer") + .HasColumnName("subscription_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Backwards") + .HasColumnType("boolean") + .HasColumnName("backwards"); + + b1.Property("On") + .HasColumnType("bigint") + .HasColumnName("on"); + + b1.Property("Recurrence") + .HasColumnType("smallint") + .HasColumnName("recurrence"); + + b1.HasKey("SubscriptionId", "Id") + .HasName("pk_subscription_schedule"); + + b1.ToTable("subscription_schedule", "infolink"); + + b1.WithOwner() + .HasForeignKey("SubscriptionId") + .HasConstraintName("fk_subscription_schedule_subscription_subscription_id"); + }); + + b.Navigation("Category"); + + b.Navigation("RetryPolicy"); + + b.Navigation("Schedules"); + + b.Navigation("WorkGroup"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.SubscriptionTrail", b => + { + b.HasOne("SW.Bitween.Domain.Subscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_subscription_trail_subscription_subscription_id"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Xchange", b => + { + b.HasOne("SW.Bitween.Domain.Document", null) + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_xchange_document_document_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeAggregation", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeAggregation", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_aggregation_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeDelivery", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeDelivery", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_delivery_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangePromotedProperties", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangePromotedProperties", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_promoted_properties_xchange_id"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.XchangeResult", b => + { + b.HasOne("SW.Bitween.Domain.Xchange", null) + .WithOne() + .HasForeignKey("SW.Bitween.Domain.XchangeResult", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xchange_result_xchange_id"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => + { + b.Navigation("Partners"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Gateway.BusGateway", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("SW.Bitween.Domain.Partner", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/20260707134908_DropRetryPolicyUpdatedFields.cs b/SW.Bitween.PgSql/Migrations/20260707134908_DropRetryPolicyUpdatedFields.cs new file mode 100644 index 00000000..09b883eb --- /dev/null +++ b/SW.Bitween.PgSql/Migrations/20260707134908_DropRetryPolicyUpdatedFields.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SW.Bitween.PgSql.Migrations +{ + /// + public partial class DropRetryPolicyUpdatedFields : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "updated_at", + schema: "infolink", + table: "retry_policy"); + + migrationBuilder.DropColumn( + name: "updated_by", + schema: "infolink", + table: "retry_policy"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "updated_at", + schema: "infolink", + table: "retry_policy", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0))); + + migrationBuilder.AddColumn( + name: "updated_by", + schema: "infolink", + table: "retry_policy", + type: "text", + nullable: true); + } + } +} diff --git a/SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs b/SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs index 14c8dc41..8cea92fe 100644 --- a/SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs +++ b/SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs @@ -148,6 +148,30 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("RefreshTokens", "infolink"); }); + modelBuilder.Entity("SW.Bitween.Domain.DelayedRetry", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("id"); + + b.Property>("GroupAttemptCounts") + .HasColumnType("jsonb") + .HasColumnName("group_attempt_counts"); + + b.Property("On") + .HasColumnType("timestamp with time zone") + .HasColumnName("on"); + + b.HasKey("Id") + .HasName("pk_delayed_retry"); + + b.HasIndex("On") + .HasDatabaseName("ix_delayed_retry_on"); + + b.ToTable("delayed_retry", "infolink"); + }); + modelBuilder.Entity("SW.Bitween.Domain.Document", b => { b.Property("Id") @@ -582,6 +606,47 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("SW.Bitween.Domain.RetryPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Groups") + .HasColumnType("text") + .HasColumnName("groups"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_retry_policy"); + + b.ToTable("retry_policy", "infolink"); + }); + modelBuilder.Entity("SW.Bitween.Domain.Subscription", b => { b.Property("Id") @@ -611,6 +676,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("consecutive_failures"); + b.Property("CustomRetryPolicy") + .HasColumnType("text") + .HasColumnName("custom_retry_policy"); + b.Property>("DocumentFilter") .HasColumnType("jsonb") .HasColumnName("document_filter"); @@ -689,6 +758,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("response_subscription_id"); + b.Property("RetryPolicyId") + .HasColumnType("integer") + .HasColumnName("retry_policy_id"); + b.Property("Temporary") .HasColumnType("boolean") .HasColumnName("temporary"); @@ -728,6 +801,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ResponseSubscriptionId") .HasDatabaseName("ix_subscription_response_subscription_id"); + b.HasIndex("RetryPolicyId") + .HasDatabaseName("ix_subscription_retry_policy_id"); + b.HasIndex("WorkGroupId") .HasDatabaseName("ix_subscription_work_group_id"); @@ -865,6 +941,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("document_id"); + b.Property>("GroupAttemptCounts") + .HasColumnType("jsonb") + .HasColumnName("group_attempt_counts"); + b.Property("HandlerId") .HasMaxLength(200) .HasColumnType("character varying(200)") @@ -1155,6 +1235,538 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToView(null, (string)null); }); + modelBuilder.Entity("SW.Scheduler.JobExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Context") + .HasColumnType("text") + .HasColumnName("context"); + + b.Property("DurationMs") + .HasColumnType("bigint") + .HasColumnName("duration_ms"); + + b.Property("EndTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time_utc"); + + b.Property("Error") + .HasColumnType("text") + .HasColumnName("error"); + + b.Property("FireInstanceId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("fire_instance_id"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobTypeName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_type_name"); + + b.Property("Node") + .IsRequired() + .HasColumnType("text") + .HasColumnName("node"); + + b.Property("StartTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time_utc"); + + b.Property("Success") + .HasColumnType("boolean") + .HasColumnName("success"); + + b.HasKey("Id") + .HasName("pk_job_executions"); + + b.HasIndex("FireInstanceId") + .IsUnique() + .HasDatabaseName("idx_je_fire_instance_id"); + + b.HasIndex("StartTimeUtc") + .HasDatabaseName("idx_je_start_time"); + + b.HasIndex("Success") + .HasDatabaseName("idx_je_success"); + + b.HasIndex("JobGroup", "JobName", "StartTimeUtc") + .HasDatabaseName("idx_je_group_name_start"); + + b.ToTable("job_executions", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_blob_triggers"); + + b.ToTable("qrtz_blob_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName") + .HasName("pk_qrtz_calendars"); + + b.ToTable("qrtz_calendars", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_cron_triggers"); + + b.ToTable("qrtz_cron_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId") + .HasName("pk_qrtz_fired_triggers"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup") + .HasName("pk_qrtz_job_details"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_j_req_recovery"); + + b.ToTable("qrtz_job_details", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName") + .HasName("pk_qrtz_locks"); + + b.ToTable("qrtz_locks", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup") + .HasName("pk_qrtz_paused_trigger_grps"); + + b.ToTable("qrtz_paused_trigger_grps", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName") + .HasName("pk_qrtz_scheduler_state"); + + b.ToTable("qrtz_scheduler_state", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simprop_triggers"); + + b.ToTable("qrtz_simprop_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_simple_triggers"); + + b.ToTable("qrtz_simple_triggers", "infolink"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("integer") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup") + .HasName("pk_qrtz_triggers"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup") + .HasDatabaseName("ix_qrtz_triggers_sched_name_job_name_job_group"); + + b.ToTable("qrtz_triggers", "infolink"); + }); + modelBuilder.Entity("SW.Bitween.Domain.Accounts.RefreshToken", b => { b.HasOne("SW.Bitween.Domain.Accounts.Account", null) @@ -1331,6 +1943,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Restrict) .HasConstraintName("fk_subscription_response_subscriber"); + b.HasOne("SW.Bitween.Domain.RetryPolicy", "RetryPolicy") + .WithMany() + .HasForeignKey("RetryPolicyId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_subscription_retry_policy_retry_policy_id"); + b.HasOne("SW.Bitween.Domain.WorkGroup", "WorkGroup") .WithMany() .HasForeignKey("WorkGroupId") @@ -1373,6 +1991,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Category"); + b.Navigation("RetryPolicy"); + b.Navigation("Schedules"); b.Navigation("WorkGroup"); @@ -1440,6 +2060,66 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasConstraintName("fk_xchange_result_xchange_id"); }); + modelBuilder.Entity("SW.Scheduler.QuartzBlobTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_blob_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzCronTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_cron_triggers_qrtz_triggers_sched_name_trigger_name_tr"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimplePropertyTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simprop_triggers_qrtz_triggers_sched_name_trigger_name"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzSimpleTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_simple_triggers_qrtz_triggers_sched_name_trigger_name_"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.HasOne("SW.Scheduler.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qrtz_triggers_qrtz_job_details_sched_name_job_name_job_group"); + + b.Navigation("JobDetail"); + }); + modelBuilder.Entity("SW.Bitween.Domain.Gateway.ApiGateway", b => { b.Navigation("Partners"); @@ -1454,6 +2134,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Navigation("Subscriptions"); }); + + modelBuilder.Entity("SW.Scheduler.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("SW.Scheduler.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); #pragma warning restore 612, 618 } } diff --git a/SW.Bitween.PgSql/SW.Bitween.PgSql.csproj b/SW.Bitween.PgSql/SW.Bitween.PgSql.csproj index 37dc5a6a..9baf28f1 100644 --- a/SW.Bitween.PgSql/SW.Bitween.PgSql.csproj +++ b/SW.Bitween.PgSql/SW.Bitween.PgSql.csproj @@ -8,12 +8,16 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/SW.Bitween.SampleConfigurableAdapter/Handler.cs b/SW.Bitween.SampleConfigurableAdapter/Handler.cs new file mode 100644 index 00000000..9967a624 --- /dev/null +++ b/SW.Bitween.SampleConfigurableAdapter/Handler.cs @@ -0,0 +1,34 @@ +using SW.PrimitiveTypes; +using SW.Serverless.Sdk; +using System; +using System.Threading.Tasks; + +namespace SW.Bitween.SampleConfigurableAdapter +{ + class Handler : IInfolinkHandler + { + public Handler() + { + Runner.Expect("DelayMs", "0"); + Runner.Expect("SimulateError", "false"); + Runner.Expect("ErrorMessage", "Simulated error"); + Runner.Expect("OutputData", ""); + } + + public async Task Handle(XchangeFile xchangeFile) + { + var delayMs = Runner.StartupValueOf("DelayMs"); + if (delayMs > 0) + await Task.Delay(delayMs); + + if (Runner.StartupValueOf("SimulateError")) + throw new InvalidOperationException(Runner.StartupValueOf("ErrorMessage")); + + var outputData = Runner.StartupValueOf("OutputData"); + if (!string.IsNullOrEmpty(outputData)) + return new XchangeFile(outputData, xchangeFile.Filename); + + return xchangeFile; + } + } +} diff --git a/SW.Bitween.SampleConfigurableAdapter/Program.cs b/SW.Bitween.SampleConfigurableAdapter/Program.cs new file mode 100644 index 00000000..8721aba4 --- /dev/null +++ b/SW.Bitween.SampleConfigurableAdapter/Program.cs @@ -0,0 +1,10 @@ +using SW.Serverless.Sdk; +using System.Threading.Tasks; + +namespace SW.Bitween.SampleConfigurableAdapter +{ + class Program + { + async static Task Main(string[] args) => await Runner.Run(new Handler()); + } +} diff --git a/SW.Bitween.SampleConfigurableAdapter/SW.Bitween.SampleConfigurableAdapter.csproj b/SW.Bitween.SampleConfigurableAdapter/SW.Bitween.SampleConfigurableAdapter.csproj new file mode 100644 index 00000000..e88ef705 --- /dev/null +++ b/SW.Bitween.SampleConfigurableAdapter/SW.Bitween.SampleConfigurableAdapter.csproj @@ -0,0 +1,10 @@ + + + Exe + net8.0 + SW.Bitween.SampleConfigurableAdapter + + + + + diff --git a/SW.Bitween.Sdk/JsonConverters/DelayStrategyJsonConverter.cs b/SW.Bitween.Sdk/JsonConverters/DelayStrategyJsonConverter.cs new file mode 100644 index 00000000..d8a090c0 --- /dev/null +++ b/SW.Bitween.Sdk/JsonConverters/DelayStrategyJsonConverter.cs @@ -0,0 +1,84 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SW.Bitween.Model; + +namespace SW.Bitween.JsonConverters; + +public class DelayStrategyJsonConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, DelayStrategy value, JsonSerializer serializer) + { + writer.WriteStartObject(); + + switch (value) + { + case FixedDelayStrategy fixedDelay: + writer.WritePropertyName("type"); + writer.WriteValue("fixed"); + writer.WritePropertyName("delayMs"); + writer.WriteValue(fixedDelay.DelayMs); + break; + + case LinearDelayStrategy linear: + writer.WritePropertyName("type"); + writer.WriteValue("linear"); + writer.WritePropertyName("initialDelayMs"); + writer.WriteValue(linear.InitialDelayMs); + writer.WritePropertyName("incrementMs"); + writer.WriteValue(linear.IncrementMs); + break; + + case ExponentialDelayStrategy exponential: + writer.WritePropertyName("type"); + writer.WriteValue("exponential"); + writer.WritePropertyName("initialDelayMs"); + writer.WriteValue(exponential.InitialDelayMs); + writer.WritePropertyName("multiplier"); + writer.WriteValue(exponential.Multiplier); + writer.WritePropertyName("maxDelayMs"); + writer.WriteValue(exponential.MaxDelayMs); + break; + + default: + throw new JsonSerializationException($"Unknown DelayStrategy type '{value.GetType()}'"); + } + + writer.WriteEndObject(); + } + + public override DelayStrategy ReadJson(JsonReader reader, Type objectType, DelayStrategy existingValue, + bool hasExistingValue, JsonSerializer serializer) + { + var jObject = serializer.Deserialize(reader); + if (jObject is null) return null; + + var type = jObject.Property("type")?.Value?.ToString(); + switch (type) + { + case "fixed": + return new FixedDelayStrategy + { + DelayMs = jObject.Property("delayMs")?.Value?.ToObject() ?? 0 + }; + + case "linear": + return new LinearDelayStrategy + { + InitialDelayMs = jObject.Property("initialDelayMs")?.Value?.ToObject() ?? 0, + IncrementMs = jObject.Property("incrementMs")?.Value?.ToObject() ?? 0 + }; + + case "exponential": + return new ExponentialDelayStrategy + { + InitialDelayMs = jObject.Property("initialDelayMs")?.Value?.ToObject() ?? 0, + Multiplier = jObject.Property("multiplier")?.Value?.ToObject() ?? 2.0, + MaxDelayMs = jObject.Property("maxDelayMs")?.Value?.ToObject() ?? 30_000 + }; + + default: + throw new JsonSerializationException($"Unknown or missing DelayStrategy discriminator 'type': '{type}'"); + } + } +} diff --git a/SW.Bitween.Sdk/JsonConverters/MatcherJsonConverter.cs b/SW.Bitween.Sdk/JsonConverters/MatcherJsonConverter.cs new file mode 100644 index 00000000..a16c515e --- /dev/null +++ b/SW.Bitween.Sdk/JsonConverters/MatcherJsonConverter.cs @@ -0,0 +1,103 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SW.Bitween.Model; + +namespace SW.Bitween.JsonConverters; + +public class MatcherJsonConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, Matcher value, JsonSerializer serializer) + { + writer.WriteStartObject(); + + switch (value) + { + case ContainsMatcher contains: + writer.WritePropertyName("type"); + writer.WriteValue("contains"); + writer.WritePropertyName("value"); + writer.WriteValue(contains.Value); + writer.WritePropertyName("caseSensitive"); + writer.WriteValue(contains.CaseSensitive); + break; + + case RegexMatcher regex: + writer.WritePropertyName("type"); + writer.WriteValue("regex"); + writer.WritePropertyName("pattern"); + writer.WriteValue(regex.Pattern); + writer.WritePropertyName("flags"); + writer.WriteValue(regex.Flags); + break; + + case ExceptionTypeMatcher exceptionType: + writer.WritePropertyName("type"); + writer.WriteValue("exceptionType"); + writer.WritePropertyName("value"); + writer.WriteValue(exceptionType.Value); + writer.WritePropertyName("includeInner"); + writer.WriteValue(exceptionType.IncludeInner); + break; + + case JsonPathMatcher jsonPath: + writer.WritePropertyName("type"); + writer.WriteValue("jsonPath"); + writer.WritePropertyName("path"); + writer.WriteValue(jsonPath.Path); + writer.WritePropertyName("op"); + writer.WriteValue(jsonPath.Op.ToString()); + writer.WritePropertyName("value"); + writer.WriteValue(jsonPath.Value); + break; + + default: + throw new JsonSerializationException($"Unknown Matcher type '{value.GetType()}'"); + } + + writer.WriteEndObject(); + } + + public override Matcher ReadJson(JsonReader reader, Type objectType, Matcher existingValue, + bool hasExistingValue, JsonSerializer serializer) + { + var jObject = serializer.Deserialize(reader); + if (jObject is null) return null; + + var type = jObject.Property("type")?.Value?.ToString(); + switch (type) + { + case "contains": + return new ContainsMatcher + { + Value = jObject.Property("value")?.Value?.ToString(), + CaseSensitive = jObject.Property("caseSensitive")?.Value?.ToObject() ?? false + }; + + case "regex": + return new RegexMatcher + { + Pattern = jObject.Property("pattern")?.Value?.ToString(), + Flags = jObject.Property("flags")?.Value?.ToString() ?? "i" + }; + + case "exceptionType": + return new ExceptionTypeMatcher + { + Value = jObject.Property("value")?.Value?.ToString(), + IncludeInner = jObject.Property("includeInner")?.Value?.ToObject() ?? true + }; + + case "jsonPath": + return new JsonPathMatcher + { + Path = jObject.Property("path")?.Value?.ToString(), + Op = Enum.Parse(jObject.Property("op")?.Value?.ToString() ?? nameof(JsonPathOp.Eq)), + Value = jObject.Property("value")?.Value?.ToString() + }; + + default: + throw new JsonSerializationException($"Unknown or missing Matcher discriminator 'type': '{type}'"); + } + } +} diff --git a/SW.Bitween.Sdk/Model/AutoRetry/DelayStrategy.cs b/SW.Bitween.Sdk/Model/AutoRetry/DelayStrategy.cs new file mode 100644 index 00000000..682c5bd5 --- /dev/null +++ b/SW.Bitween.Sdk/Model/AutoRetry/DelayStrategy.cs @@ -0,0 +1,77 @@ +using System; +using System.Text.Json.Serialization; + +namespace SW.Bitween.Model; + +/// +/// Calculates the delay before each successive retry attempt. +/// Concrete implementations are serialised polymorphically via System.Text.Json +/// using the "type" discriminator property. +/// +[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] +[JsonDerivedType(typeof(FixedDelayStrategy), typeDiscriminator: "fixed")] +[JsonDerivedType(typeof(LinearDelayStrategy), typeDiscriminator: "linear")] +[JsonDerivedType(typeof(ExponentialDelayStrategy), typeDiscriminator: "exponential")] +public abstract class DelayStrategy +{ + /// + /// Returns the wait duration before the next retry. + /// + /// + /// Zero-based index: 0 = delay before the first retry, + /// 1 = delay before the second, and so on. + /// + public abstract TimeSpan GetDelay(int attemptIndex); +} + +/// +/// Waits the same fixed duration before every retry attempt. +/// +public class FixedDelayStrategy : DelayStrategy +{ + /// Wait time in milliseconds for every attempt. + public int DelayMs { get; init; } + + /// + public override TimeSpan GetDelay(int _) => TimeSpan.FromMilliseconds(DelayMs); +} + +/// +/// Increases the wait by a fixed increment on each attempt: +/// Initial, Initial + Increment, Initial + 2×Increment, … +/// +public class LinearDelayStrategy : DelayStrategy +{ + /// Wait time in milliseconds before the first retry. + public int InitialDelayMs { get; init; } + + /// Additional milliseconds added for each successive attempt. + public int IncrementMs { get; init; } + + /// + public override TimeSpan GetDelay(int attemptIndex) => + TimeSpan.FromMilliseconds(InitialDelayMs + (long)attemptIndex * IncrementMs); +} + +/// +/// Multiplies the delay on each attempt (default ×2), capped at . +/// Formula: min(Initial × Multiplier^attemptIndex, MaxDelay). +/// +public class ExponentialDelayStrategy : DelayStrategy +{ + /// Wait time in milliseconds before the first retry. + public int InitialDelayMs { get; init; } + + /// Growth factor applied on every attempt. Defaults to 2.0 (doubling). + public double Multiplier { get; init; } = 2.0; + + /// Upper bound on the computed delay in milliseconds. Defaults to 30 seconds. + public int MaxDelayMs { get; init; } = 30_000; + + /// + public override TimeSpan GetDelay(int attemptIndex) + { + var ms = InitialDelayMs * Math.Pow(Multiplier, attemptIndex); + return TimeSpan.FromMilliseconds(Math.Min(ms, MaxDelayMs)); + } +} diff --git a/SW.Bitween.Sdk/Model/AutoRetry/IRetryPolicy.cs b/SW.Bitween.Sdk/Model/AutoRetry/IRetryPolicy.cs new file mode 100644 index 00000000..ea8eb55a --- /dev/null +++ b/SW.Bitween.Sdk/Model/AutoRetry/IRetryPolicy.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; + +namespace SW.Bitween.Model; + +/// +/// Common contract for both named templates and inline +/// objects stored directly on a subscription. +/// +public interface IRetryPolicy +{ + /// Ordered set of groups evaluated against a failed xchange. + List Groups { get; } +} + +/// +/// An inline retry policy defined directly on a subscription rather than referencing a +/// shared named template. Serialised as JSONB in the subscription table. +/// +public class CustomRetryPolicy : IRetryPolicy +{ + /// + public List Groups { get; set; } = []; +} + +/// Outcome type of a completed xchange execution. +public enum XchangeResultType +{ + /// Handler returned a successful response. Never retried. + Success, + + /// An unhandled exception was thrown. Content is a stack-trace string. + Error, + + /// Handler completed but the response payload failed business validation. + BadResult, +} + +/// +/// Intended scope of a retry policy (informational; not enforced by the evaluator). +/// +public enum PolicyScope +{ + Global, + Integration, +} + +/// What the evaluator should do when a group matches a failure. +public enum RetryAction +{ + /// Schedule a retry according to the group's . + Allow, + + /// Hard-block this error — no retry even if a budget would otherwise allow it. + Block, +} diff --git a/SW.Bitween.Sdk/Model/AutoRetry/Matcher.cs b/SW.Bitween.Sdk/Model/AutoRetry/Matcher.cs new file mode 100644 index 00000000..4b6f6146 --- /dev/null +++ b/SW.Bitween.Sdk/Model/AutoRetry/Matcher.cs @@ -0,0 +1,228 @@ +using System; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +namespace SW.Bitween.Model; + +/// +/// Comparison operator used by . +/// +public enum JsonPathOp +{ + /// Exact, case-insensitive equality. + Eq, + + /// Not equal (case-insensitive). + Neq, + + /// The actual value contains the expected substring (case-insensitive). + Contains, + + /// The actual value matches the expected regular expression (case-insensitive). + Regex, + + /// The JSON node exists at the path (any value, including null). + Exists, + + /// The JSON node does not exist at the path. + NotExists, +} + +/// +/// Tests raw failure content and returns whether it matches a specific pattern. +/// +/// +/// For groups the content is the exception stack-trace text. +/// For groups the content is the raw JSON response string. +/// Matcher implementations are serialised polymorphically via System.Text.Json. +/// +[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] +[JsonDerivedType(typeof(ContainsMatcher), typeDiscriminator: "contains")] +[JsonDerivedType(typeof(RegexMatcher), typeDiscriminator: "regex")] +[JsonDerivedType(typeof(ExceptionTypeMatcher), typeDiscriminator: "exceptionType")] +[JsonDerivedType(typeof(JsonPathMatcher), typeDiscriminator: "jsonPath")] +public abstract class Matcher +{ + /// The result type this matcher operates on. + public abstract XchangeResultType ResultType { get; } + + /// + /// Returns true when satisfies this matcher's condition. + /// Implementations must never throw — malformed input should return false. + /// + public abstract bool IsMatch(string content); +} + +// ── Error matchers ──────────────────────────────────────────────────────────── + +/// +/// Matches when the exception text contains a literal substring. +/// Applies to content. +/// +public class ContainsMatcher : Matcher +{ + /// + public override XchangeResultType ResultType => XchangeResultType.Error; + + /// The substring to search for. + public required string Value { get; init; } + + /// When true the comparison is case-sensitive. Defaults to false. + public bool CaseSensitive { get; init; } = false; + + /// + public override bool IsMatch(string content) => + content.Contains(Value, + CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); +} + +/// +/// Matches when the exception text satisfies a regular expression. +/// Applies to content. +/// +public class RegexMatcher : Matcher +{ + /// + public override XchangeResultType ResultType => XchangeResultType.Error; + + /// .NET-compatible regular expression pattern. + public required string Pattern { get; init; } + + /// + /// Modifier flags. Supported: "i" (case-insensitive). Defaults to "i". + /// Pass an empty string for case-sensitive matching. + /// + public string Flags { get; init; } = "i"; + + private Regex? _compiled; + + private Regex Compiled => _compiled ??= new Regex( + Pattern, + Flags.Contains('i') ? RegexOptions.IgnoreCase : RegexOptions.None, + matchTimeout: TimeSpan.FromMilliseconds(200)); + + /// + public override bool IsMatch(string content) => Compiled.IsMatch(content); +} + +/// +/// Matches when the exception text mentions a specific .NET exception type name, +/// scanning the entire stack-trace including inner exceptions. +/// Applies to content. +/// +/// +/// Value = "System.TimeoutException" fires on any stack trace that contains that +/// fully-qualified type name. +/// +public class ExceptionTypeMatcher : Matcher +{ + /// + public override XchangeResultType ResultType => XchangeResultType.Error; + + /// + /// Fully-qualified or short exception type name, e.g. "System.TimeoutException" + /// or "SqlException". The regex extraction captures segments matching + /// ([\w\.]+Exception). + /// + public required string Value { get; init; } + + /// + /// When true (default) the full stack trace — including inner exceptions — is + /// scanned. When false only the first type name in the text is checked. + /// + public bool IncludeInner { get; init; } = true; + + private static readonly Regex TypePattern = + new(@"([\w\.]+Exception)", RegexOptions.Compiled); + + /// + public override bool IsMatch(string content) + { + foreach (Match m in TypePattern.Matches(content)) + { + if (m.Value.Equals(Value, StringComparison.OrdinalIgnoreCase)) return true; + if (!IncludeInner) break; + } + return false; + } +} + +// ── BadResult matcher ───────────────────────────────────────────────────────── + +/// +/// Evaluates a JSONPath expression against a bad-result payload. +/// Applies to content. +/// +/// +/// Supports dot-notation paths and array indexers, e.g. $.error.code and +/// $.lines[0].status. Invalid JSON or a missing path returns false without +/// throwing. For production use consider replacing ResolvePath with +/// JsonPath.Net or Newtonsoft's SelectToken. +/// +public class JsonPathMatcher : Matcher +{ + /// + public override XchangeResultType ResultType => XchangeResultType.BadResult; + + /// JSONPath expression, e.g. "$.error.code" or "$.lines[0].status". + public required string Path { get; init; } + + /// Comparison operation to apply once the node is located. + public JsonPathOp Op { get; init; } + + /// Expected value. Not used when is or . + public string? Value { get; init; } + + /// + public override bool IsMatch(string content) + { + JsonNode? root; + try { root = JsonNode.Parse(content); } + catch { return false; } + + var node = ResolvePath(root, Path); + + return Op switch + { + JsonPathOp.Exists => node is not null, + JsonPathOp.NotExists => node is null, + _ => node is not null && Compare(node.ToString(), Value ?? "", Op) + }; + } + + private static bool Compare(string actual, string expected, JsonPathOp op) => op switch + { + JsonPathOp.Eq => actual.Equals(expected, StringComparison.OrdinalIgnoreCase), + JsonPathOp.Neq => !actual.Equals(expected, StringComparison.OrdinalIgnoreCase), + JsonPathOp.Contains => actual.Contains(expected, StringComparison.OrdinalIgnoreCase), + JsonPathOp.Regex => Regex.IsMatch(actual, expected, RegexOptions.IgnoreCase), + _ => false + }; + + private static JsonNode? ResolvePath(JsonNode? root, string path) + { + var segments = path.TrimStart('$').TrimStart('.') + .Split('.', StringSplitOptions.RemoveEmptyEntries); + + var current = root; + foreach (var segment in segments) + { + if (current is null) return null; + + var arrayMatch = Regex.Match(segment, @"^(\w+)\[(\d+)\]$"); + if (arrayMatch.Success) + { + current = current[arrayMatch.Groups[1].Value]; + if (current is JsonArray arr && + int.TryParse(arrayMatch.Groups[2].Value, out var idx)) + current = idx < arr.Count ? arr[idx] : null; + } + else + { + current = current[segment]; + } + } + return current; + } +} diff --git a/SW.Bitween.Sdk/Model/AutoRetry/RetryGroup.cs b/SW.Bitween.Sdk/Model/AutoRetry/RetryGroup.cs new file mode 100644 index 00000000..65c057bc --- /dev/null +++ b/SW.Bitween.Sdk/Model/AutoRetry/RetryGroup.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; + +namespace SW.Bitween.Model; + +/// +/// A named family of errors or bad-results that share one retry budget. +/// +/// +/// +/// AppliesTo gates which values this group +/// handles. The evaluator skips groups whose AppliesTo list does not contain the +/// current result type, so an ExceptionTypeMatcher can never accidentally fire on a +/// JSON payload and vice-versa. +/// +/// +/// Matcher logic: OR — the group fires as soon as any single matcher returns +/// true. Matchers incompatible with the current result type are silently skipped. +/// +/// +/// Priority: lower numbers are evaluated first. Leave gaps (10, 20, 30 …) +/// so new groups can be inserted without renumbering. +/// +/// +public class RetryGroup +{ + /// Stable identifier used to track per-group attempt counts across retries. + public Guid Id { get; init; } = Guid.NewGuid(); + + /// Human-readable label. Required; used in . + public required string Name { get; init; } + + /// Evaluation order. Lower number = higher priority. + public int Priority { get; init; } + + /// When false the group is skipped entirely during evaluation. + public bool Enabled { get; init; } = true; + + /// + /// Which result types this group handles. + /// Common values: ["Error"], ["BadResult"], or ["Error","BadResult"]. + /// + public List AppliesTo { get; init; } = []; + + /// OR-logic matchers. The group fires when any one of these returns true. + public List Matchers { get; init; } = []; + + /// + /// Whether to allow or hard-block retries when this group matches. + /// Defaults to . + /// + public RetryAction Action { get; init; } = RetryAction.Allow; + + /// + /// Retry limits and backoff strategy. Must be non-null when + /// is ; ignored when Block. + /// + public RetryBudget? Budget { get; init; } + + /// Optional free-text notes visible in the management UI. + public string? Notes { get; init; } +} + +/// +/// Retry limits and backoff schedule for a single . +/// +public class RetryBudget +{ + /// + /// Maximum number of retry attempts for a single failing message within this group. + /// Prevents one flapping integration from consuming all retries indefinitely. + /// + public int MaxAttemptsPerError { get; init; } + + /// + /// Hard ceiling on the total number of group-level retries across all messages in the + /// current processing window. Prevents a burst of failures from hammering the downstream. + /// + public int MaxAttemptsTotal { get; init; } + + /// Calculates the wait duration before each successive retry attempt. + public required DelayStrategy DelayStrategy { get; init; } +} diff --git a/SW.Bitween.Sdk/Model/AutoRetry/RetryPolicyEvaluator.cs b/SW.Bitween.Sdk/Model/AutoRetry/RetryPolicyEvaluator.cs new file mode 100644 index 00000000..edc9fa86 --- /dev/null +++ b/SW.Bitween.Sdk/Model/AutoRetry/RetryPolicyEvaluator.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SW.Bitween.Model; + +/// +/// Evaluates a retry policy against a single failed xchange and decides whether +/// to schedule another attempt. +/// +/// +/// +/// Stateful per processing window. The evaluator accumulates +/// per-group attempt totals in _groupAttemptCounts. For a single in-process +/// window (e.g. a running RetryJob batch) one instance handles all messages. +/// +/// +/// Cross-invocation persistence. When a retry is scheduled the +/// current counts are saved to and stored on +/// the DelayedRetry entity (and on the new Xchange so that a later +/// failure of the retry itself can pick up where the budgets left off). On the next +/// evaluation call before . +/// +/// +/// Not thread-safe. Each goroutine/task should use its own instance. +/// +/// +public class RetryPolicyEvaluator(IRetryPolicy policy) +{ + private readonly Dictionary _groupAttemptCounts = new(); + + /// + /// Restores previously persisted group-level attempt counts, allowing budgets + /// to continue from where they left off across separate process invocations. + /// + /// + /// The dictionary returned by from a prior evaluation. + /// String keys are parsed back to — invalid entries are silently ignored. + /// + public void RestoreGroupAttemptCounts(Dictionary counts) + { + foreach (var kv in counts) + if (Guid.TryParse(kv.Key, out var guid)) + _groupAttemptCounts[guid] = kv.Value; + } + + /// + /// Returns the current group-level attempt counts as a string-keyed dictionary + /// suitable for JSON serialisation and storage on DelayedRetry / Xchange. + /// + public Dictionary GetGroupAttemptCounts() => + _groupAttemptCounts.ToDictionary(kv => kv.Key.ToString(), kv => kv.Value); + + /// + /// Evaluates the policy and returns a retry decision for the failed xchange. + /// + /// + /// or . + /// Passing throws . + /// + /// + /// Raw failure content: exception stack-trace text for Error, + /// or the JSON response string for BadResult. + /// + /// + /// How many times this specific message has already been attempted (0-based). + /// Used to enforce . + /// + /// + /// A indicating whether to retry and, if so, how long to wait. + /// + public RetryDecision Evaluate( + XchangeResultType resultType, + string content, + int attemptIndexForThisMessage) + { + if (resultType == XchangeResultType.Success) + throw new InvalidOperationException("Success results must never be evaluated for retry."); + + var group = FindMatchingGroup(resultType, content); + + if (group is null) + return RetryDecision.Block("No matching group (default block)"); + + if (group.Action == RetryAction.Block) + return RetryDecision.Block($"Group '{group.Name}' explicitly blocks this error"); + + var budget = group.Budget!; + + if (attemptIndexForThisMessage >= budget.MaxAttemptsPerError) + return RetryDecision.Block( + $"Per-message cap reached ({budget.MaxAttemptsPerError}) in group '{group.Name}'"); + + var totalUsed = _groupAttemptCounts.GetValueOrDefault(group.Id, 0); + if (totalUsed >= budget.MaxAttemptsTotal) + return RetryDecision.Block( + $"Group total cap reached ({budget.MaxAttemptsTotal}) for group '{group.Name}'"); + + _groupAttemptCounts[group.Id] = totalUsed + 1; + + var delay = budget.DelayStrategy.GetDelay(attemptIndexForThisMessage); + return RetryDecision.Allow(delay, group.Name); + } + + private RetryGroup? FindMatchingGroup(XchangeResultType resultType, string content) + { + foreach (var group in policy.Groups + .Where(g => g.Enabled && g.AppliesTo.Contains(resultType)) + .OrderBy(g => g.Priority)) + { + var compatibleMatchers = group.Matchers.Where(m => m.ResultType == resultType); + if (compatibleMatchers.Any(m => m.IsMatch(content))) + return group; + } + return null; + } +} + +/// +/// The outcome of a single call. +/// +public class RetryDecision +{ + /// true when a retry should be scheduled; false to drop the message. + public bool ShouldRetry { get; private init; } + + /// How long to wait before the retry attempt. Meaningful only when is true. + public TimeSpan Delay { get; private init; } + + /// Human-readable explanation of the decision, useful for audit/debug logs. + public string Reason { get; private init; } = ""; + + /// Name of the that matched, or null when blocked. + public string? MatchedGroupName { get; private init; } + + /// Creates an Allow decision — a retry will be scheduled after . + public static RetryDecision Allow(TimeSpan delay, string groupName) => new() + { + ShouldRetry = true, + Delay = delay, + MatchedGroupName = groupName, + Reason = $"Allowed by group '{groupName}'" + }; + + /// Creates a Block decision — no retry will be scheduled. + public static RetryDecision Block(string reason) => new() + { + ShouldRetry = false, + Reason = reason + }; +} diff --git a/SW.Bitween.Sdk/Model/DelayedRetryModel.cs b/SW.Bitween.Sdk/Model/DelayedRetryModel.cs new file mode 100644 index 00000000..cc712856 --- /dev/null +++ b/SW.Bitween.Sdk/Model/DelayedRetryModel.cs @@ -0,0 +1,19 @@ +using System; + +namespace SW.Bitween.Model; + +public class DelayedRetryRow +{ + public string Id { get; set; } + public DateTime On { get; set; } + public int? SubscriptionId { get; set; } + public string SubscriptionName { get; set; } + public int DocumentId { get; set; } + public string DocumentName { get; set; } + public string Exception { get; set; } + public DateTime StartedOn { get; set; } +} + +public class DelayedRetryRunNow +{ +} diff --git a/SW.Bitween.Sdk/Model/RetryPolicyModel.cs b/SW.Bitween.Sdk/Model/RetryPolicyModel.cs new file mode 100644 index 00000000..5d617542 --- /dev/null +++ b/SW.Bitween.Sdk/Model/RetryPolicyModel.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; + +namespace SW.Bitween.Model; + +public class RetryPolicyCreate +{ + public required string Name { get; set; } + public List Groups { get; set; } = []; +} + +public class RetryPolicyUpdate : RetryPolicyCreate { } + +public class RetryPolicyRow +{ + public int Id { get; set; } + public string Name { get; set; } + public int GroupCount { get; set; } +} + +/// +/// Simulates evaluating a (possibly unsaved/draft) set of retry groups against a single +/// failure, across as many consecutive attempts as requested, so the management UI can +/// show "what would happen" before the policy is saved. +/// +public class TestRetryPolicyRequest +{ + /// The draft groups to test — not necessarily the persisted policy's groups. + public List Groups { get; set; } = []; + + /// Error or BadResult — Success is never retried and is rejected. + public XchangeResultType ResultType { get; set; } + + /// Exception text for Error, or the raw JSON response body for BadResult. + public required string Content { get; set; } + + /// How many consecutive failed attempts of this same message to simulate. + public int AttemptsToSimulate { get; set; } = 5; +} + +public class TestRetryPolicyResponse +{ + public List Attempts { get; set; } = []; +} + +public class TestRetryAttemptResult +{ + public int AttemptNumber { get; set; } + public string? MatchedGroupName { get; set; } + public bool ShouldRetry { get; set; } + public double? DelaySeconds { get; set; } + public string Reason { get; set; } = ""; +} diff --git a/SW.Bitween.Sdk/Model/Subscription.cs b/SW.Bitween.Sdk/Model/Subscription.cs index fa45ad96..17943c53 100644 --- a/SW.Bitween.Sdk/Model/Subscription.cs +++ b/SW.Bitween.Sdk/Model/Subscription.cs @@ -97,6 +97,9 @@ public class SubscriptionUpdate : SubscriptionCreateUpdateBase public DateTime? PausedOn { get; set; } public string CategoryCode { get; set; } public string CategoryDescription { get; set; } + + public int? RetryPolicyId { get; set; } + public CustomRetryPolicy CustomRetryPolicy { get; set; } } public class SubscriptionGet : SubscriptionUpdate diff --git a/SW.Bitween.Sdk/Model/Xchange.cs b/SW.Bitween.Sdk/Model/Xchange.cs index b31f17be..fa321a61 100644 --- a/SW.Bitween.Sdk/Model/Xchange.cs +++ b/SW.Bitween.Sdk/Model/Xchange.cs @@ -96,5 +96,6 @@ public class XchangeRow public bool? ResponseBad { get; set; } public string CorrelationId { get; set; } public int? PartnerId { get; set; } + public DateTime? ScheduledRetryOn { get; set; } } } \ No newline at end of file diff --git a/SW.Bitween.UnitTests/MatcherPolymorphicJsonTests.cs b/SW.Bitween.UnitTests/MatcherPolymorphicJsonTests.cs new file mode 100644 index 00000000..85b7995b --- /dev/null +++ b/SW.Bitween.UnitTests/MatcherPolymorphicJsonTests.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SW.Bitween.Model; + +namespace SW.Bitween.UnitTests; + +[TestClass] +public class MatcherPolymorphicJsonTests +{ + private static readonly JsonSerializerOptions Opts = new() + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver() + }; + + [TestMethod] + public void JsonPathMatcher_round_trips_with_DefaultJsonTypeInfoResolver() + { + var matchers = new List + { + new ExceptionTypeMatcher { Value = "System.TimeoutException" }, + new JsonPathMatcher { Path = "$.error.code", Op = JsonPathOp.Eq, Value = "500" } + }; + + var json = JsonSerializer.Serialize(matchers, Opts); + Assert.IsTrue(json.Contains("\"type\""), string.Format("No discriminator in JSON: {0}", json)); + + var roundTripped = JsonSerializer.Deserialize>(json, Opts); + Assert.IsNotNull(roundTripped); + Assert.AreEqual(2, roundTripped.Count); + Assert.IsInstanceOfType(roundTripped[0], typeof(ExceptionTypeMatcher)); + Assert.IsInstanceOfType(roundTripped[1], typeof(JsonPathMatcher)); + } +} diff --git a/SW.Bitween.UnitTests/RetryPolicyEvaluatorTests.cs b/SW.Bitween.UnitTests/RetryPolicyEvaluatorTests.cs new file mode 100644 index 00000000..bb8a11a5 --- /dev/null +++ b/SW.Bitween.UnitTests/RetryPolicyEvaluatorTests.cs @@ -0,0 +1,410 @@ +using System; +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SW.Bitween.Model; + +namespace SW.Bitween.UnitTests; + +[TestClass] +public class RetryPolicyEvaluatorTests +{ + // ─── Helpers ──────────────────────────────────────────────────────────────── + + private static IRetryPolicy PolicyWith(params RetryGroup[] groups) => new TestPolicy(groups); + + private static RetryGroup ErrorGroup( + string name, + Matcher matcher, + int maxPerError = 5, + int maxTotal = 100, + DelayStrategy delay = null, + int priority = 10, + RetryAction action = RetryAction.Allow) => + new RetryGroup + { + Name = name, + Priority = priority, + Enabled = true, + AppliesTo = [XchangeResultType.Error], + Action = action, + Matchers = [matcher], + Budget = action == RetryAction.Allow ? new RetryBudget + { + MaxAttemptsPerError = maxPerError, + MaxAttemptsTotal = maxTotal, + DelayStrategy = delay ?? new FixedDelayStrategy { DelayMs = 1000 } + } : null + }; + + private static RetryGroup BadResultGroup( + string name, + Matcher matcher, + int maxPerError = 5, + int maxTotal = 100, + DelayStrategy delay = null) => + new RetryGroup + { + Name = name, + Priority = 10, + Enabled = true, + AppliesTo = [XchangeResultType.BadResult], + Matchers = [matcher], + Budget = new RetryBudget + { + MaxAttemptsPerError = maxPerError, + MaxAttemptsTotal = maxTotal, + DelayStrategy = delay ?? new FixedDelayStrategy { DelayMs = 1000 } + } + }; + + private sealed class TestPolicy(RetryGroup[] groups) : IRetryPolicy + { + public List Groups { get; } = new List(groups); + } + + // ─── ContainsMatcher ──────────────────────────────────────────────────────── + + [TestMethod] + public void ContainsMatcher_MatchesSubstring() + { + var m = new ContainsMatcher { Value = "timeout" }; + Assert.IsTrue(m.IsMatch("Connection timeout occurred")); + } + + [TestMethod] + public void ContainsMatcher_NoMatch() + { + var m = new ContainsMatcher { Value = "timeout" }; + Assert.IsFalse(m.IsMatch("Something unrelated happened")); + } + + [TestMethod] + public void ContainsMatcher_CaseInsensitiveByDefault() + { + var m = new ContainsMatcher { Value = "TIMEOUT" }; + Assert.IsTrue(m.IsMatch("Connection timeout occurred")); + } + + [TestMethod] + public void ContainsMatcher_CaseSensitive_WrongCase_NoMatch() + { + var m = new ContainsMatcher { Value = "TIMEOUT", CaseSensitive = true }; + Assert.IsFalse(m.IsMatch("Connection timeout occurred")); + } + + [TestMethod] + public void ContainsMatcher_CaseSensitive_CorrectCase_Matches() + { + var m = new ContainsMatcher { Value = "timeout", CaseSensitive = true }; + Assert.IsTrue(m.IsMatch("Connection timeout occurred")); + } + + // ─── RegexMatcher ─────────────────────────────────────────────────────────── + + [TestMethod] + public void RegexMatcher_PatternMatches() + { + var m = new RegexMatcher { Pattern = @"\d{3}" }; + Assert.IsTrue(m.IsMatch("Error code 404 returned")); + } + + [TestMethod] + public void RegexMatcher_PatternNoMatch() + { + var m = new RegexMatcher { Pattern = @"^fatal" }; + Assert.IsFalse(m.IsMatch("non-fatal error")); + } + + [TestMethod] + public void RegexMatcher_DefaultFlagCaseInsensitive() + { + var m = new RegexMatcher { Pattern = "timeout" }; // default flags = "i" + Assert.IsTrue(m.IsMatch("TIMEOUT error")); + } + + [TestMethod] + public void RegexMatcher_ExplicitCaseSensitiveFlag_WrongCase_NoMatch() + { + var m = new RegexMatcher { Pattern = "timeout", Flags = "" }; + Assert.IsFalse(m.IsMatch("TIMEOUT error")); + } + + // ─── ExceptionTypeMatcher ─────────────────────────────────────────────────── + + [TestMethod] + public void ExceptionTypeMatcher_MatchesExactType() + { + // Matcher compares the full qualified name extracted by the regex + var m = new ExceptionTypeMatcher { Value = "System.TimeoutException" }; + Assert.IsTrue(m.IsMatch("System.TimeoutException: The operation timed out.")); + } + + [TestMethod] + public void ExceptionTypeMatcher_NoMatch_UnrelatedContent() + { + var m = new ExceptionTypeMatcher { Value = "System.TimeoutException" }; + Assert.IsFalse(m.IsMatch("Something went wrong with the database.")); + } + + [TestMethod] + public void ExceptionTypeMatcher_IncludeInner_MatchesInnerException() + { + // First match is System.Exception (outer); second is SqlException (inner) + var m = new ExceptionTypeMatcher { Value = "SqlException", IncludeInner = true }; + var content = "System.Exception: outer ---> SqlException: inner details"; + Assert.IsTrue(m.IsMatch(content)); + } + + [TestMethod] + public void ExceptionTypeMatcher_ExcludeInner_DoesNotMatchInnerException() + { + // IncludeInner = false → only the first match (System.Exception) is checked + var m = new ExceptionTypeMatcher { Value = "SqlException", IncludeInner = false }; + var content = "System.Exception: outer ---> SqlException: inner details"; + Assert.IsFalse(m.IsMatch(content)); + } + + // ─── JsonPathMatcher ──────────────────────────────────────────────────────── + + [TestMethod] + public void JsonPathMatcher_Eq_Match() + { + var m = new JsonPathMatcher { Path = "$.error.code", Op = JsonPathOp.Eq, Value = "404" }; + Assert.IsTrue(m.IsMatch("{\"error\":{\"code\":\"404\"}}")); + } + + [TestMethod] + public void JsonPathMatcher_Eq_NoMatch() + { + var m = new JsonPathMatcher { Path = "$.error.code", Op = JsonPathOp.Eq, Value = "404" }; + Assert.IsFalse(m.IsMatch("{\"error\":{\"code\":\"500\"}}")); + } + + [TestMethod] + public void JsonPathMatcher_Contains_Match() + { + var m = new JsonPathMatcher { Path = "$.message", Op = JsonPathOp.Contains, Value = "not found" }; + Assert.IsTrue(m.IsMatch("{\"message\":\"Resource not found\"}")); + } + + [TestMethod] + public void JsonPathMatcher_Exists_PathPresent() + { + var m = new JsonPathMatcher { Path = "$.retryable", Op = JsonPathOp.Exists }; + Assert.IsTrue(m.IsMatch("{\"retryable\":true}")); + } + + [TestMethod] + public void JsonPathMatcher_Exists_PathAbsent() + { + var m = new JsonPathMatcher { Path = "$.retryable", Op = JsonPathOp.Exists }; + Assert.IsFalse(m.IsMatch("{\"other\":true}")); + } + + [TestMethod] + public void JsonPathMatcher_InvalidJson_ReturnsFalse() + { + var m = new JsonPathMatcher { Path = "$.error.code", Op = JsonPathOp.Eq, Value = "404" }; + Assert.IsFalse(m.IsMatch("not json at all")); + } + + [TestMethod] + public void JsonPathMatcher_ArrayIndexer_Match() + { + var m = new JsonPathMatcher { Path = "$.lines[0].status", Op = JsonPathOp.Eq, Value = "error" }; + Assert.IsTrue(m.IsMatch("{\"lines\":[{\"status\":\"error\"},{\"status\":\"ok\"}]}")); + } + + // ─── Evaluator: basic routing ──────────────────────────────────────────────── + + [TestMethod] + public void Evaluator_MatchingGroup_AllowsRetry() + { + var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "timeout" })); + var ev = new RetryPolicyEvaluator(policy); + var decision = ev.Evaluate(XchangeResultType.Error, "Connection timeout", 0); + Assert.IsTrue(decision.ShouldRetry); + Assert.AreEqual("transient", decision.MatchedGroupName); + } + + [TestMethod] + public void Evaluator_NoMatchingGroup_Blocks() + { + var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "timeout" })); + var ev = new RetryPolicyEvaluator(policy); + var decision = ev.Evaluate(XchangeResultType.Error, "Disk full", 0); + Assert.IsFalse(decision.ShouldRetry); + } + + [TestMethod] + public void Evaluator_WrongResultType_GroupSkipped() + { + var policy = PolicyWith(BadResultGroup("bad", new JsonPathMatcher { Path = "$.retryable", Op = JsonPathOp.Exists })); + var ev = new RetryPolicyEvaluator(policy); + // Group is for BadResult only — must be skipped for Error + var decision = ev.Evaluate(XchangeResultType.Error, "some exception", 0); + Assert.IsFalse(decision.ShouldRetry); + } + + // ─── Evaluator: priority ordering ─────────────────────────────────────────── + + [TestMethod] + public void Evaluator_LowerPriorityEvaluatedFirst() + { + var g1 = ErrorGroup("low-num", new ContainsMatcher { Value = "error" }, priority: 1); + var g2 = ErrorGroup("high-num", new ContainsMatcher { Value = "error" }, priority: 20); + var policy = PolicyWith(g2, g1); // intentionally reversed in array + var ev = new RetryPolicyEvaluator(policy); + var decision = ev.Evaluate(XchangeResultType.Error, "error occurred", 0); + Assert.AreEqual("low-num", decision.MatchedGroupName); + } + + [TestMethod] + public void Evaluator_OnlyMatchingGroupFires() + { + var g1 = ErrorGroup("timeouts", new ContainsMatcher { Value = "timeout" }, priority: 1); + var g2 = ErrorGroup("disk", new ContainsMatcher { Value = "disk" }, priority: 20); + var policy = PolicyWith(g1, g2); + var ev = new RetryPolicyEvaluator(policy); + var decision = ev.Evaluate(XchangeResultType.Error, "disk full", 0); + Assert.AreEqual("disk", decision.MatchedGroupName); + } + + // ─── Evaluator: budget — MaxAttemptsPerError ───────────────────────────────── + + [TestMethod] + public void Evaluator_MaxAttemptsPerError_BlocksAfterCap() + { + var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "err" }, maxPerError: 2)); + var ev = new RetryPolicyEvaluator(policy); + + Assert.IsTrue(ev.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); + Assert.IsTrue(ev.Evaluate(XchangeResultType.Error, "err", 1).ShouldRetry); + Assert.IsFalse(ev.Evaluate(XchangeResultType.Error, "err", 2).ShouldRetry); // cap = 2 + } + + // ─── Evaluator: budget — MaxAttemptsTotal ──────────────────────────────────── + + [TestMethod] + public void Evaluator_MaxAttemptsTotal_BlocksAfterGroupCap() + { + var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "err" }, + maxPerError: 100, maxTotal: 3)); + var ev = new RetryPolicyEvaluator(policy); + + // Three different "messages" (attempt index 0 each time) exhaust the group total + Assert.IsTrue(ev.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); + Assert.IsTrue(ev.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); + Assert.IsTrue(ev.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); + Assert.IsFalse(ev.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); // exceeded total=3 + } + + // ─── Evaluator: delay strategies ───────────────────────────────────────────── + + [TestMethod] + public void FixedDelay_SameEveryAttempt() + { + var s = new FixedDelayStrategy { DelayMs = 2000 }; + Assert.AreEqual(TimeSpan.FromMilliseconds(2000), s.GetDelay(0)); + Assert.AreEqual(TimeSpan.FromMilliseconds(2000), s.GetDelay(5)); + } + + [TestMethod] + public void LinearDelay_GrowsByIncrement() + { + var s = new LinearDelayStrategy { InitialDelayMs = 1000, IncrementMs = 500 }; + Assert.AreEqual(TimeSpan.FromMilliseconds(1000), s.GetDelay(0)); + Assert.AreEqual(TimeSpan.FromMilliseconds(1500), s.GetDelay(1)); + Assert.AreEqual(TimeSpan.FromMilliseconds(2000), s.GetDelay(2)); + } + + [TestMethod] + public void ExponentialDelay_DoublesAndCaps() + { + var s = new ExponentialDelayStrategy { InitialDelayMs = 1000, MaxDelayMs = 8000 }; + Assert.AreEqual(TimeSpan.FromMilliseconds(1000), s.GetDelay(0)); + Assert.AreEqual(TimeSpan.FromMilliseconds(2000), s.GetDelay(1)); + Assert.AreEqual(TimeSpan.FromMilliseconds(4000), s.GetDelay(2)); + Assert.AreEqual(TimeSpan.FromMilliseconds(8000), s.GetDelay(3)); + Assert.AreEqual(TimeSpan.FromMilliseconds(8000), s.GetDelay(4)); // capped + } + + [TestMethod] + public void Evaluator_DelayFromStrategy_ReturnsCorrectValue() + { + var policy = PolicyWith(ErrorGroup("transient", + new ContainsMatcher { Value = "err" }, + delay: new FixedDelayStrategy { DelayMs = 3000 })); + var ev = new RetryPolicyEvaluator(policy); + var decision = ev.Evaluate(XchangeResultType.Error, "err", 0); + Assert.AreEqual(TimeSpan.FromMilliseconds(3000), decision.Delay); + } + + // ─── Evaluator: GroupAttemptCounts persistence ─────────────────────────────── + + [TestMethod] + public void GroupAttemptCounts_RestoredAcrossEvaluators_ContinuesBudget() + { + var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "err" }, + maxPerError: 100, maxTotal: 2)); + + var ev1 = new RetryPolicyEvaluator(policy); + Assert.IsTrue(ev1.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); // group count = 1 + var counts = ev1.GetGroupAttemptCounts(); + + // Simulate the next evaluator instance restoring saved state + var ev2 = new RetryPolicyEvaluator(policy); + ev2.RestoreGroupAttemptCounts(counts); // group count restored to 1 + Assert.IsTrue(ev2.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); // group count = 2 + var counts2 = ev2.GetGroupAttemptCounts(); + + var ev3 = new RetryPolicyEvaluator(policy); + ev3.RestoreGroupAttemptCounts(counts2); // group count restored to 2 + Assert.IsFalse(ev3.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); // exceeded total=2 + } + + [TestMethod] + public void GroupAttemptCounts_WithoutRestore_BudgetResetsToZero() + { + var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "err" }, + maxPerError: 100, maxTotal: 1)); + + var ev1 = new RetryPolicyEvaluator(policy); + Assert.IsTrue(ev1.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); // exhausted + + // Fresh evaluator without restore — budget starts from 0 again + var ev2 = new RetryPolicyEvaluator(policy); + Assert.IsTrue(ev2.Evaluate(XchangeResultType.Error, "err", 0).ShouldRetry); + } + + [TestMethod] + public void GetGroupAttemptCounts_ReturnsNonEmptyAfterMatch() + { + var policy = PolicyWith(ErrorGroup("transient", new ContainsMatcher { Value = "err" })); + var ev = new RetryPolicyEvaluator(policy); + ev.Evaluate(XchangeResultType.Error, "err", 0); + var counts = ev.GetGroupAttemptCounts(); + Assert.IsTrue(counts.Count > 0); + } + + // ─── Evaluator: Block action ───────────────────────────────────────────────── + + [TestMethod] + public void Evaluator_BlockAction_NeverRetries() + { + var blockGroup = new RetryGroup + { + Name = "permanent-errors", + Priority = 1, + Enabled = true, + AppliesTo = [XchangeResultType.Error], + Action = RetryAction.Block, + Matchers = [new ContainsMatcher { Value = "fatal" }], + Budget = null + }; + var policy = PolicyWith(blockGroup); + var ev = new RetryPolicyEvaluator(policy); + var decision = ev.Evaluate(XchangeResultType.Error, "fatal: cannot recover", 0); + Assert.IsFalse(decision.ShouldRetry); + } +} diff --git a/SW.Bitween.UnitTests/RetryPolicyJsonConverterTests.cs b/SW.Bitween.UnitTests/RetryPolicyJsonConverterTests.cs new file mode 100644 index 00000000..f15f9195 --- /dev/null +++ b/SW.Bitween.UnitTests/RetryPolicyJsonConverterTests.cs @@ -0,0 +1,162 @@ +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using SW.Bitween.JsonConverters; +using SW.Bitween.Model; + +namespace SW.Bitween.UnitTests; + +[TestClass] +public class RetryPolicyJsonConverterTests +{ + private static JsonSerializer BuildSerializer() + { + var serializer = new JsonSerializer(); + serializer.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter()); + serializer.Converters.Add(new MatcherJsonConverter()); + serializer.Converters.Add(new DelayStrategyJsonConverter()); + return serializer; + } + + private static T RoundTrip(T value, JsonSerializer serializer) + { + string json; + using (var sw = new StringWriter()) + using (JsonWriter writer = new JsonTextWriter(sw)) + { + serializer.Serialize(writer, value); + json = sw.ToString(); + } + + using var sr = new StringReader(json); + using JsonReader reader = new JsonTextReader(sr); + return serializer.Deserialize(reader); + } + + [TestMethod] + public void ContainsMatcher_round_trips_through_newtonsoft() + { + var serializer = BuildSerializer(); + Matcher original = new ContainsMatcher { Value = "timeout", CaseSensitive = true }; + + var result = RoundTrip(original, serializer); + + var typed = Assert1(result); + Assert.AreEqual("timeout", typed.Value); + Assert.IsTrue(typed.CaseSensitive); + } + + [TestMethod] + public void RegexMatcher_round_trips_through_newtonsoft() + { + var serializer = BuildSerializer(); + Matcher original = new RegexMatcher { Pattern = "connect.*failed", Flags = "" }; + + var result = RoundTrip(original, serializer); + + var typed = Assert1(result); + Assert.AreEqual("connect.*failed", typed.Pattern); + Assert.AreEqual("", typed.Flags); + } + + [TestMethod] + public void ExceptionTypeMatcher_round_trips_through_newtonsoft() + { + var serializer = BuildSerializer(); + Matcher original = new ExceptionTypeMatcher { Value = "System.TimeoutException", IncludeInner = false }; + + var result = RoundTrip(original, serializer); + + var typed = Assert1(result); + Assert.AreEqual("System.TimeoutException", typed.Value); + Assert.IsFalse(typed.IncludeInner); + } + + [TestMethod] + public void JsonPathMatcher_round_trips_through_newtonsoft() + { + var serializer = BuildSerializer(); + Matcher original = new JsonPathMatcher { Path = "$.error.code", Op = JsonPathOp.Eq, Value = "500" }; + + var result = RoundTrip(original, serializer); + + var typed = Assert1(result); + Assert.AreEqual("$.error.code", typed.Path); + Assert.AreEqual(JsonPathOp.Eq, typed.Op); + Assert.AreEqual("500", typed.Value); + } + + [TestMethod] + public void FixedDelayStrategy_round_trips_through_newtonsoft() + { + var serializer = BuildSerializer(); + DelayStrategy original = new FixedDelayStrategy { DelayMs = 5000 }; + + var result = RoundTrip(original, serializer); + + var typed = Assert1(result); + Assert.AreEqual(5000, typed.DelayMs); + } + + [TestMethod] + public void LinearDelayStrategy_round_trips_through_newtonsoft() + { + var serializer = BuildSerializer(); + DelayStrategy original = new LinearDelayStrategy { InitialDelayMs = 1000, IncrementMs = 500 }; + + var result = RoundTrip(original, serializer); + + var typed = Assert1(result); + Assert.AreEqual(1000, typed.InitialDelayMs); + Assert.AreEqual(500, typed.IncrementMs); + } + + [TestMethod] + public void ExponentialDelayStrategy_round_trips_through_newtonsoft() + { + var serializer = BuildSerializer(); + DelayStrategy original = new ExponentialDelayStrategy { InitialDelayMs = 1000, Multiplier = 3.0, MaxDelayMs = 60_000 }; + + var result = RoundTrip(original, serializer); + + var typed = Assert1(result); + Assert.AreEqual(1000, typed.InitialDelayMs); + Assert.AreEqual(3.0, typed.Multiplier); + Assert.AreEqual(60_000, typed.MaxDelayMs); + } + + [TestMethod] + public void RetryGroup_with_nested_matcher_and_delay_strategy_round_trips() + { + var serializer = BuildSerializer(); + var original = new RetryGroup + { + Name = "Timeout Group", + Priority = 10, + AppliesTo = [XchangeResultType.Error], + Matchers = [new ContainsMatcher { Value = "timeout" }], + Action = RetryAction.Allow, + Budget = new RetryBudget + { + MaxAttemptsPerError = 3, + MaxAttemptsTotal = 10, + DelayStrategy = new ExponentialDelayStrategy { InitialDelayMs = 1000, Multiplier = 2, MaxDelayMs = 30_000 } + } + }; + + var result = RoundTrip(original, serializer); + + Assert.IsNotNull(result); + Assert.AreEqual("Timeout Group", result.Name); + Assert.AreEqual(RetryAction.Allow, result.Action); + Assert.AreEqual(1, result.Matchers.Count); + Assert.IsInstanceOfType(result.Matchers[0], typeof(ContainsMatcher)); + Assert.IsInstanceOfType(result.Budget!.DelayStrategy, typeof(ExponentialDelayStrategy)); + } + + private static T Assert1(object value) where T : class + { + Assert.IsInstanceOfType(value, typeof(T)); + return (T)value; + } +} diff --git a/SW.Bitween.Web/Properties/launchSettings.json b/SW.Bitween.Web/Properties/launchSettings.json index 68c5a1f0..769b0d64 100644 --- a/SW.Bitween.Web/Properties/launchSettings.json +++ b/SW.Bitween.Web/Properties/launchSettings.json @@ -1,22 +1,13 @@ - - - { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:63359/", - "sslPort": 44363 - } - }, "profiles": { - "IIS Express": { - "commandName": "IISExpress", + "https": { + "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "applicationUrl": "https://localhost:5000;http://localhost:5003" } } } \ No newline at end of file diff --git a/SW.Bitween.Web/SW.Bitween.Web.csproj b/SW.Bitween.Web/SW.Bitween.Web.csproj index 71a4aef0..90610eb7 100644 --- a/SW.Bitween.Web/SW.Bitween.Web.csproj +++ b/SW.Bitween.Web/SW.Bitween.Web.csproj @@ -20,6 +20,7 @@ + @@ -35,6 +36,7 @@ + diff --git a/SW.Bitween.Web/Startup.cs b/SW.Bitween.Web/Startup.cs index 272a4d3e..ed78fc95 100644 --- a/SW.Bitween.Web/Startup.cs +++ b/SW.Bitween.Web/Startup.cs @@ -30,6 +30,11 @@ using Azure.Identity; using Microsoft.Data.SqlClient; using SW.Bitween.NativeAdapters; +using SW.Scheduler; +using SW.Scheduler.EfCore; +using SW.Scheduler.MySql; +using SW.Scheduler.PgSql; +using SW.Scheduler.SqlServer; using SqlAuthenticationProvider = Microsoft.Data.SqlClient.SqlAuthenticationProvider; using SqlAuthenticationMethod = Microsoft.Data.SqlClient.SqlAuthenticationMethod; @@ -39,12 +44,14 @@ public class Startup { private static readonly string ApiXchangeCreatedEventQueueName = "XchangeService.ApiXchangeCreatedEvent"; - public Startup(IConfiguration configuration) + public Startup(IConfiguration configuration, IWebHostEnvironment environment) { Configuration = configuration; + Environment = environment; } private IConfiguration Configuration { get; } + private IWebHostEnvironment Environment { get; } public void ConfigureServices(IServiceCollection services) { @@ -61,8 +68,8 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddHttpContextAccessor(); - services.AddHostedService(); - services.AddHostedService(); + services.AddScoped(); + services.AddHostedService(); services.AddBus(config => { @@ -78,6 +85,8 @@ public void ConfigureServices(IServiceCollection services) var serializer = new JsonSerializer(); serializer.Converters.Add(new PropertyMatchSpecificationJsonConverter()); + serializer.Converters.Add(new MatcherJsonConverter()); + serializer.Converters.Add(new DelayStrategyJsonConverter()); serializer.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter()); serializer.ContractResolver = new CamelCasePropertyNamesContractResolver { @@ -113,6 +122,12 @@ public void ConfigureServices(IServiceCollection services) case "S3": services.AddS3CloudFiles(); break; + case "LOCAL": + if (!Environment.IsDevelopment()) + throw new InvalidOperationException( + "StorageProvider 'Local' stores files on the local filesystem and is only allowed when ASPNETCORE_ENVIRONMENT is 'Development'."); + services.AddLocalTestsCloudFiles(); + break; default: services.AddS3CloudFiles(); break; @@ -133,6 +148,44 @@ public void ConfigureServices(IServiceCollection services) "Please check your appsettings.json or environment configuration."); } + // For SQL Server + managed identity, augment the connection string up front so both + // the Quartz scheduler below and the DbContext registered later use the exact same + // (fully authenticated) value — previously this was only applied after the scheduler + // had already captured the un-augmented string, so Quartz would fail to authenticate. + if (bitweenOptions.UseAzureManagedIdentity && + bitweenOptions.DatabaseType.Equals(RelationalDbType.MsSql.ToString(), StringComparison.OrdinalIgnoreCase) && + !connectionString.Contains("Authentication=", StringComparison.OrdinalIgnoreCase)) + { + connectionString += ";Authentication=Active Directory Default"; + } + + // Register the persistent Quartz scheduler using the same DB as Bitween. + // NOTE: clustering is only guaranteed once SimplyWorks.Scheduler.* is bumped past + // 8.1.1 (the version pinned in the .csproj files as of this comment) — the fix that + // makes clustering unconditional (unique auto-generated SchedulerId per instance) + // hasn't been published yet. Until that bump happens, these packages run + // NON-clustered (EnableClustering defaulted to false and no longer settable here). + if (string.Equals(bitweenOptions.DatabaseType, RelationalDbType.PgSql.ToString(), StringComparison.OrdinalIgnoreCase)) + { + services.AddPgSqlScheduler( + connectionString: connectionString, + schema: PgSql.BitweenDbContext.Schema, + assemblies: typeof(BitweenDbContext).Assembly); + } + else if (string.Equals(bitweenOptions.DatabaseType, RelationalDbType.MsSql.ToString(), StringComparison.OrdinalIgnoreCase)) + { + services.AddSqlServerScheduler( + connectionString: connectionString, + assemblies: typeof(BitweenDbContext).Assembly); + } + else + { + // MySql (default) + services.AddMySqlScheduler( + connectionString: connectionString, + assemblies: typeof(BitweenDbContext).Assembly); + } + // Configure Azure Managed Identity for SQL Server if enabled if (bitweenOptions.UseAzureManagedIdentity && bitweenOptions.DatabaseType.Equals(RelationalDbType.MsSql.ToString(), StringComparison.OrdinalIgnoreCase)) @@ -201,36 +254,30 @@ public void ConfigureServices(IServiceCollection services) }); }); } + + services.AddSchedulerMonitoring(); + } + else if (string.Equals(bitweenOptions.DatabaseType, RelationalDbType.MsSql.ToString(), + StringComparison.OrdinalIgnoreCase)) + { + services.AddDbContext(c => + { + c.EnableSensitiveDataLogging(); + c.UseSqlServer(connectionString, + b => { b.MigrationsAssembly(typeof(MsSql.DbType).Assembly.FullName); }); + }); + services.AddSchedulerMonitoring(); } else { - services.AddDbContext(c => + // MySql (default) + services.AddDbContext(c => { c.EnableSensitiveDataLogging(); - if (string.Equals(bitweenOptions.DatabaseType, RelationalDbType.MySql.ToString(), - StringComparison.CurrentCultureIgnoreCase)) - { - // MySQL doesn't support Azure Managed Identity in the same way - c.UseMySql(Configuration.GetConnectionString(BitweenDbContext.ConnectionString), - new MySqlServerVersion(new Version(8, 0, 18)), - b => { b.MigrationsAssembly(typeof(MySql.DbType).Assembly.FullName); }); - } - else if (bitweenOptions.DatabaseType.ToLower() == RelationalDbType.MsSql.ToString().ToLower()) - { - // For Azure Managed Identity with SQL Server, add Authentication parameter - if (bitweenOptions.UseAzureManagedIdentity) - { - // Ensure connection string has the required authentication mode - if (!connectionString.Contains("Authentication=", StringComparison.OrdinalIgnoreCase)) - { - connectionString += ";Authentication=Active Directory Default"; - } - } - - c.UseSqlServer(connectionString, - b => { b.MigrationsAssembly(typeof(MsSql.DbType).Assembly.FullName); }); - } + c.UseMySql(connectionString, new MySqlServerVersion(new Version(8, 0, 18)), + b => { b.MigrationsAssembly(typeof(MySql.DbType).Assembly.FullName); }); }); + services.AddSchedulerMonitoring(); } diff --git a/SW.Bitween.sln b/SW.Bitween.sln index 0c82ab3e..47c2b2c7 100644 --- a/SW.Bitween.sln +++ b/SW.Bitween.sln @@ -29,56 +29,162 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SW.Bitween.PgSql", "SW.Bitw EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SW.Bitween.NativeAdapters", "SW.Bitween.NativeAdapters\SW.Bitween.NativeAdapters.csproj", "{5D7B6BD7-427E-4F1B-B4CA-CF6B3A3ED89F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SW.Bitween.IntegrationTests", "SW.Bitween.IntegrationTests\SW.Bitween.IntegrationTests.csproj", "{A2B58DFC-A4B0-4BD2-9552-322C6E87DA39}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {E4CE64BC-3964-4CB7-A5B9-0E4CF7F30DCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E4CE64BC-3964-4CB7-A5B9-0E4CF7F30DCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4CE64BC-3964-4CB7-A5B9-0E4CF7F30DCF}.Debug|x64.ActiveCfg = Debug|Any CPU + {E4CE64BC-3964-4CB7-A5B9-0E4CF7F30DCF}.Debug|x64.Build.0 = Debug|Any CPU + {E4CE64BC-3964-4CB7-A5B9-0E4CF7F30DCF}.Debug|x86.ActiveCfg = Debug|Any CPU + {E4CE64BC-3964-4CB7-A5B9-0E4CF7F30DCF}.Debug|x86.Build.0 = Debug|Any CPU {E4CE64BC-3964-4CB7-A5B9-0E4CF7F30DCF}.Release|Any CPU.ActiveCfg = Release|Any CPU {E4CE64BC-3964-4CB7-A5B9-0E4CF7F30DCF}.Release|Any CPU.Build.0 = Release|Any CPU + {E4CE64BC-3964-4CB7-A5B9-0E4CF7F30DCF}.Release|x64.ActiveCfg = Release|Any CPU + {E4CE64BC-3964-4CB7-A5B9-0E4CF7F30DCF}.Release|x64.Build.0 = Release|Any CPU + {E4CE64BC-3964-4CB7-A5B9-0E4CF7F30DCF}.Release|x86.ActiveCfg = Release|Any CPU + {E4CE64BC-3964-4CB7-A5B9-0E4CF7F30DCF}.Release|x86.Build.0 = Release|Any CPU {70BE66CC-4FEB-4945-9E77-DAC6FC95BD91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {70BE66CC-4FEB-4945-9E77-DAC6FC95BD91}.Debug|Any CPU.Build.0 = Debug|Any CPU + {70BE66CC-4FEB-4945-9E77-DAC6FC95BD91}.Debug|x64.ActiveCfg = Debug|Any CPU + {70BE66CC-4FEB-4945-9E77-DAC6FC95BD91}.Debug|x64.Build.0 = Debug|Any CPU + {70BE66CC-4FEB-4945-9E77-DAC6FC95BD91}.Debug|x86.ActiveCfg = Debug|Any CPU + {70BE66CC-4FEB-4945-9E77-DAC6FC95BD91}.Debug|x86.Build.0 = Debug|Any CPU {70BE66CC-4FEB-4945-9E77-DAC6FC95BD91}.Release|Any CPU.ActiveCfg = Release|Any CPU {70BE66CC-4FEB-4945-9E77-DAC6FC95BD91}.Release|Any CPU.Build.0 = Release|Any CPU + {70BE66CC-4FEB-4945-9E77-DAC6FC95BD91}.Release|x64.ActiveCfg = Release|Any CPU + {70BE66CC-4FEB-4945-9E77-DAC6FC95BD91}.Release|x64.Build.0 = Release|Any CPU + {70BE66CC-4FEB-4945-9E77-DAC6FC95BD91}.Release|x86.ActiveCfg = Release|Any CPU + {70BE66CC-4FEB-4945-9E77-DAC6FC95BD91}.Release|x86.Build.0 = Release|Any CPU {877D7394-ACC5-482C-8DE5-0B0D0639D4BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {877D7394-ACC5-482C-8DE5-0B0D0639D4BE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {877D7394-ACC5-482C-8DE5-0B0D0639D4BE}.Debug|x64.ActiveCfg = Debug|Any CPU + {877D7394-ACC5-482C-8DE5-0B0D0639D4BE}.Debug|x64.Build.0 = Debug|Any CPU + {877D7394-ACC5-482C-8DE5-0B0D0639D4BE}.Debug|x86.ActiveCfg = Debug|Any CPU + {877D7394-ACC5-482C-8DE5-0B0D0639D4BE}.Debug|x86.Build.0 = Debug|Any CPU {877D7394-ACC5-482C-8DE5-0B0D0639D4BE}.Release|Any CPU.ActiveCfg = Release|Any CPU {877D7394-ACC5-482C-8DE5-0B0D0639D4BE}.Release|Any CPU.Build.0 = Release|Any CPU + {877D7394-ACC5-482C-8DE5-0B0D0639D4BE}.Release|x64.ActiveCfg = Release|Any CPU + {877D7394-ACC5-482C-8DE5-0B0D0639D4BE}.Release|x64.Build.0 = Release|Any CPU + {877D7394-ACC5-482C-8DE5-0B0D0639D4BE}.Release|x86.ActiveCfg = Release|Any CPU + {877D7394-ACC5-482C-8DE5-0B0D0639D4BE}.Release|x86.Build.0 = Release|Any CPU {DEE62F36-0B35-45AC-A035-817D461DB0B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DEE62F36-0B35-45AC-A035-817D461DB0B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DEE62F36-0B35-45AC-A035-817D461DB0B6}.Debug|x64.ActiveCfg = Debug|Any CPU + {DEE62F36-0B35-45AC-A035-817D461DB0B6}.Debug|x64.Build.0 = Debug|Any CPU + {DEE62F36-0B35-45AC-A035-817D461DB0B6}.Debug|x86.ActiveCfg = Debug|Any CPU + {DEE62F36-0B35-45AC-A035-817D461DB0B6}.Debug|x86.Build.0 = Debug|Any CPU {DEE62F36-0B35-45AC-A035-817D461DB0B6}.Release|Any CPU.ActiveCfg = Release|Any CPU {DEE62F36-0B35-45AC-A035-817D461DB0B6}.Release|Any CPU.Build.0 = Release|Any CPU + {DEE62F36-0B35-45AC-A035-817D461DB0B6}.Release|x64.ActiveCfg = Release|Any CPU + {DEE62F36-0B35-45AC-A035-817D461DB0B6}.Release|x64.Build.0 = Release|Any CPU + {DEE62F36-0B35-45AC-A035-817D461DB0B6}.Release|x86.ActiveCfg = Release|Any CPU + {DEE62F36-0B35-45AC-A035-817D461DB0B6}.Release|x86.Build.0 = Release|Any CPU {58412551-7DCB-44AF-95F5-991FD44C51C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {58412551-7DCB-44AF-95F5-991FD44C51C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58412551-7DCB-44AF-95F5-991FD44C51C6}.Debug|x64.ActiveCfg = Debug|Any CPU + {58412551-7DCB-44AF-95F5-991FD44C51C6}.Debug|x64.Build.0 = Debug|Any CPU + {58412551-7DCB-44AF-95F5-991FD44C51C6}.Debug|x86.ActiveCfg = Debug|Any CPU + {58412551-7DCB-44AF-95F5-991FD44C51C6}.Debug|x86.Build.0 = Debug|Any CPU {58412551-7DCB-44AF-95F5-991FD44C51C6}.Release|Any CPU.ActiveCfg = Release|Any CPU {58412551-7DCB-44AF-95F5-991FD44C51C6}.Release|Any CPU.Build.0 = Release|Any CPU + {58412551-7DCB-44AF-95F5-991FD44C51C6}.Release|x64.ActiveCfg = Release|Any CPU + {58412551-7DCB-44AF-95F5-991FD44C51C6}.Release|x64.Build.0 = Release|Any CPU + {58412551-7DCB-44AF-95F5-991FD44C51C6}.Release|x86.ActiveCfg = Release|Any CPU + {58412551-7DCB-44AF-95F5-991FD44C51C6}.Release|x86.Build.0 = Release|Any CPU {BB2BE362-BB25-45A5-AE5A-B223EAB80CC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BB2BE362-BB25-45A5-AE5A-B223EAB80CC0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB2BE362-BB25-45A5-AE5A-B223EAB80CC0}.Debug|x64.ActiveCfg = Debug|Any CPU + {BB2BE362-BB25-45A5-AE5A-B223EAB80CC0}.Debug|x64.Build.0 = Debug|Any CPU + {BB2BE362-BB25-45A5-AE5A-B223EAB80CC0}.Debug|x86.ActiveCfg = Debug|Any CPU + {BB2BE362-BB25-45A5-AE5A-B223EAB80CC0}.Debug|x86.Build.0 = Debug|Any CPU {BB2BE362-BB25-45A5-AE5A-B223EAB80CC0}.Release|Any CPU.ActiveCfg = Release|Any CPU {BB2BE362-BB25-45A5-AE5A-B223EAB80CC0}.Release|Any CPU.Build.0 = Release|Any CPU + {BB2BE362-BB25-45A5-AE5A-B223EAB80CC0}.Release|x64.ActiveCfg = Release|Any CPU + {BB2BE362-BB25-45A5-AE5A-B223EAB80CC0}.Release|x64.Build.0 = Release|Any CPU + {BB2BE362-BB25-45A5-AE5A-B223EAB80CC0}.Release|x86.ActiveCfg = Release|Any CPU + {BB2BE362-BB25-45A5-AE5A-B223EAB80CC0}.Release|x86.Build.0 = Release|Any CPU {2A3CEEB9-32A4-4F6F-BB3B-D786B92A7212}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2A3CEEB9-32A4-4F6F-BB3B-D786B92A7212}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A3CEEB9-32A4-4F6F-BB3B-D786B92A7212}.Debug|x64.ActiveCfg = Debug|Any CPU + {2A3CEEB9-32A4-4F6F-BB3B-D786B92A7212}.Debug|x64.Build.0 = Debug|Any CPU + {2A3CEEB9-32A4-4F6F-BB3B-D786B92A7212}.Debug|x86.ActiveCfg = Debug|Any CPU + {2A3CEEB9-32A4-4F6F-BB3B-D786B92A7212}.Debug|x86.Build.0 = Debug|Any CPU {2A3CEEB9-32A4-4F6F-BB3B-D786B92A7212}.Release|Any CPU.ActiveCfg = Release|Any CPU {2A3CEEB9-32A4-4F6F-BB3B-D786B92A7212}.Release|Any CPU.Build.0 = Release|Any CPU + {2A3CEEB9-32A4-4F6F-BB3B-D786B92A7212}.Release|x64.ActiveCfg = Release|Any CPU + {2A3CEEB9-32A4-4F6F-BB3B-D786B92A7212}.Release|x64.Build.0 = Release|Any CPU + {2A3CEEB9-32A4-4F6F-BB3B-D786B92A7212}.Release|x86.ActiveCfg = Release|Any CPU + {2A3CEEB9-32A4-4F6F-BB3B-D786B92A7212}.Release|x86.Build.0 = Release|Any CPU {C78BFACC-0794-4164-A4E9-F5B7FBFD3010}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C78BFACC-0794-4164-A4E9-F5B7FBFD3010}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C78BFACC-0794-4164-A4E9-F5B7FBFD3010}.Debug|x64.ActiveCfg = Debug|Any CPU + {C78BFACC-0794-4164-A4E9-F5B7FBFD3010}.Debug|x64.Build.0 = Debug|Any CPU + {C78BFACC-0794-4164-A4E9-F5B7FBFD3010}.Debug|x86.ActiveCfg = Debug|Any CPU + {C78BFACC-0794-4164-A4E9-F5B7FBFD3010}.Debug|x86.Build.0 = Debug|Any CPU {C78BFACC-0794-4164-A4E9-F5B7FBFD3010}.Release|Any CPU.ActiveCfg = Release|Any CPU {C78BFACC-0794-4164-A4E9-F5B7FBFD3010}.Release|Any CPU.Build.0 = Release|Any CPU + {C78BFACC-0794-4164-A4E9-F5B7FBFD3010}.Release|x64.ActiveCfg = Release|Any CPU + {C78BFACC-0794-4164-A4E9-F5B7FBFD3010}.Release|x64.Build.0 = Release|Any CPU + {C78BFACC-0794-4164-A4E9-F5B7FBFD3010}.Release|x86.ActiveCfg = Release|Any CPU + {C78BFACC-0794-4164-A4E9-F5B7FBFD3010}.Release|x86.Build.0 = Release|Any CPU {CDA68A45-ABF9-46D6-ADED-628CC7CD1200}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CDA68A45-ABF9-46D6-ADED-628CC7CD1200}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CDA68A45-ABF9-46D6-ADED-628CC7CD1200}.Debug|x64.ActiveCfg = Debug|Any CPU + {CDA68A45-ABF9-46D6-ADED-628CC7CD1200}.Debug|x64.Build.0 = Debug|Any CPU + {CDA68A45-ABF9-46D6-ADED-628CC7CD1200}.Debug|x86.ActiveCfg = Debug|Any CPU + {CDA68A45-ABF9-46D6-ADED-628CC7CD1200}.Debug|x86.Build.0 = Debug|Any CPU {CDA68A45-ABF9-46D6-ADED-628CC7CD1200}.Release|Any CPU.ActiveCfg = Release|Any CPU {CDA68A45-ABF9-46D6-ADED-628CC7CD1200}.Release|Any CPU.Build.0 = Release|Any CPU + {CDA68A45-ABF9-46D6-ADED-628CC7CD1200}.Release|x64.ActiveCfg = Release|Any CPU + {CDA68A45-ABF9-46D6-ADED-628CC7CD1200}.Release|x64.Build.0 = Release|Any CPU + {CDA68A45-ABF9-46D6-ADED-628CC7CD1200}.Release|x86.ActiveCfg = Release|Any CPU + {CDA68A45-ABF9-46D6-ADED-628CC7CD1200}.Release|x86.Build.0 = Release|Any CPU {1474658D-E225-478E-80D6-D41A0376F88C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1474658D-E225-478E-80D6-D41A0376F88C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1474658D-E225-478E-80D6-D41A0376F88C}.Debug|x64.ActiveCfg = Debug|Any CPU + {1474658D-E225-478E-80D6-D41A0376F88C}.Debug|x64.Build.0 = Debug|Any CPU + {1474658D-E225-478E-80D6-D41A0376F88C}.Debug|x86.ActiveCfg = Debug|Any CPU + {1474658D-E225-478E-80D6-D41A0376F88C}.Debug|x86.Build.0 = Debug|Any CPU {1474658D-E225-478E-80D6-D41A0376F88C}.Release|Any CPU.ActiveCfg = Release|Any CPU {1474658D-E225-478E-80D6-D41A0376F88C}.Release|Any CPU.Build.0 = Release|Any CPU + {1474658D-E225-478E-80D6-D41A0376F88C}.Release|x64.ActiveCfg = Release|Any CPU + {1474658D-E225-478E-80D6-D41A0376F88C}.Release|x64.Build.0 = Release|Any CPU + {1474658D-E225-478E-80D6-D41A0376F88C}.Release|x86.ActiveCfg = Release|Any CPU + {1474658D-E225-478E-80D6-D41A0376F88C}.Release|x86.Build.0 = Release|Any CPU {5D7B6BD7-427E-4F1B-B4CA-CF6B3A3ED89F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5D7B6BD7-427E-4F1B-B4CA-CF6B3A3ED89F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D7B6BD7-427E-4F1B-B4CA-CF6B3A3ED89F}.Debug|x64.ActiveCfg = Debug|Any CPU + {5D7B6BD7-427E-4F1B-B4CA-CF6B3A3ED89F}.Debug|x64.Build.0 = Debug|Any CPU + {5D7B6BD7-427E-4F1B-B4CA-CF6B3A3ED89F}.Debug|x86.ActiveCfg = Debug|Any CPU + {5D7B6BD7-427E-4F1B-B4CA-CF6B3A3ED89F}.Debug|x86.Build.0 = Debug|Any CPU {5D7B6BD7-427E-4F1B-B4CA-CF6B3A3ED89F}.Release|Any CPU.ActiveCfg = Release|Any CPU {5D7B6BD7-427E-4F1B-B4CA-CF6B3A3ED89F}.Release|Any CPU.Build.0 = Release|Any CPU + {5D7B6BD7-427E-4F1B-B4CA-CF6B3A3ED89F}.Release|x64.ActiveCfg = Release|Any CPU + {5D7B6BD7-427E-4F1B-B4CA-CF6B3A3ED89F}.Release|x64.Build.0 = Release|Any CPU + {5D7B6BD7-427E-4F1B-B4CA-CF6B3A3ED89F}.Release|x86.ActiveCfg = Release|Any CPU + {5D7B6BD7-427E-4F1B-B4CA-CF6B3A3ED89F}.Release|x86.Build.0 = Release|Any CPU + {A2B58DFC-A4B0-4BD2-9552-322C6E87DA39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2B58DFC-A4B0-4BD2-9552-322C6E87DA39}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2B58DFC-A4B0-4BD2-9552-322C6E87DA39}.Debug|x64.ActiveCfg = Debug|Any CPU + {A2B58DFC-A4B0-4BD2-9552-322C6E87DA39}.Debug|x64.Build.0 = Debug|Any CPU + {A2B58DFC-A4B0-4BD2-9552-322C6E87DA39}.Debug|x86.ActiveCfg = Debug|Any CPU + {A2B58DFC-A4B0-4BD2-9552-322C6E87DA39}.Debug|x86.Build.0 = Debug|Any CPU + {A2B58DFC-A4B0-4BD2-9552-322C6E87DA39}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2B58DFC-A4B0-4BD2-9552-322C6E87DA39}.Release|Any CPU.Build.0 = Release|Any CPU + {A2B58DFC-A4B0-4BD2-9552-322C6E87DA39}.Release|x64.ActiveCfg = Release|Any CPU + {A2B58DFC-A4B0-4BD2-9552-322C6E87DA39}.Release|x64.Build.0 = Release|Any CPU + {A2B58DFC-A4B0-4BD2-9552-322C6E87DA39}.Release|x86.ActiveCfg = Release|Any CPU + {A2B58DFC-A4B0-4BD2-9552-322C6E87DA39}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/docs/architecture.md b/docs/architecture.md index ff9f16be..51336544 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -82,6 +82,31 @@ Custom adapter execution environment: - **Mapper Interface** (`IInfolinkMapper`) - **Receiver Interface** (`IInfolinkReceiver`) +## Scheduling Architecture + +Receiving and aggregation jobs run on a persistent, clustered schedule powered by **SW-Scheduler** — a typed Quartz.NET wrapper. + +``` +SchedulerSeedService (startup) + └── SubscriptionSchedulerService.ScheduleAll(sub) + └── IScheduleRepository.ScheduleIfNotExists(param, cron, key) + └── Quartz persistent job store (qrtz_* tables, same DB as Bitween) + +Quartz fires at scheduled time + └── ReceivingJob.Execute(ReceivingJobParams) ← polls receiver adapter, creates Xchanges + └── AggregationJob.Execute(AggregationJobParams) ← batches successful Xchanges into one +``` + +Key points: + +- Quartz state lives in `qrtz_*` tables in the same database as Bitween, added via EF Core migrations. +- `SubscriptionSchedulerService` is the single point where Bitween's `Schedule` domain entity is translated into Quartz triggers. Call `Sync()` on update, `ScheduleAll()` on create, `RunNow()` for immediate execution. +- `SchedulerSeedService` re-registers all active subscriptions on startup using `ScheduleIfNotExists` — safe to run against a persistent store without creating duplicates. +- `EnableClustering = true` ensures only one node fires each trigger across a horizontally scaled deployment. +- Execution history is recorded in `job_executions` via `AddSchedulerMonitoring()`. + +See [docs/scheduler.md](scheduler.md) for the full reference. + ## Processing Architecture ### Message Flow diff --git a/docs/scheduler.md b/docs/scheduler.md new file mode 100644 index 00000000..f7c50ce8 --- /dev/null +++ b/docs/scheduler.md @@ -0,0 +1,267 @@ +# Scheduler + +Bitween uses **SW-Scheduler** (`SimplyWorks.Scheduler.*`) as its job scheduling backbone. SW-Scheduler is a thin, opinionated wrapper around [Quartz.NET](https://www.quartz-scheduler.net/) that replaces Quartz's raw `IJob` / `ITrigger` APIs with typed C# records and attributes. + +--- + +## Why SW-Scheduler instead of raw Quartz + +Quartz.NET is powerful but verbose: you wire jobs through `IJobDetail`, pass runtime data via an untyped `JobDataMap`, and manage trigger keys manually. SW-Scheduler removes that boilerplate: + +| Raw Quartz | SW-Scheduler equivalent | +|---|---| +| `IJob.Execute(IJobExecutionContext)` | `IScheduledJob.Execute()` or `IScheduledJob.Execute(TParam)` | +| `JobDataMap` string dictionary | Typed `TParam` record, serialized/deserialized automatically | +| Trigger keys + `IScheduler.ScheduleJob(...)` | `IScheduleRepository.Schedule(param, cron, key)` | +| Quartz attributes on the class | `[Schedule]`, `[RetryConfig]`, `[ScheduleConfig]` | +| `IJobExecutionContext.Scheduler.Clustered` | `EnableClustering = true` on the provider options | +| Separate `JobStore` configuration | EF Core migration in the same DB as the app | + +The result is that Bitween's two background jobs are about 50 lines of plain C# each, with no Quartz types in their signatures. + +--- + +## NuGet packages + +SW-Scheduler is published on NuGet.org under the `SimplyWorks.Scheduler.*` prefix. The solution references them as follows: + +| NuGet package | Version | Referenced by | What it adds | +|---|---|---|---| +| `SimplyWorks.Scheduler.Sdk` | 8.1.1 | `SW.Bitween.Api` | `IScheduledJob`, `[ScheduleConfig]`, `IScheduleRepository` interfaces — no Quartz dependency | +| `SimplyWorks.Scheduler.EfCore` | 8.1.1 | `SW.Bitween.Web` | `AddSchedulerMonitoring()`, `job_executions` EF model | +| `SimplyWorks.Scheduler.PgSql` | 8.1.1 | `SW.Bitween.PgSql` | `AddPgSqlScheduler(...)`, `modelBuilder.UseSchedulerPostgreSql(schema)` | +| `SimplyWorks.Scheduler.SqlServer` | 8.1.1 | `SW.Bitween.MsSql` | `AddSqlServerScheduler(...)`, `modelBuilder.UseSchedulerSqlServer()` | +| `SimplyWorks.Scheduler.MySql` | 8.1.1 | `SW.Bitween.MySql` | `AddMySqlScheduler(...)`, `modelBuilder.UseSchedulerMySql()` | + +`SW.Bitween.Api` references only `SimplyWorks.Scheduler.Sdk` — it defines jobs and uses `IScheduleRepository` but has no dependency on Quartz itself. Each DB provider project references the matching provider package, which transitively brings in the full Quartz runtime. `SW.Bitween.Web` adds `SimplyWorks.Scheduler.EfCore` directly; the three provider packages reach it transitively through the DB provider project references. + +--- + +## The two Bitween jobs + +### `ReceivingJob` + +Polls a configured receiver adapter for new files and creates an inbound `Xchange` for each one. + +```csharp +public record ReceivingJobParams(int SubscriptionId, string? CronExpression); + +[ScheduleConfig(AllowConcurrentExecution = false, MisfireInstructions = MisfireInstructions.Skip)] +public class ReceivingJob( + BitweenDbContext dbContext, + RunFlagUpdater runFlagUpdater, + NativeAdapterDiscoveryService nativeAdapterDiscovery, + IServerlessService serverless, + XchangeService xchangeService, + ILogger logger) : IScheduledJob +{ + public async Task Execute(ReceivingJobParams jobParams) { ... } +} +``` + +- `AllowConcurrentExecution = false` — Quartz will not fire a second instance of this job for the same subscription while one is still running. +- `MisfireInstructions.Skip` — if the scheduler was down at the scheduled fire time, skip that execution rather than pile up missed runs. +- The `RunFlagUpdater` adds a DB-level guard (`is_running` on the subscription) so that even across a cluster restart, two nodes cannot run the same subscription's job simultaneously. + +### `AggregationJob` + +Collects successful `Xchange` records belonging to a source subscription, generates a single aggregation `Xchange` containing their file URLs, and marks each source Xchange as aggregated. + +```csharp +public record AggregationJobParams(int SubscriptionId, string? CronExpression); + +[ScheduleConfig(AllowConcurrentExecution = false, MisfireInstructions = MisfireInstructions.Skip)] +public class AggregationJob( + BitweenDbContext dbContext, + XchangeService xchangeService, + ILogger logger) : IScheduledJob +{ + public async Task Execute(AggregationJobParams jobParams) { ... } +} +``` + +The aggregation query selects Xchanges where `XchangeResult.Success = true` and no `XchangeAggregation` link exists yet, so each source Xchange is included in exactly one aggregation batch. + +--- + +## Schedules: from Subscription to Quartz trigger + +Scheduling is driven by the `Schedule` owned entity on each `Subscription`. A `Schedule` stores a `Recurrence` (Hourly / Daily / Weekly / Monthly) and a `TimeSpan On` that encodes the offset within the period. + +### Schedule → cron conversion + +`ScheduleToCronExtension.ToCronExpression()` converts a `Schedule` to a 6-field Quartz cron string: + +| Recurrence | Example `On` | Cron output | +|---|---|---| +| `Hourly` | `00:15:00` | `0 15 * * * ?` | +| `Daily` | `02:30:00` | `0 30 2 * * ?` | +| `Weekly` | `1.08:00:00` (Mon 08:00) | `0 0 8 ? * 2` | +| `Monthly` | `15.09:00:00` (15th 09:00) | `0 0 9 15 * ?` | + +Quartz uses 6-field syntax (`second minute hour day-of-month month day-of-week`); day-of-week is 1-based starting from Sunday. + +### Schedule key + +Each `(subscription, schedule)` pair maps to a deterministic Quartz schedule key: + +``` +receiver-{subscriptionId}-{recurrence}-{on.Ticks}-{backwards ? 1 : 0} +aggregator-{subscriptionId}-{recurrence}-{on.Ticks}-{backwards ? 1 : 0} +``` + +The key is stable across restarts, which is what makes `ScheduleIfNotExists` idempotent. + +--- + +## `SubscriptionSchedulerService` + +This scoped service is the bridge between the Bitween domain and Quartz. It wraps `IScheduleRepository` with subscription-aware logic: + +```csharp +// Registers all active schedules for a subscription (idempotent). +await subScheduler.ScheduleAll(sub); + +// Syncs after an update — removes old triggers, adds new ones. +// Pass oldSchedules captured BEFORE calling sub.SetSchedules(...). +await subScheduler.Sync(sub, oldSchedules); + +// Triggers one immediate execution outside the cron cadence. +await subScheduler.RunNow(sub); +``` + +`Sync` is called from the subscription update command handler whenever a subscription's schedules change or its active/inactive state is toggled. + +--- + +## `SchedulerSeedService` + +A `BackgroundService` that runs once on startup and registers all active Receiving and Aggregation subscriptions with Quartz. It uses `ScheduleIfNotExists` so that restarting a node against a persistent Quartz store never creates duplicate triggers: + +``` +startup + └── SchedulerSeedService.ExecuteAsync() + ├── query all active Receiving + Aggregation subscriptions with at least one Schedule + └── for each: SubscriptionSchedulerService.ScheduleAll(sub) + └── IScheduleRepository.ScheduleIfNotExists(param, cron, key) +``` + +--- + +## Quartz tables in the database + +Quartz stores its own state (job definitions, triggers, calendar data, cluster locks) in a set of `qrtz_*` tables. In Bitween these tables live **in the same database as the application** under the same schema, added via normal EF Core migrations. + +Each DB provider calls the matching extension in `OnModelCreating`: + +```csharp +// SW.Bitween.PgSql/BitweenDbContext.cs +modelBuilder.UseSchedulerPostgreSql(Schema); // all qrtz_* + job_executions + +// SW.Bitween.MySql/BitweenDbContext.cs +modelBuilder.UseSchedulerMySql(); + +// SW.Bitween.MsSql/BitweenDbContext.cs +modelBuilder.UseSchedulerSqlServer(); +``` + +The Quartz tables were added in migration `Quartz` (generated 2026-06-14) in each provider project. Apply like any other migration: + +```bash +# PostgreSQL +dotnet ef database update --project SW.Bitween.PgSql + +# MySQL +dotnet ef database update --project SW.Bitween.MySql + +# SQL Server +dotnet ef database update --project SW.Bitween.MsSql +``` + +Tables added by the migration: + +| Table | Purpose | +|---|---| +| `qrtz_job_details` | Registered job types and their serialized data maps | +| `qrtz_triggers` | All triggers (base row for every trigger type) | +| `qrtz_cron_triggers` | Cron expression per cron trigger | +| `qrtz_simple_triggers` | Interval/count for simple triggers (one-off runs) | +| `qrtz_simprop_triggers` | Property bag for calendar interval and daily triggers | +| `qrtz_blob_triggers` | Fallback for non-standard trigger types | +| `qrtz_fired_triggers` | Currently executing or recently fired triggers | +| `qrtz_scheduler_state` | Heartbeat rows per cluster node | +| `qrtz_locks` | Pessimistic row-level locks used by the clustering algorithm | +| `qrtz_paused_trigger_grps` | Paused trigger group names | +| `qrtz_calendars` | Named calendars for blackout dates | +| `job_executions` | SW-Scheduler execution history (not a Quartz table) | + +--- + +## Job execution monitoring + +`AddSchedulerMonitoring()` registers `IJobExecutionStore` backed by the application's `BitweenDbContext`. After each job run, SW-Scheduler writes a row to `job_executions`: + +| Column | Description | +|---|---| +| `job_name` / `job_group` | Quartz job identity | +| `fire_instance_id` | Unique per execution; cluster-safe | +| `start_time_utc` / `end_time_utc` / `duration_ms` | Timing | +| `success` / `error` | Outcome | +| `node` | `Environment.MachineName` of the executing node | +| `context` | JSON blob — contains `JobParameter` (the serialized `ReceivingJobParams` / `AggregationJobParams`) | + +--- + +## Host registration (`Startup.cs`) + +The scheduler is registered in the provider-specific block of `Startup.ConfigureServices`, using the same connection string as the application database: + +```csharp +// PostgreSQL +services.AddPgSqlScheduler( + connectionString: connectionString, + schema: PgSql.BitweenDbContext.Schema, + configure: o => o.EnableClustering = true, + assemblies: typeof(BitweenDbContext).Assembly); + +// SQL Server +services.AddSqlServerScheduler( + connectionString: connectionString, + configure: o => o.EnableClustering = true, + assemblies: typeof(BitweenDbContext).Assembly); + +// MySQL +services.AddMySqlScheduler( + connectionString: connectionString, + configure: o => o.EnableClustering = true, + assemblies: typeof(BitweenDbContext).Assembly); + +// Shared — regardless of provider +services.AddScoped(); +services.AddHostedService(); +services.AddSchedulerMonitoring(); +``` + +`EnableClustering = true` means each node acquires a DB lock before firing a trigger, preventing duplicate execution in a horizontally scaled deployment. + +--- + +## Adding a new scheduled job type + +1. Add a `record` for the parameters in `SW.Bitween.Api`: + ```csharp + public record MyJobParams(int SubscriptionId, string? CronExpression); + ``` + +2. Implement `IScheduledJob` in the same project: + ```csharp + [ScheduleConfig(AllowConcurrentExecution = false, MisfireInstructions = MisfireInstructions.Skip)] + public class MyJob(BitweenDbContext db, ...) : IScheduledJob + { + public async Task Execute(MyJobParams p) { ... } + } + ``` + +3. In `SubscriptionSchedulerService`, add a branch to `Schedule` / `TryUnschedule` / `RunNow` for the new subscription type. + +4. No Quartz plumbing needed — discovery is automatic because `assemblies: typeof(BitweenDbContext).Assembly` is passed to the provider registration.