From 8ba2910d08f483dff92f823dac87751cd7dc3f60 Mon Sep 17 00:00:00 2001 From: samerz Date: Wed, 11 Feb 2026 14:21:31 +0300 Subject: [PATCH 1/2] native adapters --- .../Resources/Adapters/GetProperties.cs | 15 +- SW.Bitween.Api/Resources/Adapters/Search.cs | 13 +- .../Resources/Adapters/SearchVersioned.cs | 20 +- .../Resources/Subscriptions/Update.cs | 67 ++++- SW.Bitween.Api/SW.Bitween.Api.csproj | 1 + .../Services/NativeAdapterDiscoveryService.cs | 172 ++++++++++++ SW.Bitween.Api/Services/ReceivingService.cs | 106 ++++++- SW.Bitween.Api/Services/XchangeService.cs | 132 +++++++-- .../HttpHandler/HttpHandler.cs | 159 +++++++++++ .../HttpHandler/HttpHandlerInput.cs | 29 ++ .../HttpHandler/HttpHandlerModels.cs | 18 ++ .../SW.Bitween.NativeAdapters.csproj | 15 + .../Services/DictionaryConverter.cs | 157 +++++++++++ SW.Bitween.Web/DEVELOPMENT.md | 263 ++++++++++++++++++ SW.Bitween.Web/SW.Bitween.Web.csproj | 1 + SW.Bitween.Web/Startup.cs | 1 + SW.Bitween.sln | 6 + 17 files changed, 1126 insertions(+), 49 deletions(-) create mode 100644 SW.Bitween.Api/Services/NativeAdapterDiscoveryService.cs create mode 100644 SW.Bitween.NativeAdapters/HttpHandler/HttpHandler.cs create mode 100644 SW.Bitween.NativeAdapters/HttpHandler/HttpHandlerInput.cs create mode 100644 SW.Bitween.NativeAdapters/HttpHandler/HttpHandlerModels.cs create mode 100644 SW.Bitween.NativeAdapters/SW.Bitween.NativeAdapters.csproj create mode 100644 SW.Bitween.NativeAdapters/Services/DictionaryConverter.cs create mode 100644 SW.Bitween.Web/DEVELOPMENT.md diff --git a/SW.Bitween.Api/Resources/Adapters/GetProperties.cs b/SW.Bitween.Api/Resources/Adapters/GetProperties.cs index 4974470e..67bb6312 100644 --- a/SW.Bitween.Api/Resources/Adapters/GetProperties.cs +++ b/SW.Bitween.Api/Resources/Adapters/GetProperties.cs @@ -12,15 +12,26 @@ namespace SW.Bitween.Resources.Adapters public class GetProperties : IGetHandler { private readonly IServerlessService serverless; + private readonly NativeAdapterDiscoveryService _nativeAdapterDiscovery; - public GetProperties(IServerlessService serverless) + public GetProperties(IServerlessService serverless, NativeAdapterDiscoveryService nativeAdapterDiscovery) { this.serverless = serverless; + _nativeAdapterDiscovery = nativeAdapterDiscovery; } async public Task Handle(string key) { - await serverless.StartAsync( Uri.UnescapeDataString(key), null); + var decodedKey = Uri.UnescapeDataString(key); + + // Check if it's a native adapter + if (decodedKey.StartsWith("native.", StringComparison.OrdinalIgnoreCase)) + { + return _nativeAdapterDiscovery.GetNativeAdapterProperties(decodedKey); + } + + // Handle serverless adapters + await serverless.StartAsync(decodedKey, null); var expected = await serverless.GetExpectedStartupValues(); return expected.ToList().ToDictionary(k => k.Key, v => $"{v.Key} {(v.Value.Optional ? $" ({v.Value.Default ?? "null"})" : " *")}"); } diff --git a/SW.Bitween.Api/Resources/Adapters/Search.cs b/SW.Bitween.Api/Resources/Adapters/Search.cs index 108afbe9..ee84d76a 100644 --- a/SW.Bitween.Api/Resources/Adapters/Search.cs +++ b/SW.Bitween.Api/Resources/Adapters/Search.cs @@ -10,17 +10,23 @@ public class Search : IQueryHandler { private readonly ServerlessOptions _serverlessOptions; private readonly ICloudFilesService _cloudFilesService; + private readonly NativeAdapterDiscoveryService _nativeAdapterDiscovery; - public Search(ServerlessOptions serverlessOptions, ICloudFilesService cloudFilesService) + public Search(ServerlessOptions serverlessOptions, ICloudFilesService cloudFilesService, + NativeAdapterDiscoveryService nativeAdapterDiscovery) { _serverlessOptions = serverlessOptions; _cloudFilesService = cloudFilesService; + _nativeAdapterDiscovery = nativeAdapterDiscovery; } public async Task Handle(AdapterSearchRequest request) { + // Get native adapters first + var nativeAdapters = _nativeAdapterDiscovery.GetNativeAdapters(request.Prefix).ToList(); + // Get external adapters from storage var cloudFilesList = (await _cloudFilesService.ListAsync( $"{_serverlessOptions.AdapterRemotePath}/infolink6.{request.Prefix}")) @@ -33,10 +39,13 @@ public async Task Handle(AdapterSearchRequest request) return key; }) + .Distinct() .ToList(); + // Combine native (first) and external adapters + var allAdapters = nativeAdapters.Concat(cloudFilesList); - return cloudFilesList.Distinct().ToDictionary(k => k, v => v); + return allAdapters.ToDictionary(k => k, v => v); } } } \ No newline at end of file diff --git a/SW.Bitween.Api/Resources/Adapters/SearchVersioned.cs b/SW.Bitween.Api/Resources/Adapters/SearchVersioned.cs index 3bb3d313..1f4528d4 100644 --- a/SW.Bitween.Api/Resources/Adapters/SearchVersioned.cs +++ b/SW.Bitween.Api/Resources/Adapters/SearchVersioned.cs @@ -12,11 +12,14 @@ public class SearchVersioned : IQueryHandler { private readonly ServerlessOptions _serverlessOptions; private readonly ICloudFilesService _cloudFilesService; + private readonly NativeAdapterDiscoveryService _nativeAdapterDiscovery; - public SearchVersioned(ServerlessOptions serverlessOptions, ICloudFilesService cloudFilesService) + public SearchVersioned(ServerlessOptions serverlessOptions, ICloudFilesService cloudFilesService, + NativeAdapterDiscoveryService nativeAdapterDiscovery) { _serverlessOptions = serverlessOptions; _cloudFilesService = cloudFilesService; + _nativeAdapterDiscovery = nativeAdapterDiscovery; } @@ -24,6 +27,16 @@ public async Task Handle(AdapterSearchRequest request) { var index = _serverlessOptions.AdapterRemotePath.Length + 1; + // Get native adapters first (they don't have versions) + var nativeAdapters = _nativeAdapterDiscovery.GetNativeAdapters(request.Prefix) + .Select(key => new + { + Key = key, + Versions = new List() // Native adapters have no versions + }) + .ToList(); + + // Get external adapters from storage var cloudFilesList = (await _cloudFilesService.ListAsync( $"{_serverlessOptions.AdapterRemotePath}/infolink6.{request.Prefix}")) @@ -40,7 +53,7 @@ public async Task Handle(AdapterSearchRequest request) return key; }); - return grouped.Select(i => new + var externalAdapters = grouped.Select(i => new { i.Key, Versions = i.Where(v => v.Key != i.Key && Semver.IsVersionNumber(v.Key.Split("/").Last())) @@ -49,6 +62,9 @@ public async Task Handle(AdapterSearchRequest request) Key = v.Key[index..] }).ToList() }); + + // Return native adapters first, then external + return nativeAdapters.Concat(externalAdapters); } } } \ No newline at end of file diff --git a/SW.Bitween.Api/Resources/Subscriptions/Update.cs b/SW.Bitween.Api/Resources/Subscriptions/Update.cs index f918b028..ad0b3f83 100644 --- a/SW.Bitween.Api/Resources/Subscriptions/Update.cs +++ b/SW.Bitween.Api/Resources/Subscriptions/Update.cs @@ -102,10 +102,24 @@ public Validate(IServiceProvider serviceProvider) { RuleFor(i => i.MapperProperties).CustomAsync(async (i, context, ct) => { - var serverless = serviceProvider.GetService(); - await serverless.StartAsync(((SubscriptionUpdate)context.InstanceToValidate).MapperId, null); - var mustProps = (await serverless.GetExpectedStartupValues()) - .Where(p => p.Value.Optional == false).Select(p => p.Key); + var mapperId = ((SubscriptionUpdate)context.InstanceToValidate).MapperId; + var mustProps = Enumerable.Empty(); + + // Check if it's a native adapter + if (mapperId.StartsWith("native.", StringComparison.OrdinalIgnoreCase)) + { + var nativeAdapterDiscovery = serviceProvider.GetService(); + var properties = nativeAdapterDiscovery.GetNativeAdapterProperties(mapperId); + mustProps = properties.Where(p => p.Value.EndsWith(" *")).Select(p => p.Key); + } + else + { + var serverless = serviceProvider.GetService(); + await serverless.StartAsync(mapperId, null); + mustProps = (await serverless.GetExpectedStartupValues()) + .Where(p => p.Value.Optional == false).Select(p => p.Key); + } + var missing = mustProps.ToHashSet(StringComparer.OrdinalIgnoreCase) .Except(i.Where(p => !string.IsNullOrEmpty(p.Value)).Select(p => p.Key)); if (missing.Any()) @@ -117,10 +131,24 @@ public Validate(IServiceProvider serviceProvider) { RuleFor(i => i.HandlerProperties).CustomAsync(async (i, context, ct) => { - var serverless = serviceProvider.GetService(); - await serverless.StartAsync(((SubscriptionUpdate)context.InstanceToValidate).HandlerId, null); - var mustProps = (await serverless.GetExpectedStartupValues()) - .Where(p => p.Value.Optional == false).Select(p => p.Key); + var handlerId = ((SubscriptionUpdate)context.InstanceToValidate).HandlerId; + var mustProps = Enumerable.Empty(); + + // Check if it's a native adapter + if (handlerId.StartsWith("native.", StringComparison.OrdinalIgnoreCase)) + { + var nativeAdapterDiscovery = serviceProvider.GetService(); + var properties = nativeAdapterDiscovery.GetNativeAdapterProperties(handlerId); + mustProps = properties.Where(p => p.Value.EndsWith(" *")).Select(p => p.Key); + } + else + { + var serverless = serviceProvider.GetService(); + await serverless.StartAsync(handlerId, null); + mustProps = (await serverless.GetExpectedStartupValues()) + .Where(p => p.Value.Optional == false).Select(p => p.Key); + } + var missing = mustProps.ToHashSet(StringComparer.OrdinalIgnoreCase) .Except(i.Where(p => !string.IsNullOrEmpty(p.Value)).Select(p => p.Key)); if (missing.Any()) @@ -137,11 +165,24 @@ public Validate(IServiceProvider serviceProvider) { RuleFor(i => i.ReceiverProperties).CustomAsync(async (i, context, ct) => { - var serverless = serviceProvider.GetService(); - await serverless.StartAsync(((SubscriptionUpdate)context.InstanceToValidate).ReceiverId, - null); - var mustProps = (await serverless.GetExpectedStartupValues()) - .Where(p => p.Value.Optional == false).Select(p => p.Key); + var receiverId = ((SubscriptionUpdate)context.InstanceToValidate).ReceiverId; + var mustProps = Enumerable.Empty(); + + // Check if it's a native adapter + if (receiverId.StartsWith("native.", StringComparison.OrdinalIgnoreCase)) + { + var nativeAdapterDiscovery = serviceProvider.GetService(); + var properties = nativeAdapterDiscovery.GetNativeAdapterProperties(receiverId); + mustProps = properties.Where(p => p.Value.EndsWith(" *")).Select(p => p.Key); + } + else + { + var serverless = serviceProvider.GetService(); + await serverless.StartAsync(receiverId, null); + mustProps = (await serverless.GetExpectedStartupValues()) + .Where(p => p.Value.Optional == false).Select(p => p.Key); + } + var missing = mustProps.ToHashSet(StringComparer.OrdinalIgnoreCase) .Except(i.Where(p => !string.IsNullOrEmpty(p.Value)).Select(p => p.Key)); if (missing.Any()) diff --git a/SW.Bitween.Api/SW.Bitween.Api.csproj b/SW.Bitween.Api/SW.Bitween.Api.csproj index 5694d925..98448c29 100644 --- a/SW.Bitween.Api/SW.Bitween.Api.csproj +++ b/SW.Bitween.Api/SW.Bitween.Api.csproj @@ -27,6 +27,7 @@ + diff --git a/SW.Bitween.Api/Services/NativeAdapterDiscoveryService.cs b/SW.Bitween.Api/Services/NativeAdapterDiscoveryService.cs new file mode 100644 index 00000000..6bf15119 --- /dev/null +++ b/SW.Bitween.Api/Services/NativeAdapterDiscoveryService.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using SW.Bitween.NativeAdapters; +using SW.PrimitiveTypes; + +namespace SW.Bitween +{ + public class NativeAdapterDiscoveryService + { + private readonly Dictionary> _adaptersCache; + + public NativeAdapterDiscoveryService() + { + _adaptersCache = new Dictionary>(); + DiscoverNativeAdapters(); + } + + private void DiscoverNativeAdapters() + { + var assemblies = new List() { typeof(DictionaryConverter).Assembly }; + + + + foreach (var assembly in assemblies) + { + try + { + var types = assembly.GetTypes() + .Where(t => t.IsClass && !t.IsAbstract); + + foreach (var type in types) + { + if (typeof(IInfolinkHandler).IsAssignableFrom(type)) + { + AddAdapter("handlers", type); + } + else if (typeof(IInfolinkValidator).IsAssignableFrom(type)) + { + AddAdapter("validators", type); + } + else if (typeof(IInfolinkReceiver).IsAssignableFrom(type)) + { + AddAdapter("receivers", type); + } + } + } + catch + { + // Skip assemblies that can't be loaded or scanned + } + } + } + + private void AddAdapter(string category, Type type) + { + if (!_adaptersCache.ContainsKey(category)) + { + _adaptersCache[category] = new List(); + } + + var adapterName = type.Name;//.Replace("Handler", "").Replace("Mapper", "") + //.Replace("Validator", "").Replace("Receiver", "").ToLower(); + + _adaptersCache[category].Add(new NativeAdapterInfo + { + Key = $"native.{adapterName}", + Name = type.Name, + Type = type, + Category = category + }); + } + + public IEnumerable GetNativeAdapters(string prefix) + { + if (string.IsNullOrEmpty(prefix)) + { + return _adaptersCache.Values.SelectMany(v => v).Select(a => a.Key); + } + + var category = prefix.ToLower().TrimStart('.'); + + if (_adaptersCache.TryGetValue(category, out var adapters)) + { + return adapters.Select(a => a.Key); + } + + return Enumerable.Empty(); + } + + public NativeAdapterInfo GetNativeAdapterInfo(string adapterId) + { + return _adaptersCache.Values + .SelectMany(v => v) + .FirstOrDefault(a => a.Key.Equals(adapterId, StringComparison.OrdinalIgnoreCase)); + } + + public Dictionary GetNativeAdapterProperties(string adapterId) + { + var adapterInfo = GetNativeAdapterInfo(adapterId); + if (adapterInfo == null) + return new Dictionary(); + + var result = new Dictionary(); + + // Get constructor parameters + var constructor = adapterInfo.Type.GetConstructors() + .FirstOrDefault(c => c.GetParameters().Length > 0); + + if (constructor == null) + return result; + + // Get the first parameter type (input model) + var inputParameter = constructor.GetParameters().FirstOrDefault(); + if (inputParameter == null) + return result; + + var inputType = inputParameter.ParameterType; + + // Get all properties from the input model + var properties = inputType.GetProperties(BindingFlags.Public | BindingFlags.Instance); + + foreach (var prop in properties) + { + var defaultValue = GetDefaultValue(prop); + var hasRequiredAttribute = prop.GetCustomAttribute() != null; + var isRequired = hasRequiredAttribute || (!IsNullableType(prop.PropertyType) && defaultValue == null); + + if (isRequired) + { + result[prop.Name] = $"{prop.Name} *"; + } + else + { + result[prop.Name] = $"{prop.Name} ({defaultValue ?? "null"})"; + } + } + + return result; + } + + private string? GetDefaultValue(PropertyInfo property) + { + // Try to get default value from DefaultValueAttribute if it exists + var defaultAttr = property.GetCustomAttribute(); + if (defaultAttr != null) + return defaultAttr.Value?.ToString(); + + // For value types, return their default + if (property.PropertyType.IsValueType) + return Activator.CreateInstance(property.PropertyType)?.ToString(); + + return null; + } + + private bool IsNullableType(Type type) + { + return !type.IsValueType || + Nullable.GetUnderlyingType(type) != null || + (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)); + } + } + + public class NativeAdapterInfo + { + public string Key { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public Type Type { get; set; } = null!; + public string Category { get; set; } = string.Empty; + } +} diff --git a/SW.Bitween.Api/Services/ReceivingService.cs b/SW.Bitween.Api/Services/ReceivingService.cs index ab0bb40b..684001aa 100644 --- a/SW.Bitween.Api/Services/ReceivingService.cs +++ b/SW.Bitween.Api/Services/ReceivingService.cs @@ -78,26 +78,106 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) async Task RunReceiver(IServiceProvider serviceProvider, string serverlessId, IDictionary startupParameters, int subId) { - 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(); + // Check if it's a native adapter + if (serverlessId.StartsWith("native.", StringComparison.OrdinalIgnoreCase)) + { + var nativeAdapterDiscovery = serviceProvider.GetRequiredService(); + var receiver = InstantiateNativeReceiver(nativeAdapterDiscovery, 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($"Subscription:'{subId}' found {fileList.Count()} items for retrieval."); + logger.LogInformation($"Submitting received file for subscriber: '{subId}'."); - foreach (var file in fileList) + var xchangeService = serviceProvider.GetService(); + await xchangeService.SubmitSubscriptionXchange(subId, xchangeFile); + await receiver.DeleteFile(file); + } + + await receiver.Finalize(); + } + else { - var xchangeFile = await serverless.InvokeAsync(nameof(IInfolinkReceiver.GetFile), file); + // 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); + } + } + + private IInfolinkReceiver InstantiateNativeReceiver(NativeAdapterDiscoveryService nativeAdapterDiscovery, + string adapterId, IDictionary properties) + { + var adapterInfo = nativeAdapterDiscovery.GetNativeAdapterInfo(adapterId); + if (adapterInfo == null) + throw new BitweenException($"Native adapter not found: {adapterId}"); + + // Get the constructor that takes a parameter + var constructor = adapterInfo.Type.GetConstructors() + .FirstOrDefault(c => c.GetParameters().Length > 0); + + if (constructor == null) + throw new BitweenException($"Native adapter {adapterId} must have a constructor that accepts an input model"); + + // Get the input parameter type + var inputParameter = constructor.GetParameters().First(); + var inputType = inputParameter.ParameterType; - logger.LogInformation($"Submitting received file for subscriber: '{subId}'."); + // Create an instance of the input model by mapping properties + var inputInstance = Activator.CreateInstance(inputType); - var xchangeService = serviceProvider.GetService(); - await xchangeService.SubmitSubscriptionXchange(subId, xchangeFile); - await serverless.InvokeAsync(nameof(IInfolinkReceiver.DeleteFile), file); + // Map dictionary properties to the input model + foreach (var prop in inputType.GetProperties()) + { + // Case-insensitive property lookup + var propEntry = properties.FirstOrDefault(p => + string.Equals(p.Key, prop.Name, StringComparison.OrdinalIgnoreCase)); + + if (!string.IsNullOrEmpty(propEntry.Key)) + { + var value = propEntry.Value; + try + { + var convertedValue = Convert.ChangeType(value, + Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType); + prop.SetValue(inputInstance, convertedValue); + } + catch + { + // If conversion fails, set string value directly + if (prop.PropertyType == typeof(string)) + prop.SetValue(inputInstance, value); + } + } } - await serverless.InvokeAsync(nameof(IInfolinkReceiver.Finalize), null); + // Instantiate the adapter with the input model + var adapter = Activator.CreateInstance(adapterInfo.Type, inputInstance); + + return (IInfolinkReceiver)adapter; } diff --git a/SW.Bitween.Api/Services/XchangeService.cs b/SW.Bitween.Api/Services/XchangeService.cs index ecf5fc39..b0c058b5 100644 --- a/SW.Bitween.Api/Services/XchangeService.cs +++ b/SW.Bitween.Api/Services/XchangeService.cs @@ -33,16 +33,19 @@ public class XchangeService : private readonly IPublish _publish; private readonly ILogger _logger; private readonly IInfolinkCache _BitweenCache; + private readonly NativeAdapterDiscoveryService _nativeAdapterDiscovery; public XchangeService(BitweenOptions BitweenSettings, BitweenDbContext dbContext, FilterService filterService, ICloudFilesService cloudFiles, IServiceProvider serviceProvider, - IPublish publish, ILogger logger, IInfolinkCache BitweenCache) + IPublish publish, ILogger logger, IInfolinkCache BitweenCache, + NativeAdapterDiscoveryService nativeAdapterDiscovery) { _BitweenSettings = BitweenSettings; _dbContext = dbContext; _filterService = filterService; _cloudFiles = cloudFiles; + _nativeAdapterDiscovery = nativeAdapterDiscovery; _serviceProvider = serviceProvider; _publish = publish; _logger = logger; @@ -125,13 +128,23 @@ private async Task RunMapper(Xchange xchange, XchangeFile xchangeFi { if (xchange.MapperId == null) return xchangeFile; - var serverless = _serviceProvider.GetRequiredService(); - var mapperProperties = xchange.MapperProperties.ToDictionary(); mapperProperties["xchangeid"] = xchange.Id; - await serverless.StartAsync(xchange.MapperId, xchange.CorrelationId ?? xchange.Id, mapperProperties); - xchangeFile = await serverless.InvokeAsync(nameof(IInfolinkHandler.Handle), xchangeFile); + // Check if it's a native adapter + if (xchange.MapperId.StartsWith("native.", StringComparison.OrdinalIgnoreCase)) + { + var handler = InstantiateNativeAdapter(xchange.MapperId, mapperProperties); + xchangeFile = await handler.Handle(xchangeFile); + } + else + { + // Use serverless for external adapters + var serverless = _serviceProvider.GetRequiredService(); + await serverless.StartAsync(xchange.MapperId, xchange.CorrelationId ?? xchange.Id, mapperProperties); + xchangeFile = await serverless.InvokeAsync(nameof(IInfolinkHandler.Handle), xchangeFile); + } + if (xchangeFile is null) throw new BitweenException( $"Unexpected null return value after running mapping for exchange id: {xchange.Id}, adapter id: {xchange.MapperId}"); @@ -145,10 +158,22 @@ public async Task RunValidator(string validatorId, IDictionary p { if (validatorId == null) return; - var serverless = _serviceProvider.GetRequiredService(); - await serverless.StartAsync(validatorId, null, properties); - var result = - await serverless.InvokeAsync(nameof(IInfolinkValidator.Validate), xchangeFile); + InfolinkValidatorResult result; + + // Check if it's a native adapter + if (validatorId.StartsWith("native.", StringComparison.OrdinalIgnoreCase)) + { + var validator = InstantiateNativeAdapter(validatorId, properties); + result = await validator.Validate(xchangeFile); + } + else + { + // Use serverless for external adapters + var serverless = _serviceProvider.GetRequiredService(); + await serverless.StartAsync(validatorId, null, properties); + result = await serverless.InvokeAsync(nameof(IInfolinkValidator.Validate), xchangeFile); + } + if (!result.Success) throw new SWValidationException(result.Validations); } @@ -157,18 +182,79 @@ private async Task RunHandler(Xchange xchange, XchangeFile xchangeF { if (xchange.HandlerId == null) return null; - var serverless = _serviceProvider.GetRequiredService(); - var handlerProperties = xchange.HandlerProperties.ToDictionary(); handlerProperties["xchangeid"] = xchange.Id; - await serverless.StartAsync(xchange.HandlerId, xchange.CorrelationId ?? xchange.Id, handlerProperties); - xchangeFile = await serverless.InvokeAsync(nameof(IInfolinkHandler.Handle), xchangeFile); + // Check if it's a native adapter + if (xchange.HandlerId.StartsWith("native.", StringComparison.OrdinalIgnoreCase)) + { + var handler = InstantiateNativeAdapter(xchange.HandlerId, handlerProperties); + xchangeFile = await handler.Handle(xchangeFile); + } + else + { + // Use serverless for external adapters + var serverless = _serviceProvider.GetRequiredService(); + await serverless.StartAsync(xchange.HandlerId, xchange.CorrelationId ?? xchange.Id, handlerProperties); + xchangeFile = await serverless.InvokeAsync(nameof(IInfolinkHandler.Handle), xchangeFile); + } + if (xchangeFile != null) await AddFile(xchange.Id, XchangeFileType.Response, xchangeFile); return xchangeFile; } + private T InstantiateNativeAdapter(string adapterId, IDictionary properties) + { + var adapterInfo = _nativeAdapterDiscovery.GetNativeAdapterInfo(adapterId); + if (adapterInfo == null) + throw new BitweenException($"Native adapter not found: {adapterId}"); + + // Get the constructor that takes a parameter + var constructor = adapterInfo.Type.GetConstructors() + .FirstOrDefault(c => c.GetParameters().Length > 0); + + if (constructor == null) + throw new BitweenException($"Native adapter {adapterId} must have a constructor that accepts an input model"); + + // Get the input parameter type + var inputParameter = constructor.GetParameters().First(); + var inputType = inputParameter.ParameterType; + + // Create an instance of the input model by mapping properties + var inputInstance = Activator.CreateInstance(inputType); + + // Map dictionary properties to the input model + foreach (var prop in inputType.GetProperties()) + { + // Case-insensitive property lookup + var propEntry = properties.FirstOrDefault(p => + string.Equals(p.Key, prop.Name, StringComparison.OrdinalIgnoreCase)); + + if (!string.IsNullOrEmpty(propEntry.Key)) + { + var value = propEntry.Value; + try + { + var convertedValue = Convert.ChangeType(value, + Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType); + prop.SetValue(inputInstance, convertedValue); + } + catch + { + // If conversion fails, set string value directly + if (prop.PropertyType == typeof(string)) + prop.SetValue(inputInstance, value); + } + } + } + + // Instantiate the adapter with the input model + var adapter = Activator.CreateInstance(adapterInfo.Type, inputInstance); + + return (T)adapter; + } + private async Task AddFile(string xchangeId, XchangeFileType type, XchangeFile file) { await _cloudFiles.WriteTextAsync(file.Data, new WriteFileSettings @@ -354,16 +440,28 @@ private async Task NotifyResult(Notifier notifier, XchangeResult xchangeResult, CorrelationId = xchange.CorrelationId }; - var serverless = _serviceProvider.GetRequiredService(); + var handlerProperties = notifier.HandlerProperties.ToDictionary(); handlerProperties["xchangeid"] = xchangeResult.Id; try { - await serverless.StartAsync(notifier.HandlerId, correlationId, handlerProperties); - await serverless.InvokeAsync(nameof(IInfolinkHandler.Handle), - new XchangeFile(JsonConvert.SerializeObject(notificationData), xchangeResult.Id)); + // Check if it's a native adapter + if (notifier.HandlerId.StartsWith("native.", StringComparison.OrdinalIgnoreCase)) + { + var handler = InstantiateNativeAdapter(notifier.HandlerId, handlerProperties); + await handler.Handle(new XchangeFile(JsonConvert.SerializeObject(notificationData), xchangeResult.Id)); + } + else + { + + // Use serverless for external adapters + var serverless = _serviceProvider.GetRequiredService(); + await serverless.StartAsync(notifier.HandlerId, correlationId, handlerProperties); + await serverless.InvokeAsync(nameof(IInfolinkHandler.Handle), + new XchangeFile(JsonConvert.SerializeObject(notificationData), xchangeResult.Id)); + } _dbContext.Add(new XchangeNotification(xchangeResult.Id, notifier.Id, notifier.Name)); } diff --git a/SW.Bitween.NativeAdapters/HttpHandler/HttpHandler.cs b/SW.Bitween.NativeAdapters/HttpHandler/HttpHandler.cs new file mode 100644 index 00000000..8f6ae161 --- /dev/null +++ b/SW.Bitween.NativeAdapters/HttpHandler/HttpHandler.cs @@ -0,0 +1,159 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using DotLiquid; +using Newtonsoft.Json; +using SW.PrimitiveTypes; + +namespace SW.Bitween.NativeAdapters; + +public class HttpHandler : IInfolinkHandler +{ + private HttpMethod HttpMethodFromString(string method) + { + switch (method.ToLower()) + { + case "get": + return HttpMethod.Get; + case "delete": + return HttpMethod.Delete; + case "put": + return HttpMethod.Put; + default: + return HttpMethod.Post; + } + } + + private readonly HttpHandlerInput _options; + + public HttpHandler(HttpHandlerInput options) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public async Task Handle(XchangeFile xchangeFile) + { + HttpClient client = new HttpClient(); + if (_options.AuthType == "ApiKey") + client.DefaultRequestHeaders.Add("ApiKey", _options.ApiKey); + else if (_options.AuthType == "Bearer") + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", _options.LoginPassword); + else if (_options.AuthType == "Basic") + { + string credentials = + Convert.ToBase64String( + Encoding.ASCII.GetBytes(_options.LoginUsername + ":" + _options.LoginPassword)); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials); + } + else if (_options.AuthType == "Login") + { + string loginJson = JsonConvert.SerializeObject(new UserLoginModel() + { + Email = _options.LoginUsername, + Password = _options.LoginPassword + }); + HttpResponseMessage loginResponse = await client.PostAsync(new Uri(_options.LoginUrl!), + new StringContent(loginJson, Encoding.UTF8, "application/json")); + loginResponse.EnsureSuccessStatusCode(); + if (loginResponse.StatusCode != HttpStatusCode.OK) + throw new Exception(loginResponse.StatusCode.ToString()); + string rs = await loginResponse.Content.ReadAsStringAsync(); + LoginResponse? rsDeserialized = JsonConvert.DeserializeObject(rs); + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", rsDeserialized?.Jwt); + } + else if (_options.AuthType == "OAuth2") + { + var oathRequest = new HttpRequestMessage(HttpMethod.Post, _options.LoginUrl); + var oauthContentDictionary = new List>(); + oauthContentDictionary.Add(new("client_id", _options.ClientId!)); + oauthContentDictionary.Add(new("client_secret", _options.ClientSecret!)); + oauthContentDictionary.Add(new("grant_type", "client_credentials")); + var oauthContent = new FormUrlEncodedContent(oauthContentDictionary); + oathRequest.Content = oauthContent; + var oauthResponse = await client.SendAsync(oathRequest); + var res = await oauthResponse.Content.ReadAsStringAsync(); + var resDeserialized = JsonConvert.DeserializeObject(res); + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", resDeserialized?.access_token); + } + + string requestBody = xchangeFile.Data; + if (string.IsNullOrEmpty(requestBody)) + requestBody = _options.DefaultRequest ?? string.Empty; + string str = _options.ContentType.ToLower(); + HttpContent content; + MultipartFormDataContent multipartTmp; + byte[] fileContent; + switch (str) + { + case "application/x-www-form-urlencoded": + content = new FormUrlEncodedContent( + JsonConvert.DeserializeObject>(requestBody) + ?? new Dictionary()); + break; + case "multipart/form-data": + multipartTmp = new MultipartFormDataContent(); + fileContent = Encoding.UTF8.GetBytes(requestBody); + multipartTmp.Add(new ByteArrayContent(fileContent), "file", xchangeFile.Filename ?? "file"); + content = multipartTmp; + break; + case "application/json": + content = new StringContent(requestBody, Encoding.UTF8, "application/json"); + break; + default: + content = new StringContent(requestBody, Encoding.UTF8, _options.ContentType); + break; + } + + Uri uri; + if (!string.IsNullOrEmpty(xchangeFile.Data) && _options.Url.Contains("{{")) + { + Template parsedTemplate = Template.Parse(_options.Url); + IDictionary obj = + JsonConvert.DeserializeObject>(xchangeFile.Data, + new DictionaryConverter()) ?? new Dictionary(); + Hash jsonHash = Hash.FromDictionary(obj); + uri = new Uri(parsedTemplate.Render(jsonHash)); + } + else + uri = new Uri(_options.Url); + + HttpRequestMessage request = new HttpRequestMessage() + { + RequestUri = uri, + Method = HttpMethodFromString(_options.Verb), + Content = content + }; + string? headers1 = _options.Headers; + IEnumerable>? headers = headers1 != null + ? (headers1.Split(',')).Select((Func>)(h => + { + string[] strArray = h.Split(':'); + return new KeyValuePair(strArray[0], strArray[1]); + })) + : null; + if (headers != null) + { + foreach (KeyValuePair keyValuePair1 in headers) + { + KeyValuePair keyValuePair = keyValuePair1; + request.Headers.Add(keyValuePair.Key, keyValuePair.Value); + } + } + + if (!string.IsNullOrEmpty(_options.CorrelationId)) + request.Headers.Add("request-context-correlation-id", _options.CorrelationId); + HttpResponseMessage response = await client.SendAsync(request); + if (response.StatusCode < HttpStatusCode.OK || response.StatusCode >= HttpStatusCode.InternalServerError) + throw new Exception(response.StatusCode.ToString()); + string resp = await response.Content.ReadAsStringAsync(); + XchangeFile xchangeFile1 = response.StatusCode < HttpStatusCode.BadRequest + ? new XchangeFile(resp) + : new XchangeFile(resp, badData: true); + return xchangeFile1; + } + + +} \ No newline at end of file diff --git a/SW.Bitween.NativeAdapters/HttpHandler/HttpHandlerInput.cs b/SW.Bitween.NativeAdapters/HttpHandler/HttpHandlerInput.cs new file mode 100644 index 00000000..c69fc53b --- /dev/null +++ b/SW.Bitween.NativeAdapters/HttpHandler/HttpHandlerInput.cs @@ -0,0 +1,29 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace SW.Bitween.NativeAdapters; + +public class HttpHandlerInput +{ + public string? AuthType { get; set; } + public string? ApiKey { get; set; } + public string? LoginUrl { get; set; } + + [DefaultValue("post")] + public string Verb { get; set; } = "post"; + + public string? LoginUsername { get; set; } + public string? LoginPassword { get; set; } + + [Required] + public string Url { get; set; } = string.Empty; + + [DefaultValue("application/json")] + public string ContentType { get; set; } = "application/json"; + + public string? Headers { get; set; } + public string? CorrelationId { get; set; } + public string? ClientId { get; set; } + public string? ClientSecret { get; set; } + public string? DefaultRequest { get; set; } +} \ No newline at end of file diff --git a/SW.Bitween.NativeAdapters/HttpHandler/HttpHandlerModels.cs b/SW.Bitween.NativeAdapters/HttpHandler/HttpHandlerModels.cs new file mode 100644 index 00000000..623b5059 --- /dev/null +++ b/SW.Bitween.NativeAdapters/HttpHandler/HttpHandlerModels.cs @@ -0,0 +1,18 @@ +namespace SW.Bitween.NativeAdapters; + +public class UserLoginModel +{ + public string? Email { get; set; } + public string? Password { get; set; } +} + +public class LoginResponse +{ + public string? Jwt { get; set; } + public string? Refresh { get; set; } +} + +public class OAuth2Response +{ + public string? access_token { get; set; } +} diff --git a/SW.Bitween.NativeAdapters/SW.Bitween.NativeAdapters.csproj b/SW.Bitween.NativeAdapters/SW.Bitween.NativeAdapters.csproj new file mode 100644 index 00000000..f0d60c4b --- /dev/null +++ b/SW.Bitween.NativeAdapters/SW.Bitween.NativeAdapters.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + + diff --git a/SW.Bitween.NativeAdapters/Services/DictionaryConverter.cs b/SW.Bitween.NativeAdapters/Services/DictionaryConverter.cs new file mode 100644 index 00000000..28edd394 --- /dev/null +++ b/SW.Bitween.NativeAdapters/Services/DictionaryConverter.cs @@ -0,0 +1,157 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace SW.Bitween.NativeAdapters; + +public class DictionaryConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + this.WriteValue(writer, value); + } + + private void WriteValue(JsonWriter writer, object? value) + { + if (value == null) + { + writer.WriteNull(); + return; + } + + var t = JToken.FromObject(value); + switch (t.Type) + { + case JTokenType.Object: + this.WriteObject(writer, value); + break; + case JTokenType.Array: + this.WriteArray(writer, value); + break; + default: + writer.WriteValue(value); + break; + } + } + + private void WriteObject(JsonWriter writer, object value) + { + writer.WriteStartObject(); + var obj = value as IDictionary; + if (obj != null) + { + foreach (var kvp in obj) + { + writer.WritePropertyName(kvp.Key); + this.WriteValue(writer, kvp.Value); + } + } + writer.WriteEndObject(); + } + + private void WriteArray(JsonWriter writer, object value) + { + writer.WriteStartArray(); + var array = value as IEnumerable; + if (array != null) + { + foreach (var o in array) + { + this.WriteValue(writer, o); + } + } + writer.WriteEndArray(); + } + + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + return ReadValue(reader); + } + + private object? ReadValue(JsonReader reader) + { + while (reader.TokenType == JsonToken.Comment) + { + if (!reader.Read()) + throw new JsonSerializationException("Unexpected Token when converting IDictionary"); + } + + switch (reader.TokenType) + { + case JsonToken.StartObject: + return ReadObject(reader); + case JsonToken.StartArray: + return this.ReadArray(reader); + case JsonToken.Integer: + case JsonToken.Float: + case JsonToken.String: + case JsonToken.Boolean: + case JsonToken.Undefined: + case JsonToken.Null: + case JsonToken.Date: + case JsonToken.Bytes: + return reader.Value; + default: + throw new JsonSerializationException( + $"Unexpected token when converting IDictionary: {reader.TokenType}"); + } + } + + private object ReadArray(JsonReader reader) + { + IList list = new List(); + + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonToken.Comment: + break; + case JsonToken.EndArray: + return list; + default: + var v = ReadValue(reader); + if (v != null) + list.Add(v); + break; + } + } + + throw new JsonSerializationException("Unexpected end when reading IDictionary"); + } + + private object ReadObject(JsonReader reader) + { + var obj = new Dictionary(); + + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonToken.PropertyName: + var propertyName = reader.Value?.ToString(); + if (propertyName == null) break; + + if (!reader.Read()) + { + throw new JsonSerializationException("Unexpected end when reading IDictionary"); + } + + var v = ReadValue(reader); + if (v != null) + obj[propertyName] = v; + break; + case JsonToken.Comment: + break; + case JsonToken.EndObject: + return obj; + } + } + + throw new JsonSerializationException("Unexpected end when reading IDictionary"); + } + + public override bool CanConvert(Type objectType) + { + return typeof(IDictionary).IsAssignableFrom(objectType); + } +} diff --git a/SW.Bitween.Web/DEVELOPMENT.md b/SW.Bitween.Web/DEVELOPMENT.md new file mode 100644 index 00000000..95ada7ab --- /dev/null +++ b/SW.Bitween.Web/DEVELOPMENT.md @@ -0,0 +1,263 @@ +# Bitween Development Configuration Guide + +## Prerequisites + +Before running Bitween in development mode, ensure you have: + +### 1. **PostgreSQL Database** +```bash +# Using Docker +docker run --name bitween-postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=bitween_dev -p 5432:5432 -d postgres:15 + +# Or install locally on macOS +brew install postgresql@15 +brew services start postgresql@15 +createdb bitween_dev +``` + +### 2. **MinIO (S3-Compatible Storage)** +```bash +# Using Docker +docker run --name bitween-minio \ + -p 9000:9000 \ + -p 9001:9001 \ + -e MINIO_ROOT_USER=minioadmin \ + -e MINIO_ROOT_PASSWORD=minioadmin \ + -d minio/minio server /data --console-address ":9001" + +# Access MinIO Console at http://localhost:9001 +# Login: minioadmin / minioadmin +# Create bucket named: bitween-dev +``` + +### 3. **RabbitMQ (Message Bus)** +```bash +# Using Docker +docker run --name bitween-rabbitmq \ + -p 5672:5672 \ + -p 15672:15672 \ + -d rabbitmq:3-management + +# Access RabbitMQ Management UI at http://localhost:15672 +# Login: guest / guest +``` + +## Quick Start with Docker Compose + +Create a `docker-compose.yml` file in the root: + +```yaml +version: '3.8' + +services: + postgres: + image: postgres:15 + container_name: bitween-postgres + environment: + POSTGRES_DB: bitween_dev + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + minio: + image: minio/minio + container_name: bitween-minio + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + ports: + - "9000:9000" + - "9001:9001" + volumes: + - minio_data:/data + + rabbitmq: + image: rabbitmq:3-management + container_name: bitween-rabbitmq + ports: + - "5672:5672" + - "15672:15672" + volumes: + - rabbitmq_data:/var/lib/rabbitmq + +volumes: + postgres_data: + minio_data: + rabbitmq_data: +``` + +Start all services: +```bash +docker-compose up -d +``` + +## Configuration Sections Explained + +### **ConnectionStrings** +- **BitweenDb**: PostgreSQL connection string + - Format: `Host=localhost;Port=5432;Database=bitween_dev;Username=postgres;Password=postgres` + - Change `DatabaseType` in Bitween section if using MySQL or SQL Server + +### **Bitween Section** +Core platform configuration: +- **DatabaseType**: `PgSql` | `MySql` | `MsSql` +- **AdminCredentials**: Format `username:password` for initial admin user +- **StorageProvider**: `S3` | `AS` (Azure Storage) | `OC` (Oracle Cloud) +- **DocumentPrefix**: Path prefix for file storage (e.g., `bitween-dev/documents`) +- **QueuePrefix**: RabbitMQ queue prefix (e.g., `bitween-dev`) +- **JwtExpiryMinutes**: JWT token expiration (1440 = 24 hours) + +### **S3CloudFiles Section** +MinIO/S3 configuration for file storage: +- **ServiceUrl**: MinIO endpoint (http://localhost:9000 for local) +- **AccessKeyId**: MinIO access key (minioadmin) +- **SecretAccessKey**: MinIO secret key (minioadmin) +- **BucketName**: S3 bucket name (bitween-dev) +- **ForcePathStyle**: true for MinIO, false for AWS S3 + +### **Bus Section** +RabbitMQ message bus configuration: +- **ConnectionString**: `amqp://guest:guest@localhost:5672` +- For production, use proper credentials and connection pooling + +### **Serverless Section** +Adapter execution configuration: +- **AdapterRemotePath**: Path in S3 where external adapters are stored +- **CommandTimeout**: Timeout for adapter execution (300 seconds) + +### **JwtTokenParameters** +Authentication configuration: +- **Key**: Must be at least 32 characters +- **Issuer**: Token issuer identifier +- **Audience**: Token audience identifier + +## Database Migration + +After configuration, run database migrations: + +```bash +cd SW.Bitween.Web + +# For PostgreSQL +dotnet ef database update --context PgSql.BitweenDbContext + +# For MySQL +dotnet ef database update --context MySql.BitweenDbContext + +# For SQL Server +dotnet ef database update --context MsSql.BitweenDbContext +``` + +Or use the migration script: +```bash +./migratedb.sh +``` + +## Running Bitween + +```bash +cd SW.Bitween.Web +dotnet run +``` + +The API will be available at: +- HTTP: http://localhost:5000 +- HTTPS: https://localhost:5001 +- Swagger: http://localhost:5000/swagger + +## Default Admin Login + +After first run, log in with: +- **Username**: admin +- **Password**: Admin@123456 + +Change this immediately in production! + +## Troubleshooting + +### Port Already in Use +```bash +# Check what's using the port +lsof -i :5000 +# Kill the process +kill -9 +``` + +### Database Connection Failed +- Ensure PostgreSQL is running: `pg_isready` +- Check connection string in appsettings.Development.json +- Verify database exists: `psql -l` + +### MinIO Connection Failed +- Check MinIO is running: `curl http://localhost:9000/minio/health/live` +- Verify bucket exists via MinIO Console (http://localhost:9001) +- Check ForcePathStyle=true in configuration + +### RabbitMQ Connection Failed +- Check RabbitMQ is running: `curl http://localhost:15672` +- Verify guest user is enabled +- Check firewall/network settings + +## Environment-Specific Settings + +### For MySQL +```json +{ + "ConnectionStrings": { + "BitweenDb": "Server=localhost;Database=bitween_dev;Uid=root;Pwd=password;" + }, + "Bitween": { + "DatabaseType": "MySql" + } +} +``` + +### For SQL Server +```json +{ + "ConnectionStrings": { + "BitweenDb": "Server=localhost;Database=bitween_dev;User Id=sa;Password=YourStrong@Passw0rd;" + }, + "Bitween": { + "DatabaseType": "MsSql" + } +} +``` + +### For Azure Storage +```json +{ + "Bitween": { + "StorageProvider": "AS" + }, + "AzureBlobStorage": { + "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.net", + "ContainerName": "bitween-dev" + } +} +``` + +## Production Considerations + +1. **Change default passwords** in all services +2. **Use environment variables** for sensitive data +3. **Enable SSL/TLS** for all connections +4. **Set UseAzureManagedIdentity=true** when running in Azure +5. **Configure proper logging** (ElasticSearch URL in SWLogger section) +6. **Use production-grade message broker** settings +7. **Set AreXChangeFilesPrivate=true** for sensitive data +8. **Reduce BusDefaultQueuePrefetch** in high-load scenarios + +## Next Steps + +1. Start the UI project (Bitween-UI) +2. Create your first Document +3. Configure Partners +4. Set up Subscriptions +5. Deploy external adapters or use native adapters (native.http) + +For more details, see the [full documentation](../docs/getting-started.md). diff --git a/SW.Bitween.Web/SW.Bitween.Web.csproj b/SW.Bitween.Web/SW.Bitween.Web.csproj index 795a599f..b8f2030f 100644 --- a/SW.Bitween.Web/SW.Bitween.Web.csproj +++ b/SW.Bitween.Web/SW.Bitween.Web.csproj @@ -34,6 +34,7 @@ + diff --git a/SW.Bitween.Web/Startup.cs b/SW.Bitween.Web/Startup.cs index 7d62f424..1b136cfe 100644 --- a/SW.Bitween.Web/Startup.cs +++ b/SW.Bitween.Web/Startup.cs @@ -57,6 +57,7 @@ public void ConfigureServices(IServiceCollection services) services.AddMemoryCache(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddScoped(); services.AddHostedService(); diff --git a/SW.Bitween.sln b/SW.Bitween.sln index 2d76a9c1..0c82ab3e 100644 --- a/SW.Bitween.sln +++ b/SW.Bitween.sln @@ -27,6 +27,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SW.Bitween.SampleValidator" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SW.Bitween.PgSql", "SW.Bitween.PgSql\SW.Bitween.PgSql.csproj", "{1474658D-E225-478E-80D6-D41A0376F88C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SW.Bitween.NativeAdapters", "SW.Bitween.NativeAdapters\SW.Bitween.NativeAdapters.csproj", "{5D7B6BD7-427E-4F1B-B4CA-CF6B3A3ED89F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -73,6 +75,10 @@ Global {1474658D-E225-478E-80D6-D41A0376F88C}.Debug|Any CPU.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 + {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}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D7B6BD7-427E-4F1B-B4CA-CF6B3A3ED89F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From d9cec1fe4a492ecb47b0e783830dc3e7f450efac Mon Sep 17 00:00:00 2001 From: samerz Date: Wed, 11 Feb 2026 14:22:56 +0300 Subject: [PATCH 2/2] remove md --- SW.Bitween.Web/DEVELOPMENT.md | 263 ---------------------------------- 1 file changed, 263 deletions(-) delete mode 100644 SW.Bitween.Web/DEVELOPMENT.md diff --git a/SW.Bitween.Web/DEVELOPMENT.md b/SW.Bitween.Web/DEVELOPMENT.md deleted file mode 100644 index 95ada7ab..00000000 --- a/SW.Bitween.Web/DEVELOPMENT.md +++ /dev/null @@ -1,263 +0,0 @@ -# Bitween Development Configuration Guide - -## Prerequisites - -Before running Bitween in development mode, ensure you have: - -### 1. **PostgreSQL Database** -```bash -# Using Docker -docker run --name bitween-postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=bitween_dev -p 5432:5432 -d postgres:15 - -# Or install locally on macOS -brew install postgresql@15 -brew services start postgresql@15 -createdb bitween_dev -``` - -### 2. **MinIO (S3-Compatible Storage)** -```bash -# Using Docker -docker run --name bitween-minio \ - -p 9000:9000 \ - -p 9001:9001 \ - -e MINIO_ROOT_USER=minioadmin \ - -e MINIO_ROOT_PASSWORD=minioadmin \ - -d minio/minio server /data --console-address ":9001" - -# Access MinIO Console at http://localhost:9001 -# Login: minioadmin / minioadmin -# Create bucket named: bitween-dev -``` - -### 3. **RabbitMQ (Message Bus)** -```bash -# Using Docker -docker run --name bitween-rabbitmq \ - -p 5672:5672 \ - -p 15672:15672 \ - -d rabbitmq:3-management - -# Access RabbitMQ Management UI at http://localhost:15672 -# Login: guest / guest -``` - -## Quick Start with Docker Compose - -Create a `docker-compose.yml` file in the root: - -```yaml -version: '3.8' - -services: - postgres: - image: postgres:15 - container_name: bitween-postgres - environment: - POSTGRES_DB: bitween_dev - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - - minio: - image: minio/minio - container_name: bitween-minio - command: server /data --console-address ":9001" - environment: - MINIO_ROOT_USER: minioadmin - MINIO_ROOT_PASSWORD: minioadmin - ports: - - "9000:9000" - - "9001:9001" - volumes: - - minio_data:/data - - rabbitmq: - image: rabbitmq:3-management - container_name: bitween-rabbitmq - ports: - - "5672:5672" - - "15672:15672" - volumes: - - rabbitmq_data:/var/lib/rabbitmq - -volumes: - postgres_data: - minio_data: - rabbitmq_data: -``` - -Start all services: -```bash -docker-compose up -d -``` - -## Configuration Sections Explained - -### **ConnectionStrings** -- **BitweenDb**: PostgreSQL connection string - - Format: `Host=localhost;Port=5432;Database=bitween_dev;Username=postgres;Password=postgres` - - Change `DatabaseType` in Bitween section if using MySQL or SQL Server - -### **Bitween Section** -Core platform configuration: -- **DatabaseType**: `PgSql` | `MySql` | `MsSql` -- **AdminCredentials**: Format `username:password` for initial admin user -- **StorageProvider**: `S3` | `AS` (Azure Storage) | `OC` (Oracle Cloud) -- **DocumentPrefix**: Path prefix for file storage (e.g., `bitween-dev/documents`) -- **QueuePrefix**: RabbitMQ queue prefix (e.g., `bitween-dev`) -- **JwtExpiryMinutes**: JWT token expiration (1440 = 24 hours) - -### **S3CloudFiles Section** -MinIO/S3 configuration for file storage: -- **ServiceUrl**: MinIO endpoint (http://localhost:9000 for local) -- **AccessKeyId**: MinIO access key (minioadmin) -- **SecretAccessKey**: MinIO secret key (minioadmin) -- **BucketName**: S3 bucket name (bitween-dev) -- **ForcePathStyle**: true for MinIO, false for AWS S3 - -### **Bus Section** -RabbitMQ message bus configuration: -- **ConnectionString**: `amqp://guest:guest@localhost:5672` -- For production, use proper credentials and connection pooling - -### **Serverless Section** -Adapter execution configuration: -- **AdapterRemotePath**: Path in S3 where external adapters are stored -- **CommandTimeout**: Timeout for adapter execution (300 seconds) - -### **JwtTokenParameters** -Authentication configuration: -- **Key**: Must be at least 32 characters -- **Issuer**: Token issuer identifier -- **Audience**: Token audience identifier - -## Database Migration - -After configuration, run database migrations: - -```bash -cd SW.Bitween.Web - -# For PostgreSQL -dotnet ef database update --context PgSql.BitweenDbContext - -# For MySQL -dotnet ef database update --context MySql.BitweenDbContext - -# For SQL Server -dotnet ef database update --context MsSql.BitweenDbContext -``` - -Or use the migration script: -```bash -./migratedb.sh -``` - -## Running Bitween - -```bash -cd SW.Bitween.Web -dotnet run -``` - -The API will be available at: -- HTTP: http://localhost:5000 -- HTTPS: https://localhost:5001 -- Swagger: http://localhost:5000/swagger - -## Default Admin Login - -After first run, log in with: -- **Username**: admin -- **Password**: Admin@123456 - -Change this immediately in production! - -## Troubleshooting - -### Port Already in Use -```bash -# Check what's using the port -lsof -i :5000 -# Kill the process -kill -9 -``` - -### Database Connection Failed -- Ensure PostgreSQL is running: `pg_isready` -- Check connection string in appsettings.Development.json -- Verify database exists: `psql -l` - -### MinIO Connection Failed -- Check MinIO is running: `curl http://localhost:9000/minio/health/live` -- Verify bucket exists via MinIO Console (http://localhost:9001) -- Check ForcePathStyle=true in configuration - -### RabbitMQ Connection Failed -- Check RabbitMQ is running: `curl http://localhost:15672` -- Verify guest user is enabled -- Check firewall/network settings - -## Environment-Specific Settings - -### For MySQL -```json -{ - "ConnectionStrings": { - "BitweenDb": "Server=localhost;Database=bitween_dev;Uid=root;Pwd=password;" - }, - "Bitween": { - "DatabaseType": "MySql" - } -} -``` - -### For SQL Server -```json -{ - "ConnectionStrings": { - "BitweenDb": "Server=localhost;Database=bitween_dev;User Id=sa;Password=YourStrong@Passw0rd;" - }, - "Bitween": { - "DatabaseType": "MsSql" - } -} -``` - -### For Azure Storage -```json -{ - "Bitween": { - "StorageProvider": "AS" - }, - "AzureBlobStorage": { - "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.net", - "ContainerName": "bitween-dev" - } -} -``` - -## Production Considerations - -1. **Change default passwords** in all services -2. **Use environment variables** for sensitive data -3. **Enable SSL/TLS** for all connections -4. **Set UseAzureManagedIdentity=true** when running in Azure -5. **Configure proper logging** (ElasticSearch URL in SWLogger section) -6. **Use production-grade message broker** settings -7. **Set AreXChangeFilesPrivate=true** for sensitive data -8. **Reduce BusDefaultQueuePrefetch** in high-load scenarios - -## Next Steps - -1. Start the UI project (Bitween-UI) -2. Create your first Document -3. Configure Partners -4. Set up Subscriptions -5. Deploy external adapters or use native adapters (native.http) - -For more details, see the [full documentation](../docs/getting-started.md).