Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions SW.Bitween.Api/Resources/Adapters/GetProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,26 @@ namespace SW.Bitween.Resources.Adapters
public class GetProperties : IGetHandler<string,object>
{
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<object> 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"})" : " *")}");
}
Expand Down
13 changes: 11 additions & 2 deletions SW.Bitween.Api/Resources/Adapters/Search.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,23 @@ public class Search : IQueryHandler<AdapterSearchRequest,object>
{
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<object> 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}"))
Expand All @@ -33,10 +39,13 @@ public async Task<object> 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);
}
}
}
20 changes: 18 additions & 2 deletions SW.Bitween.Api/Resources/Adapters/SearchVersioned.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,31 @@ public class SearchVersioned : IQueryHandler<AdapterSearchRequest,object>
{
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;
}


public async Task<object> 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<object>() // Native adapters have no versions
})
.ToList();

// Get external adapters from storage
var cloudFilesList =
(await _cloudFilesService.ListAsync(
$"{_serverlessOptions.AdapterRemotePath}/infolink6.{request.Prefix}"))
Expand All @@ -40,7 +53,7 @@ public async Task<object> 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()))
Expand All @@ -49,6 +62,9 @@ public async Task<object> Handle(AdapterSearchRequest request)
Key = v.Key[index..]
}).ToList()
});

// Return native adapters first, then external
return nativeAdapters.Concat<object>(externalAdapters);
}
}
}
67 changes: 54 additions & 13 deletions SW.Bitween.Api/Resources/Subscriptions/Update.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,24 @@ public Validate(IServiceProvider serviceProvider)
{
RuleFor(i => i.MapperProperties).CustomAsync(async (i, context, ct) =>
{
var serverless = serviceProvider.GetService<IServerlessService>();
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<string>();

// Check if it's a native adapter
if (mapperId.StartsWith("native.", StringComparison.OrdinalIgnoreCase))
{
var nativeAdapterDiscovery = serviceProvider.GetService<NativeAdapterDiscoveryService>();
var properties = nativeAdapterDiscovery.GetNativeAdapterProperties(mapperId);
Comment on lines +111 to +112

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use GetRequiredService instead of GetService to fail fast.

GetService<NativeAdapterDiscoveryService>() returns null if the service isn't registered, and the very next line dereferences it without a null check. Use GetRequiredService to get a clear exception instead of a NullReferenceException. Same applies to GetService<IServerlessService>() on lines 117, 146, and 180.

🐛 Proposed fix (apply to all three blocks)
-                            var nativeAdapterDiscovery = serviceProvider.GetService<NativeAdapterDiscoveryService>();
+                            var nativeAdapterDiscovery = serviceProvider.GetRequiredService<NativeAdapterDiscoveryService>();
-                            var serverless = serviceProvider.GetService<IServerlessService>();
+                            var serverless = serviceProvider.GetRequiredService<IServerlessService>();

Also applies to: 140-141, 174-175

🤖 Prompt for AI Agents
In `@SW.Bitween.Api/Resources/Subscriptions/Update.cs` around lines 111 - 112,
Replace calls to serviceProvider.GetService<T>() with
serviceProvider.GetRequiredService<T>() for the referenced services so the app
fails fast with a clear exception instead of risking a NullReferenceException;
specifically update the usages of NativeAdapterDiscoveryService (used before
calling GetNativeAdapterProperties(mapperId)) and the IServerlessService lookups
(the calls around where you later call methods on the returned instance) to use
GetRequiredService on the serviceProvider variable in Update.cs so the service
resolution throws immediately if the registration is missing.

mustProps = properties.Where(p => p.Value.EndsWith(" *")).Select(p => p.Key);
}
else
{
var serverless = serviceProvider.GetService<IServerlessService>();
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())
Expand All @@ -117,10 +131,24 @@ public Validate(IServiceProvider serviceProvider)
{
RuleFor(i => i.HandlerProperties).CustomAsync(async (i, context, ct) =>
{
var serverless = serviceProvider.GetService<IServerlessService>();
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<string>();

// Check if it's a native adapter
if (handlerId.StartsWith("native.", StringComparison.OrdinalIgnoreCase))
{
var nativeAdapterDiscovery = serviceProvider.GetService<NativeAdapterDiscoveryService>();
var properties = nativeAdapterDiscovery.GetNativeAdapterProperties(handlerId);
mustProps = properties.Where(p => p.Value.EndsWith(" *")).Select(p => p.Key);
}
else
{
var serverless = serviceProvider.GetService<IServerlessService>();
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())
Expand All @@ -137,11 +165,24 @@ public Validate(IServiceProvider serviceProvider)
{
RuleFor(i => i.ReceiverProperties).CustomAsync(async (i, context, ct) =>
{
var serverless = serviceProvider.GetService<IServerlessService>();
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<string>();

// Check if it's a native adapter
if (receiverId.StartsWith("native.", StringComparison.OrdinalIgnoreCase))
{
var nativeAdapterDiscovery = serviceProvider.GetService<NativeAdapterDiscoveryService>();
var properties = nativeAdapterDiscovery.GetNativeAdapterProperties(receiverId);
mustProps = properties.Where(p => p.Value.EndsWith(" *")).Select(p => p.Key);
}
else
{
var serverless = serviceProvider.GetService<IServerlessService>();
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())
Expand Down
1 change: 1 addition & 0 deletions SW.Bitween.Api/SW.Bitween.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\SW.Bitween.NativeAdapters\SW.Bitween.NativeAdapters.csproj" />
<ProjectReference Include="..\SW.Bitween.Sdk\SW.Bitween.Sdk.csproj" />
</ItemGroup>

Expand Down
172 changes: 172 additions & 0 deletions SW.Bitween.Api/Services/NativeAdapterDiscoveryService.cs
Original file line number Diff line number Diff line change
@@ -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<string, List<NativeAdapterInfo>> _adaptersCache;

public NativeAdapterDiscoveryService()
{
_adaptersCache = new Dictionary<string, List<NativeAdapterInfo>>();
DiscoverNativeAdapters();
}

private void DiscoverNativeAdapters()
{
var assemblies = new List<Assembly>() { 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
}
}
Comment on lines +48 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bare catch swallows all exceptions silently.

Catching all exceptions without logging makes it very difficult to diagnose adapter discovery failures. At minimum, log a warning with the exception and assembly name.

🔧 Proposed fix
             catch
+            catch (Exception ex)
             {
-                // Skip assemblies that can't be loaded or scanned
+                // Log warning but continue discovery for other assemblies
+                System.Diagnostics.Debug.WriteLine($"Failed to scan assembly {assembly.FullName}: {ex.Message}");
             }

Ideally, inject an ILogger<NativeAdapterDiscoveryService> and use structured logging instead of Debug.WriteLine.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
}
catch
{
// Skip assemblies that can't be loaded or scanned
}
}
}
catch (Exception ex)
{
// Log warning but continue discovery for other assemblies
System.Diagnostics.Debug.WriteLine($"Failed to scan assembly {assembly.FullName}: {ex.Message}");
}
🤖 Prompt for AI Agents
In `@SW.Bitween.Api/Services/NativeAdapterDiscoveryService.cs` around lines 48 -
53, The bare catch in NativeAdapterDiscoveryService silently swallows all
exceptions during assembly scanning; change it to catch (Exception ex) and log a
warning that includes the assembly name and exception details using an injected
ILogger<NativeAdapterDiscoveryService> (replace any Debug.WriteLine calls with
logger.LogWarning or logger.LogError and use structured logging like "Failed to
load/scan assembly {AssemblyName}: {Exception}"). Ensure
ILogger<NativeAdapterDiscoveryService> is added to the service via constructor
injection and used inside the catch block to record the assembly name and ex.

}

private void AddAdapter(string category, Type type)
{
if (!_adaptersCache.ContainsKey(category))
{
_adaptersCache[category] = new List<NativeAdapterInfo>();
}

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<string> 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<string>();
}

public NativeAdapterInfo GetNativeAdapterInfo(string adapterId)
{
return _adaptersCache.Values
.SelectMany(v => v)
.FirstOrDefault(a => a.Key.Equals(adapterId, StringComparison.OrdinalIgnoreCase));
}
Comment on lines +92 to +97

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

GetNativeAdapterInfo can return null but the return type is non-nullable.

FirstOrDefault returns null when no match is found, but the method signature declares NativeAdapterInfo (not NativeAdapterInfo?). Callers (e.g., ReceivingService.cs line 135) already null-check the result, but the signature should reflect nullability to prevent misuse.

Proposed fix
-    public NativeAdapterInfo GetNativeAdapterInfo(string adapterId)
+    public NativeAdapterInfo? GetNativeAdapterInfo(string adapterId)
🤖 Prompt for AI Agents
In `@SW.Bitween.Api/Services/NativeAdapterDiscoveryService.cs` around lines 92 -
97, GetNativeAdapterInfo can return null because it uses FirstOrDefault, so
update the method signature to return NativeAdapterInfo? (nullable) and keep the
existing FirstOrDefault usage; ensure callers (e.g., ReceivingService and any
other callers) continue to null-check the result or are updated to handle the
nullable return appropriately. Locate the method GetNativeAdapterInfo and change
its return type to NativeAdapterInfo? so the nullability is explicit.


public Dictionary<string, string> GetNativeAdapterProperties(string adapterId)
{
var adapterInfo = GetNativeAdapterInfo(adapterId);
if (adapterInfo == null)
return new Dictionary<string, string>();

var result = new Dictionary<string, string>();

// 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<System.ComponentModel.DataAnnotations.RequiredAttribute>() != 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<System.ComponentModel.DefaultValueAttribute>();
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;
}
}
Loading