Skip to content
106 changes: 106 additions & 0 deletions src/modules/Elsa.Common/Models/Result.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
namespace Elsa.Common.Models;

/// <summary>
/// A strongly typed monad that runs either the <see cref="OnSuccess"/> or <see cref="OnFailure"/> lambda, depending on whether or not the operation succeeded.
/// </summary>
public class Result<T>(bool success, T? value, Exception? exception)
{
/// <summary>
/// True if the conversion succeeded, false otherwise.
/// </summary>
public bool IsSuccess { get; } = success;

/// <summary>
/// The result value. Throws an exception if accessed when the result is a failure.
/// </summary>
public T Value
{
get
{
if (!IsSuccess)
throw new InvalidOperationException("Cannot access Value on a failed result. Check IsSuccess first or use ValueOrDefault.", Exception);
return value!;
}
}

/// <summary>
/// The result value, or null/default if the result is a failure. Useful when null is a valid success value.
/// </summary>
public T? ValueOrDefault => value;

/// <summary>
/// Any exception that may have occurred during the operation.
/// </summary>
public Exception? Exception { get; } = exception;

/// <summary>
/// Runs the provided delegate if the result is successful.
/// </summary>
public Result<T> OnSuccess(Action<T> successHandler)
{
if (IsSuccess)
successHandler(Value);

return this;
}

/// <summary>
/// Runs the provided async delegate if the result is successful.
/// </summary>
public async Task<Result<T>> OnSuccessAsync(Func<T, Task> successHandler)
{
if (IsSuccess)
await successHandler(Value);

return this;
}

/// <summary>
/// Runs the provided delegate if the result is unsuccessful.
/// </summary>
public Result<T> OnFailure(Action<Exception> failureHandler)
{
if (Exception != null)
failureHandler(Exception);

return this;
}

/// <summary>
/// Runs the provided async delegate if the result is unsuccessful.
/// </summary>
public async Task<Result<T>> OnFailureAsync(Func<Exception, Task> failureHandler)
{
if (Exception != null)
await failureHandler(Exception);

return this;
}

/// <summary>
/// Throws the exception if the result is a failure.
/// </summary>
public Result<T> ThrowIfFailure()
{
if (!IsSuccess && Exception != null)
throw Exception;

return this;
}
}

/// <summary>
/// A simple monad that runs either the <see cref="Result{T}.OnSuccess"/> or <see cref="Result{T}.OnFailure"/> lambda, depending on whether or not the operation succeeded.
/// </summary>
public class Result(bool success, object? value, Exception? exception) : Result<object>(success, value, exception)
{
/// <summary>
/// Creates a successful result with the specified value.
/// </summary>
public static Result<T> Success<T>(T value) => new(true, value, null);

/// <summary>
/// Creates a failed result with the specified exception.
/// </summary>
public static Result<T> Failure<T>(Exception exception) => new(false, default, exception);
}
19 changes: 10 additions & 9 deletions src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.Unicode;
using Elsa.Common.Models;
using Elsa.Expressions.Contracts;
using Elsa.Expressions.Exceptions;
using Elsa.Expressions.Extensions;
Expand Down Expand Up @@ -217,15 +218,15 @@ public static Result TryConvertTo(this object? value, Type targetType, ObjectCon
var sourceTypeConverter = TypeDescriptor.GetConverter(underlyingSourceType);

if (sourceTypeConverter.CanConvertTo(underlyingTargetType))
{
// TypeConverter.IsValid is not supported for ConvertTo, so we have to try and ignore any exceptions.
try
{
return sourceTypeConverter.ConvertTo(value, underlyingTargetType);
}
catch
{
// Ignore and try other conversion strategies.
{
// TypeConverter.IsValid is not supported for ConvertTo, so we have to try and ignore any exceptions.
try
{
return sourceTypeConverter.ConvertTo(value, underlyingTargetType);
}
catch
{
// Ignore and try other conversion strategies.
}
}

Expand Down
51 changes: 0 additions & 51 deletions src/modules/Elsa.Expressions/Models/Result.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
using Elsa.Common.Entities;
using Elsa.Common.Multitenancy;
using Elsa.Tenants.Options;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.Extensions.Options;

namespace Elsa.Persistence.EFCore.EntityHandlers;

/// <summary>
/// Represents a handler for applying the tenant ID to an entity before saving changes.
/// </summary>
public class ApplyTenantId : IEntitySavingHandler
public class ApplyTenantId(IOptions<TenantsOptions> tenantsOptions) : IEntitySavingHandler
{
/// <inheritdoc />
public ValueTask HandleAsync(ElsaDbContextBase dbContext, EntityEntry entry, CancellationToken cancellationToken = default)
{
if (entry.Entity is Entity entity)
{
// Don't touch tenant-agnostic entities (marked with "*")
if (entity.TenantId == Tenant.AgnosticTenantId)
return default;
// Only apply tenant ID if multitenancy is enabled
if (!tenantsOptions.Value.IsEnabled)
return default;

// Apply current tenant ID to entities without one
if (entity.TenantId == null && dbContext.TenantId != null)
entity.TenantId = dbContext.TenantId;
}
if (entry.Entity is not Entity entity)
return default;

// Don't touch tenant-agnostic entities (marked with "*")
if (entity.TenantId == Tenant.AgnosticTenantId)
return default;

// Apply current tenant ID to entities without one
if (entity.TenantId == null && dbContext.TenantId != null)
entity.TenantId = dbContext.TenantId;

return default;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
using System.Linq.Expressions;
using Elsa.Common.Entities;
using Elsa.Common.Multitenancy;
using Elsa.Tenants.Options;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.Extensions.Options;

namespace Elsa.Persistence.EFCore.EntityHandlers;

/// <summary>
/// Represents a class that applies a filter to set the TenantId for entities.
/// </summary>
public class SetTenantIdFilter : IEntityModelCreatingHandler
public class SetTenantIdFilter(IOptions<TenantsOptions> tenantsOptions) : IEntityModelCreatingHandler
{
/// <inheritdoc />
public void Handle(ElsaDbContextBase dbContext, ModelBuilder modelBuilder, IMutableEntityType entityType)
{
if (!typeof(Entity).IsAssignableFrom(entityType.ClrType))
return;

// Only apply the tenant filter if multitenancy is enabled
if (!tenantsOptions.Value.IsEnabled)
return;

modelBuilder
.Entity(entityType.ClrType)
.HasQueryFilter(CreateTenantFilterExpression(dbContext, entityType.ClrType));
Expand All @@ -26,7 +32,7 @@ private LambdaExpression CreateTenantFilterExpression(ElsaDbContextBase dbContex
{
var parameter = Expression.Parameter(clrType, "e");

// e => EF.Property<string>(e, "TenantId") == this.TenantId || EF.Property<string>(e, "TenantId") == "*"
// e => EF.Property<string>(e, "TenantId") == this.TenantId || EF.Property<string>(e, "TenantId") == "*" || (EF.Property<string>(e, "TenantId") == null && this.TenantId == "")
var tenantIdProperty = Expression.Call(
typeof(EF),
nameof(EF.Property),
Expand All @@ -40,7 +46,15 @@ private LambdaExpression CreateTenantFilterExpression(ElsaDbContextBase dbContex

var equalityCheck = Expression.Equal(tenantIdProperty, tenantIdOnContext);
var agnosticCheck = Expression.Equal(tenantIdProperty, Expression.Constant(Tenant.AgnosticTenantId, typeof(string)));
var body = Expression.OrElse(equalityCheck, agnosticCheck);

// For backwards compatibility: include records with null TenantId when context TenantId is empty string
var nullTenantCheck = Expression.Equal(tenantIdProperty, Expression.Constant(null, typeof(string)));
var emptyContextCheck = Expression.Equal(tenantIdOnContext, Expression.Constant(string.Empty, typeof(string)));
var backwardsCompatibilityCheck = Expression.AndAlso(nullTenantCheck, emptyContextCheck);

var body = Expression.OrElse(
Expression.OrElse(equalityCheck, agnosticCheck),
backwardsCompatibilityCheck);

return Expression.Lambda(body, parameter);
}
Expand Down
1 change: 1 addition & 0 deletions src/modules/Elsa.Tenants/Features/TenantsFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public override void ConfigureHostedServices()
public override void Apply()
{
Services.Configure(MultitenancyOptions);
Services.Configure<TenantsOptions>(options => options.IsEnabled = true);

Services
.AddScoped<ITenantResolverPipelineInvoker, DefaultTenantResolverPipelineInvoker>()
Expand Down
5 changes: 5 additions & 0 deletions src/modules/Elsa.Tenants/Options/TenantsOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ namespace Elsa.Tenants.Options;
/// </summary>
public class TenantsOptions
{
/// <summary>
/// Gets or sets a value indicating whether multitenancy is enabled.
/// </summary>
public bool IsEnabled { get; set; }

/// <summary>
/// Gets or sets the tenants through configuration. Will be used by the <see cref="ConfigurationTenantsProvider"/>
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public class GetRequest : IExecutionRequest
}
});

return result?.Success == true ? (IDictionary<string, object>?)result.Value : null;
return result?.IsSuccess == true ? (IDictionary<string, object>?)result.Value : null;
}
}

Expand Down
31 changes: 16 additions & 15 deletions src/modules/Elsa.Workflows.Core/Extensions/DictionaryExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Elsa.Common.Models;
using Elsa.Expressions.Helpers;
using Elsa.Expressions.Models;

Expand Down Expand Up @@ -37,20 +38,20 @@ public bool TryGetValue<T>(TKey key, out T value)
}

var result = TryConvertValue<T>(item);
value = result.Success ? (T)result.Value! : default!;
return result.Success;
value = result.IsSuccess ? (T)result.Value! : default!;
return result.IsSuccess;
}

public bool TryGetValue<T>(IEnumerable<TKey> keys, out T value)
{
foreach (var key in keys)
{
if (dictionary.TryGetValue(key, out var item))
{
var result = TryConvertValue<T>(item);
value = result.Success ? (T)result.Value! : default!;
return result.Success;
}
if (!dictionary.TryGetValue(key, out var item))
continue;

var result = TryConvertValue<T>(item);
value = result.IsSuccess ? (T)result.Value : default!;
return result.IsSuccess;
}

value = default!;
Expand All @@ -64,21 +65,21 @@ public bool TryGetValue<T>(IEnumerable<TKey> keys, out T value)

extension<TKey>(IDictionary<TKey, object> dictionary)
{
public T? GetValueOrDefault<T>(TKey key, Func<T?> defaultValueFactory) => TryGetValue<TKey, T>(dictionary, key, out var value) ? value : defaultValueFactory();
public T? GetValueOrDefault<T>(TKey key) => GetValueOrDefault<TKey, T>(dictionary, key, () => default);
public T? GetValueOrDefault<T>(TKey key, Func<T?> defaultValueFactory) => dictionary.TryGetValue<TKey, T>(key, out var value) ? value : defaultValueFactory();
public T? GetValueOrDefault<T>(TKey key) => dictionary.GetValueOrDefault<TKey, T>(key, () => default);
}

extension(IDictionary<string, object> dictionary)
{
public T? GetValueOrDefault<T>(string key, Func<T?> defaultValueFactory) => TryGetValue<T>(dictionary, key, out var value) ? value : defaultValueFactory();
public T? GetValueOrDefault<T>(IEnumerable<string> keys, Func<T?> defaultValueFactory) => TryGetValue<T>(dictionary, keys, out var value) ? value : defaultValueFactory();
public T? GetValueOrDefault<T>(string key) => GetValueOrDefault<T>(dictionary, key, () => default);
public object? GetValueOrDefault(string key) => GetValueOrDefault<object>(dictionary, key, () => null);
public T? GetValueOrDefault<T>(string key, Func<T?> defaultValueFactory) => dictionary.TryGetValue<T>(key, out var value) ? value : defaultValueFactory();
public T? GetValueOrDefault<T>(IEnumerable<string> keys, Func<T?> defaultValueFactory) => dictionary.TryGetValue<T>(keys, out var value) ? value : defaultValueFactory();
public T? GetValueOrDefault<T>(string key) => dictionary.GetValueOrDefault<T>(key, () => default);
public object? GetValueOrDefault(string key) => dictionary.GetValueOrDefault<object>(key, () => null);
}

public static T GetOrAdd<TKey, T>(this IDictionary<TKey, T> dictionary, TKey key, Func<T> valueFactory)
{
if(dictionary.TryGetValue(key, out T? value))
if(dictionary.TryGetValue(key, out var value))
return value;

value = valueFactory()!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public ValueTask WriteAsync(string id, object value, StorageDriverContext contex
SerializerOptions = payloadSerializer.GetOptions()
};
var result = node.TryConvertTo(variableType, options);
var parsedValue = result.Success ? result.Value : node;
var parsedValue = result.IsSuccess ? result.Value : node;
return new (parsedValue);
}

Expand Down
Loading
Loading