Add Elsa Connection Module (wip on persistence and naming)

This commit is contained in:
Jérémie DEVILLARD 2025-01-07 10:05:25 +01:00
parent e39f671bcc
commit 7ed2bc1c1f
36 changed files with 1921 additions and 973 deletions

1977
Elsa.sln

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Provides Activity Connection API endpoints</Description>
<PackageTags>elsa module activity connection abstraction api</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Api.Common\Elsa.Api.Common.csproj" />
<ProjectReference Include="..\Elsa.Connections.Persistence\Elsa.Connections.Persistence.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,33 @@
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json.Nodes;
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Connections.Contracts;
using Elsa.Connections.Models;
using Elsa.Models;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
using FastEndpoints;
using Humanizer;
namespace Elsa.Connections.Api.Endpoints.ActivityConnectionDescriptor.List;
public class List(IConnectionDescriptorRegistry registry) : ElsaEndpointWithoutRequest<PagedListResponse<ConnectionDescriptor>>
{
public override void Configure()
{
Get("/connection-configuration/descriptors");
AllowAnonymous();
}
public override Task<PagedListResponse<ConnectionDescriptor>> ExecuteAsync(CancellationToken ct)
{
var descriptors = registry.ListAll().ToList();
return Task.FromResult(new PagedListResponse<ConnectionDescriptor>(Page.Of(descriptors, descriptors.Count())) );
}
}

View file

@ -0,0 +1,26 @@
using Elsa.Abstractions;
using Elsa.Connections.Contracts;
using Elsa.Workflows.Models;
namespace Elsa.Connections.Api.Endpoints.ActivityConnectionDescriptor.List;
public class Get(IConnectionDescriptorRegistry store) : ElsaEndpointWithoutRequest<IEnumerable<InputDescriptor>>
{
public override void Configure()
{
Get("/connection-configuration/input-descriptor/{ActivityType}");
AllowAnonymous();
}
public override async Task HandleAsync(CancellationToken ct)
{
string type = Route<string>("ActivityType");
var config = await store.GetConnectionDescriptor(type);
if (config == null)
await SendNotFoundAsync();
else
await SendOkAsync(config);
}
}

View file

@ -0,0 +1,6 @@
namespace Elsa.Connections.Api.Endpoints.ActivityConnectionDescriptor.List;
public class Request
{
public string ActivityType { get; set; }
}

View file

@ -0,0 +1,40 @@
using Elsa.Abstractions;
using Elsa.Connections.Contracts;
using Elsa.Connections.Models;
namespace Elsa.Connections.Api.Endpoints.Add;
public class Endpoint(IConnectionRepository store) : ElsaEndpoint<ConnectionConfigurationMetadataModel>
{
public override void Configure()
{
Post("/connection-configuration");
AllowAnonymous();
}
public override async Task<object?> ExecuteAsync(ConnectionConfigurationMetadataModel model, CancellationToken ct)
{
await store.AddConnectionConfigurationAsync(model, ct);
await SendOkAsync();
return null;
}
}
public class EndpointUpdate(IConnectionRepository store) : ElsaEndpoint<ConnectionConfigurationMetadataModel>
{
public override void Configure()
{
Put("/connection-configuration/{id}");
AllowAnonymous();
}
public override async Task<object?> ExecuteAsync(ConnectionConfigurationMetadataModel model, CancellationToken ct)
{
await store.UpdateConnectionAsync(model.Id , model, ct);
await SendOkAsync();
return null;
}
}

View file

@ -0,0 +1,23 @@
using Elsa.Abstractions;
using Elsa.Connections.Contracts;
namespace Elsa.Connections.Api.Endpoints.Delete;
public class Endpoint(IConnectionRepository store) : ElsaEndpointWithoutRequest
{
public override void Configure()
{
Delete("/connection-configuration/{id}");
AllowAnonymous();
}
public override async Task<object?> HandleAsync(CancellationToken ct)
{
var configurationId = Route<string>("id");
await store.DeleteConnectionConfigurationAsync(configurationId, ct);
await SendOkAsync();
return null;
}
}

View file

@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Nodes;
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Connections.Contracts;
using Elsa.Connections.Models;
using Elsa.Models;
namespace Elsa.Connections.Api.Endpoints.List;
public class Endpoint(IConnectionRepository store) : ElsaEndpointWithoutRequest<PagedListResponse<ConnectionConfigurationMetadataModel>>
{
public override void Configure()
{
Get("/connection-configuration");
AllowAnonymous();
}
public override async Task<PagedListResponse<ConnectionConfigurationMetadataModel>> ExecuteAsync(CancellationToken ct)
{
var config = await store.GetConnectionsAsync();
return new PagedListResponse<ConnectionConfigurationMetadataModel>(Page.Of(config, config.Count()));
}
}

View file

@ -0,0 +1,19 @@
using Elsa.Connections.Api.Features;
using Elsa.Features.Services;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Extends <see cref="IModule"/> with methods to Connection API endpoints.
/// </summary>
public static class ModuleExtensions
{
/// <summary>
/// Installs the Semantic Kernel API feature.
/// </summary>
public static IModule UseConnectionsApi(this IModule module, Action<ConnectionsApiFeature>? configure = null)
{
return module.Use(configure);
}
}

View file

@ -0,0 +1,37 @@
using Elsa.Connections.Api.Services;
using Elsa.Connections.Contracts;
using Elsa.Connections.Models;
using Elsa.Connections.Persistence.Features;
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Connections.Api.Features;
/// <summary>
/// A feature that installs API endpoints to interact connections.
/// </summary>
[DependsOn(typeof(ConnectionPersistenceFeature))]
[UsedImplicitly]
public class ConnectionsApiFeature(IModule module) : FeatureBase(module)
{
/// <inheritdoc />
public override void Configure()
{
Module.AddFastEndpointsAssembly<ConnectionsApiFeature>();
}
public override void Apply()
{
Services.AddSingleton<IConnectionRepository, InMemoryConnectionRepository>();
Services
.AddMemoryStore<ConnectionConfigurationMetadataModel, InMemoryConnectionRepository>();
}
}

View file

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>

View file

@ -0,0 +1,45 @@
using Elsa.Common.Services;
using Elsa.Connections.Contracts;
using Elsa.Connections.Models;
namespace Elsa.Connections.Api.Services;
public class InMemoryConnectionRepository(MemoryStore<ConnectionConfigurationMetadataModel> memoryStore) : IConnectionRepository
{
public Task AddConnectionConfigurationAsync(ConnectionConfigurationMetadataModel model, CancellationToken cancellationToken = default)
{
model.Id = Guid.NewGuid().ToString("n");
memoryStore.Add(model, model =>model.Id);
return Task.CompletedTask;
}
public Task DeleteConnectionConfigurationAsync(string id, CancellationToken cancellationToken = default)
{
memoryStore.Delete(id);
return Task.CompletedTask;
}
public Task<ConnectionConfigurationMetadataModel> GetConnectionAsync(string name, CancellationToken cancellationToken = default)
{
var result = memoryStore.Find(c => c.Name == name);
return Task.FromResult(result);
}
public Task<ICollection<ConnectionConfigurationMetadataModel>> GetConnectionsAsync(CancellationToken cancellationToken)
{
ICollection<ConnectionConfigurationMetadataModel> results = memoryStore.List().ToList() ;
return Task.FromResult(results);
}
public Task<ICollection<ConnectionConfigurationMetadataModel>> GetConnectionsFromTypeAsync(string type, CancellationToken cancellationToken = default)
{
ICollection<ConnectionConfigurationMetadataModel> items = memoryStore.FindMany(c => c.ConnectionType == type).ToList();
return Task.FromResult(items);
}
public Task UpdateConnectionAsync(string id, ConnectionConfigurationMetadataModel model, CancellationToken cancellationToken = default)
{
memoryStore.Update(model, model => model.Id);
return Task.CompletedTask;
}
}

View file

@ -0,0 +1,15 @@
namespace Elsa.Connections.Attributes;
[AttributeUsage(AttributeTargets.Class)]
public class ConnectionActivityAttribute : Attribute
{
public ConnectionActivityAttribute(string type)
{
Type = type;
}
/// <summary>
/// The TypeName of the connection
/// </summary>
public string Type { get; set; }
}

View file

@ -0,0 +1,15 @@
namespace Elsa.Connections.Attributes;
public class ConnectionPropertyAttribute : Attribute
{
public ConnectionPropertyAttribute(string @namespace, string displayName, string? description= default)
{
Namespace = @namespace;
DisplayName = displayName;
Description = description;
}
public string? Namespace { get; set; }
public string? Description { get; set; }
public string? DisplayName { get; set; }
}

View file

@ -0,0 +1,13 @@
namespace Elsa.Connections.Attributes;
public class ConnectionTypeAttribute : Attribute
{
public ConnectionTypeAttribute(Type type)
{
Type = type;
}
/// <summary>
/// The TypeName of the connection
/// </summary>
public Type Type { get; set; }
}

View file

@ -0,0 +1,5 @@
namespace Elsa.Connections.Attributes;
public class NoLogAttribute : Attribute
{
}

View file

@ -0,0 +1,39 @@
using Elsa.Connections.Models;
using Elsa.Workflows.Models;
namespace Elsa.Connections.Contracts;
/// <summary>
/// Store all connection descriptors available to the system
/// </summary>
public interface IConnectionDescriptorRegistry
{
/// <summary>
/// Adds a connection descriptor to the registry
/// </summary>
/// <param name="connectionType">The type of the connection </param>
/// <param name="connectionDescriptor">The type desiptor of the connection</param>
void Add(Type connectionType, ConnectionDescriptor connectionDescriptor);
/// <summary>
/// Removes an activity descriptor from the registry.
/// </summary>
/// <param name="connectionType">The type of the connection.</param>
void Remove(Type connectionType, ActivityDescriptor descriptor);
/// <summary>
/// Returns all connection descriptors in the registry.
/// </summary>
/// <returns>All connection descriptors in the registry.</returns>
IEnumerable<ConnectionDescriptor> ListAll();
Type Get(string type);
/// <summary>
/// Get the Input Descriptors for an connection
/// </summary>
/// <param name="activityType">The type of the connectn</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<IEnumerable<InputDescriptor>> GetConnectionDescriptor(string activityType, CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,9 @@
using Elsa.Connections.Models;
using Elsa.Workflows.Models;
namespace Elsa.Connections.Contracts;
public interface IConnectionDescriptorRegistryEX
{
}

View file

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Elsa.Connections.Contracts;
interface IConnectionProperty
{
public string ConnectionName { get; set; }
[JsonIgnore]
public object Properties { get; set; }
}

View file

@ -0,0 +1,15 @@
using Elsa.Connections.Models;
namespace Elsa.Connections.Contracts;
public interface IConnectionRepository
{
public Task<ConnectionConfigurationMetadataModel> GetConnectionAsync(string name, CancellationToken cancellationToken = default);
public Task<ICollection<ConnectionConfigurationMetadataModel>> GetConnectionsAsync(CancellationToken cancellationToken = default);
public Task<ICollection<ConnectionConfigurationMetadataModel>> GetConnectionsFromTypeAsync(string type, CancellationToken cancellationToken = default);
public Task AddConnectionConfigurationAsync(ConnectionConfigurationMetadataModel model, CancellationToken cancellationToken = default);
public Task UpdateConnectionAsync(string name, ConnectionConfigurationMetadataModel model, CancellationToken cancellationToken = default);
public Task DeleteConnectionConfigurationAsync(string id, CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Provides an connection framework to use connection abtraction in activities</Description>
<PackageTags>elsa module connection abstratction</PackageTags>
<RootNamespace>Elsa.Connections</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Common\Elsa.Common.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,19 @@
using Elsa.Connections.Features;
using Elsa.Features.Services;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Extends <see cref="IModule"/> with methods to install Semantic Kernel API endpoints.
/// </summary>
public static class ModuleExtensions
{
/// <summary>
/// Installs the Semantic Kernel API feature.
/// </summary>
public static IModule UseConnections(this IModule module, Action<ConnectionsFeatures>? configure = null)
{
return module.Use(configure);
}
}

View file

@ -0,0 +1,80 @@
using System.Reflection;
using Elsa.Connections.Attributes;
using Elsa.Connections.Contracts;
using Elsa.Connections.Filters;
using Elsa.Connections.ServiceProvider;
using Elsa.Connections.Services;
using Elsa.Connections.UIHints;
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Workflows;
using Elsa.Workflows.Pipelines.ActivityExecution;
using Elsa.Workflows.Features;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Elsa.Connections.Middleware;
namespace Elsa.Connections.Features;
/// <summary>
/// A feature that installs API endpoints to interact with skilled agents.
/// </summary>
[DependsOn(typeof(WorkflowsFeature))]
[UsedImplicitly]
public class ConnectionsFeatures(IModule module) : FeatureBase(module)
{
/// <summary>
/// A set of connection types to make available to the system.
/// </summary>
public HashSet<Type> ConnectionTypes { get; } = [];
/// <inheritdoc />
public override void Apply()
{
// Obfuscate Connection Properties.
Services.AddActivityStateFilter<PropertyAttributeFilter>();
// Activity property options providers.
Services.AddScoped<Elsa.Workflows.IPropertyUIHandler, ConnectionOptionsProvider>();
Services.AddSingleton<IConnectionDescriptorRegistry, ConnectionRegistry>();
//UIHints
Services.AddScoped<IUIHintHandler, ConnexionDropDownUIHintHandler>();
Services.Configure<ConnectionOptions>(options =>
{
foreach (var connectionType in ConnectionTypes.Distinct())
options.ConnectionTypes.Add(connectionType);
});
}
public override void Configure()
{
//TODO: Need to insert the middleware just before the BackgroundActivityInvoker
//How to be sure that it is inserted before?
var workflowFeature = Module.Configure<WorkflowsFeature>()
.WithDefaultActivityExecutionPipeline(pipeline => pipeline.Insert<ConnectionMiddleware>(3));
base.Configure();
}
public ConnectionsFeatures AddConnectionsFrom<TMarker>()
{
var connectionTypes = typeof(TMarker).Assembly.GetExportedTypes()
.Where(x => x.GetCustomAttribute<ConnectionPropertyAttribute>() != null )
.ToList();
ConnectionTypes.AddRange(connectionTypes);
return this;
}
}
public class ConnectionOptions
{
/// <summary>
/// A collection of connection types that are available to the system.
/// </summary>
public HashSet<Type> ConnectionTypes { get; set; } = new();
}

View file

@ -0,0 +1,27 @@
using Elsa.Connections.Attributes;
using Elsa.Workflows;
namespace Elsa.Connections.Filters;
public class PropertyAttributeFilter : ActivityStateFilterBase
{
protected override ActivityStateFilterResult OnExecute(ActivityStateFilterContext context)
{
var activityExecutionContext = context.ActivityExecutionContext;
var activity = activityExecutionContext.Activity;
var inputDescriptor = context.InputDescriptor;
if (Attribute.IsDefined(inputDescriptor.PropertyInfo, typeof(NoLogAttribute)))
{
var contextValue = context.Value.GetProperty("connectionName").GetString();
if (contextValue == null)
return ActivityStateFilterResult.Pass();
var maskedValue = $"**** see connection information for {contextValue} ****";
return Filtered(maskedValue);
}
else
return ActivityStateFilterResult.Pass();
}
}

View file

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>

View file

@ -0,0 +1,101 @@
using System.Reflection;
using System.Text.Json;
using Elsa.Connections.Attributes;
using Elsa.Connections.Contracts;
using Elsa.Connections.Models;
using Elsa.Workflows;
using Elsa.Workflows.Pipelines.ActivityExecution;
using Elsa.Workflows.UIHints.Dropdown;
using JetBrains.Annotations;
namespace Elsa.Connections.Middleware;
/// <summary>
/// Adds extension methods to <see cref="ExecutionLogMiddleware"/>.
/// </summary>
public static class ConnectionMiddlewareExtensions
{
/// <summary>
/// Installs the <see cref="ConnectionMiddleware"/> component in the activity execution pipeline.
/// </summary>
public static IActivityExecutionPipelineBuilder UseConnectionMiddleware(this IActivityExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware<ConnectionMiddleware>();
}
/// <summary>
/// An activity execution middleware component that extracts execution details as <see cref="WorkflowExecutionLogEntry"/> objects.
/// </summary>
[UsedImplicitly]
public class ConnectionMiddleware(ActivityMiddlewareDelegate next, IConnectionRepository connectionStore) : IActivityExecutionMiddleware
{
/// <inheritdoc />
public async ValueTask InvokeAsync(ActivityExecutionContext context)
{
var activityDescriptor = context.ActivityDescriptor;
if (activityDescriptor.Attributes.Any(attr => attr.GetType() == typeof(ConnectionActivityAttribute)))
{
var inputDescriptors = activityDescriptor.Inputs.Where(x => x?.PropertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(ConnectionProperties<>));
if (inputDescriptors != null && inputDescriptors.Any())
{
var input = inputDescriptors.First();
var propertyType = input?.PropertyInfo?.PropertyType.GetGenericArguments()[0];
dynamic inputValue = input.ValueGetter(context.Activity);
var connectionName = (string)inputValue?.ConnectionName;
//Get connection from store, if exist,
var connectionConfiguration = await connectionStore.GetConnectionAsync(connectionName);
if (connectionConfiguration != null)
{
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
options.Converters.Add(new Int32Converter());
dynamic deserializedjson =
JsonSerializer.Deserialize(connectionConfiguration.ConnectionConfiguration, propertyType, options);
inputValue.Properties = deserializedjson;
input.ValueSetter(context.Activity, inputValue);
}
}
}
await next(context);
}
private static bool IsActivityBookmarked(ActivityExecutionContext context)
{
return context.WorkflowExecutionContext.Bookmarks.Any(b => b.ActivityNodeId.Equals(context.ActivityNode.NodeId));
}
}
public class Int32Converter : System.Text.Json.Serialization.JsonConverter<int>
{
public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
string stringValue = reader.GetString();
if (int.TryParse(stringValue, out int value))
{
return value;
}
}
else if (reader.TokenType == JsonTokenType.Number)
{
return reader.GetInt32();
}
throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
{
writer.WriteNumberValue(value);
}
}

View file

@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Nodes;
namespace Elsa.Connections.Models;
public class ConnectionConfigurationMetadataModel
{
public string Id { get; set; }
[Required]
public string? Name { get; set; }
public string Description { get; set; }
public JsonObject ConnectionConfiguration { get; set; }
[Required]
public string ConnectionType { get; set; }
}

View file

@ -0,0 +1,3 @@
namespace Elsa.Connections.Models;
public record ConnectionDescriptor(string type, string description, string @namespace, string providerName);

View file

@ -0,0 +1,20 @@
using System.Text.Json.Serialization;
namespace Elsa.Connections.Models;
public class ConnectionProperties<T> where T : class, new()
{
/// <summary>
/// Creates a new instance of the <see cref="ConnectionProperties"/> class.
/// </summary>
[JsonConstructor]
public ConnectionProperties()
{
}
public string ConnectionName { get; set; }
[JsonIgnore]
public T Properties { get; set; } = new T();
}

View file

@ -0,0 +1,46 @@
using System.Reflection;
using Elsa.Connections.Attributes;
using Elsa.Connections.Contracts;
using Elsa.Workflows.UIHints.Dropdown;
namespace Elsa.Connections.ServiceProvider;
public class ConnectionOptionsProvider : DropDownOptionsProviderBase
{
private readonly IConnectionRepository _store;
public ConnectionOptionsProvider(IConnectionRepository store)
{
_store = store;
}
public new async ValueTask<IDictionary<string, object>> GetUIPropertiesAsync(PropertyInfo propertyInfo, object? context, CancellationToken cancellationToken = default)
{
var options = await base.GetUIPropertiesAsync(propertyInfo, context, cancellationToken);
options.Add("Refresh", true);
return options;
}
protected override async ValueTask<ICollection<SelectListItem>> GetItemsAsync(PropertyInfo propertyInfo, object? context, CancellationToken cancellationToken)
{
var connection = new List<SelectListItem>();
var connectionType = propertyInfo.GetCustomAttribute<ConnectionTypeAttribute>()?.Type;
if (connectionType == null)
return connection;
var connections = await _store.GetConnectionsFromTypeAsync(connectionType.ToString());
foreach(var conn in connections)
connection.Add(new SelectListItem(conn.Name, conn.Name));
//connection.Add(new SelectListItem("connection 2", "idConnection2"));
//connection.Add(new SelectListItem("connection 3", "idConnection3"));
return connection;
//throw new NotImplementedException();
}
}

View file

@ -0,0 +1,86 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Elsa.Connections.Attributes;
using Elsa.Connections.Contracts;
using Elsa.Connections.Features;
using Elsa.Connections.Models;
using Elsa.Workflows;
using Elsa.Workflows.Models;
using Microsoft.Extensions.Options;
namespace Elsa.Connections.Services;
public class ConnectionRegistry : IConnectionDescriptorRegistry
{
private readonly ConcurrentDictionary<Type, ConnectionDescriptor> _connectionDescriptors = new();
private readonly ConnectionOptions _options;
private readonly IActivityDescriber _describer;
public ConnectionRegistry(IOptions<ConnectionOptions> options, IActivityDescriber describer)
{
_options = options.Value;
_describer = describer;
}
public void Add(Type connectionType, ConnectionDescriptor connectionDescriptor)
{
var descriptor = DescribeConnection(connectionType);
_connectionDescriptors.TryAdd(connectionType, descriptor);
}
public IEnumerable<ConnectionDescriptor> ListAll()
{
foreach (var connectionType in _options.ConnectionTypes)
yield return DescribeConnection(connectionType);
}
public void Remove(Type connectionType, ActivityDescriptor descriptor)
{
throw new NotImplementedException();
}
public Type Get(string type)
{
var item = _options.ConnectionTypes.Where(c => c.ToString() == type).FirstOrDefault();
return item;
}
public async Task<IEnumerable<InputDescriptor>> GetConnectionDescriptor(string activityType, CancellationToken cancellationToken = default)
{
var propertyType2 = Type.GetType(activityType);
var propertyType = Get(activityType);
if (propertyType == null)
return null;
var connectionInputProperty = _describer.GetInputProperties(propertyType);
var connectionInputDescriptor = await DescribeInputPropertiesAsync(connectionInputProperty);
return connectionInputDescriptor;
}
private async Task<IEnumerable<InputDescriptor>> DescribeInputPropertiesAsync(IEnumerable<PropertyInfo> properties, CancellationToken cancellationToken = default)
{
return await Task.WhenAll(properties.Select(async x => await _describer.DescribeInputPropertyAsync(x, cancellationToken)));
}
private ConnectionDescriptor DescribeConnection(Type connectionType)
{
var connectionAttribute = connectionType.GetCustomAttribute<ConnectionPropertyAttribute>();
if (connectionAttribute == null)
throw new ArgumentNullException($"{connectionType} is not a valid connection, make sure [ConnectionPropertyAttribute] is set ");
return new ConnectionDescriptor(
connectionType.ToString(),
connectionAttribute?.Description,
connectionAttribute.Namespace,
connectionAttribute.DisplayName
);
}
}

View file

@ -0,0 +1,18 @@
using System.Reflection;
using Elsa.Workflows;
using Elsa.Workflows.UIHints;
using Elsa.Workflows.UIHints.Dropdown;
namespace Elsa.Connections.UIHints;
public class ConnexionDropDownUIHintHandler : IUIHintHandler
{
/// <inheritdoc />
public string UIHint => $"connexion-{InputUIHints.DropDown}";
/// <inheritdoc />
public ValueTask<IEnumerable<Type>> GetPropertyUIHandlersAsync(PropertyInfo propertyInfo, CancellationToken cancellationToken)
{
return new(new[] { typeof(StaticDropDownOptionsProvider) });
}
}

View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Folder Include="Configs\" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Provides Connections persistence services</Description>
<PackageTags>elsa module connections persistence </PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Common\Elsa.Common.csproj" />
<ProjectReference Include="..\Elsa.Connections.Core\Elsa.Connections.Core.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,58 @@
using Elsa.Common.Features;
using Elsa.Connections.Features;
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Connections.Persistence.Features;
[DependsOn(typeof(ConnectionsFeatures))]
public class ConnectionPersistenceFeature(IModule module) : FeatureBase(module)
{
//private Func<IServiceProvider, IApiKeyStore> _apiKeyStoreFactory = sp => sp.GetRequiredService<MemoryApiKeyStore>();
//private Func<IServiceProvider, IServiceStore> _serviceStoreFactory = sp => sp.GetRequiredService<MemoryServiceStore>();
//private Func<IServiceProvider, IAgentStore> _agentStoreFactory = sp => sp.GetRequiredService<MemoryAgentStore>();
//public AgentPersistenceFeature UseApiKeyStore(Func<IServiceProvider, IApiKeyStore> factory)
//{
// _apiKeyStoreFactory = factory;
// return this;
//}
//public AgentPersistenceFeature UseServiceStore(Func<IServiceProvider, IServiceStore> factory)
//{
// _serviceStoreFactory = factory;
// return this;
//}
//public AgentPersistenceFeature UseAgentStore(Func<IServiceProvider, IAgentStore> factory)
//{
// _agentStoreFactory = factory;
// return this;
//}
//public override void Configure()
//{
// Module.UseAgents(agents => agents.UseKernelConfigProvider(sp => sp.GetRequiredService<StoreKernelConfigProvider>()));
//}
//public override void Apply()
//{
// Services
// .AddScoped(_apiKeyStoreFactory)
// .AddScoped(_serviceStoreFactory)
// .AddScoped(_agentStoreFactory);
// Services
// .AddScoped<IAgentManager, AgentManager>();
// Services
// .AddMemoryStore<ApiKeyDefinition, MemoryApiKeyStore>()
// .AddMemoryStore<ServiceDefinition, MemoryServiceStore>()
// .AddMemoryStore<AgentDefinition, MemoryAgentStore>();
// Services.AddScoped<StoreKernelConfigProvider>();
//}
}

View file

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>