Skip to content

Commit

Permalink
Introduce IResilienceStrategyBuilder
Browse files Browse the repository at this point in the history
  • Loading branch information
martintmk committed Mar 16, 2023
1 parent 0229729 commit 6f3f774
Show file tree
Hide file tree
Showing 9 changed files with 432 additions and 0 deletions.
202 changes: 202 additions & 0 deletions src/Polly.Core.Tests/Builder/ResilienceStrategyBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
using System;
using System.ComponentModel.DataAnnotations;
using FluentAssertions;
using Polly.Builder;
using Polly.Core.Tests.Utils;
using Xunit;

namespace Polly.Core.Tests.Builder;

public class ResilienceStrategyBuilderTests
{
[Fact]
public void AddStrategy_Single_Ok()
{
// arrange
var executions = new List<int>();
var builder = new ResilienceStrategyBuilder();
var first = new TestResilienceStrategy
{
Before = (_, _) => executions.Add(1),
After = (_, _) => executions.Add(3),
};

builder.AddStrategy(first);

// act
var strategy = builder.Build();

// assert
strategy.Execute(_ => executions.Add(2));

executions.Should().BeInAscendingOrder();
executions.Should().HaveCount(3);
}

[Fact]
public void AddStrategy_Multiple_Ok()
{
// arrange
var executions = new List<int>();
var builder = new ResilienceStrategyBuilder();
var first = new TestResilienceStrategy
{
Before = (_, _) => executions.Add(1),
After = (_, _) => executions.Add(7),
};
var second = new TestResilienceStrategy
{
Before = (_, _) => executions.Add(2),
After = (_, _) => executions.Add(6),
};
var third = new TestResilienceStrategy
{
Before = (_, _) => executions.Add(3),
After = (_, _) => executions.Add(5),
};

builder.AddStrategy(first);
builder.AddStrategy(second);
builder.AddStrategy(third);

// act
var strategy = builder.Build();

// assert
strategy.Execute(_ => executions.Add(4));

executions.Should().BeInAscendingOrder();
executions.Should().HaveCount(7);
}

[Fact]
public void Build_Empty_ReturnsNullResilienceStrategy()
{
new ResilienceStrategyBuilder().Build().Should().BeSameAs(NullResilienceStrategy.Instance);
}

[Fact]
public void Options_SetNull_Throws()
{
var builder = new ResilienceStrategyBuilder();

builder.Invoking(b => b.Options = null!).Should().Throw<ArgumentNullException>();
}

[Fact]
public void Build_InvalidBuilderOptions_Throw()
{
var builder = new ResilienceStrategyBuilder();
builder.Options.BuilderName = null!;

builder.Invoking(b => b.Build()).Should().Throw<ValidationException>();
}

[Fact]
public void AddStrategy_InvalidOptions_Throws()
{
var builder = new ResilienceStrategyBuilder();

builder
.Invoking(b => b.AddStrategy(NullResilienceStrategy.Instance, new ResilienceStrategyOptions { StrategyName = null!, StrategyType = null! }))
.Should()
.Throw<ValidationException>()
.WithMessage("The StrategyName field is required.");
}

[Fact]
public void AddStrategy_NullFactory_Throws()
{
var builder = new ResilienceStrategyBuilder();

builder
.Invoking(b => b.AddStrategy(null!))
.Should()
.Throw<ArgumentNullException>()
.And.ParamName
.Should()
.Be("factory");
}

[Fact]
public void AddStrategy_CombinePipelines_Ok()
{
// arrange
var executions = new List<int>();
var first = new TestResilienceStrategy
{
Before = (_, _) => executions.Add(1),
After = (_, _) => executions.Add(7),
};
var second = new TestResilienceStrategy
{
Before = (_, _) => executions.Add(2),
After = (_, _) => executions.Add(6),
};

var pipeline1 = new ResilienceStrategyBuilder().AddStrategy(first).AddStrategy(second).Build();

var third = new TestResilienceStrategy
{
Before = (_, _) => executions.Add(3),
After = (_, _) => executions.Add(5),
};
var pipeline2 = new ResilienceStrategyBuilder().AddStrategy(third).Build();

// act
var strategy = new ResilienceStrategyBuilder().AddStrategy(pipeline1).AddStrategy(pipeline2).Build();

// assert
strategy.Execute(_ => executions.Add(4));

executions.Should().BeInAscendingOrder();
executions.Should().HaveCount(7);
}

[Fact]
public void BuildStrategy_EnsureCorrectContext()
{
// arrange
bool verified1 = false;
bool verified2 = false;

var builder = new ResilienceStrategyBuilder
{
Options = new ResilienceStrategyBuilderOptions
{
BuilderName = "builder-name"
}
};

builder.AddStrategy(
context =>
{
context.BuilderName.Should().Be("builder-name");
context.StrategyName.Should().Be("strategy-name");
context.StrategyType.Should().Be("strategy-type");
verified1 = true;

return NullResilienceStrategy.Instance;
},
new ResilienceStrategyOptions { StrategyName = "strategy-name", StrategyType = "strategy-type" });

builder.AddStrategy(
context =>
{
context.BuilderName.Should().Be("builder-name");
context.StrategyName.Should().Be("strategy-name-2");
context.StrategyType.Should().Be("strategy-type-2");
verified2 = true;

return NullResilienceStrategy.Instance;
},
new ResilienceStrategyOptions { StrategyName = "strategy-name-2", StrategyType = "strategy-type-2" });

// act
builder.Build();

// assert
verified1.Should().BeTrue();
verified2.Should().BeTrue();
}
}
21 changes: 21 additions & 0 deletions src/Polly.Core/Builder/DelegatingStrategyWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;

namespace Polly.Builder;

/// <summary>
/// A wrapper that converts a <see cref="IResilienceStrategy"/> into a <see cref="DelegatingResilienceStrategy"/>.
/// </summary>
internal sealed class DelegatingStrategyWrapper : DelegatingResilienceStrategy
{
private readonly IResilienceStrategy _strategy;

public DelegatingStrategyWrapper(IResilienceStrategy strategy) => _strategy = strategy;

protected override ValueTask<TResult> ExecuteCoreAsync<TResult, TState>(Func<ResilienceContext, TState, ValueTask<TResult>> callback, ResilienceContext context, TState state)
{
return _strategy.ExecuteAsync(
static (context, state) => state.Next.ExecuteAsync(state.callback, context, state.state),
context,
(Next, callback, state));
}
}
31 changes: 31 additions & 0 deletions src/Polly.Core/Builder/IResilienceStrategyBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace Polly.Builder;

/// <summary>
/// A builder that is used to create an instance of <see cref="IResilienceStrategy"/>.
/// </summary>
/// <remarks>
/// The builder supports chaining multiple strategies into a pipeline of strategies.
/// The resulting instance of <see cref="IResilienceStrategy"/> created by the <see cref="Build"/> call will execute the strategies in the same order they were added to the builder.
/// The order of the strategies is important.
/// </remarks>
public interface IResilienceStrategyBuilder
{
/// <summary>
/// Gets or sets the builder options.
/// </summary>
ResilienceStrategyBuilderOptions Options { get; set; }

/// <summary>
/// Adds a strategy to the builder.
/// </summary>
/// <param name="factory">The factory that creates a resilience strategy.</param>
/// <param name="options">The options associated with the strategy. If none are provided the default instance of <see cref="ResilienceStrategyOptions"/> is created.</param>
/// <returns>The same builder instance.</returns>
IResilienceStrategyBuilder AddStrategy(Func<ResilienceStrategyBuilderContext, IResilienceStrategy> factory, ResilienceStrategyOptions? options = null);

/// <summary>
/// Builds the resilience strategy.
/// </summary>
/// <returns>An instance of <see cref="IResilienceStrategy"/>.</returns>
IResilienceStrategy Build();
}
84 changes: 84 additions & 0 deletions src/Polly.Core/Builder/ResilienceStrategyBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System.ComponentModel.DataAnnotations;

namespace Polly.Builder;

/// <inheritdoc/>
public class ResilienceStrategyBuilder : IResilienceStrategyBuilder
{
private readonly List<Entry> _entries = new();
private ResilienceStrategyBuilderOptions _options = new();

/// <inheritdoc/>
public ResilienceStrategyBuilderOptions Options
{
get => _options;
set => _options = Guard.NotNull(value);
}

/// <inheritdoc/>
public IResilienceStrategyBuilder AddStrategy(Func<ResilienceStrategyBuilderContext, IResilienceStrategy> factory, ResilienceStrategyOptions? options = null)
{
Guard.NotNull(factory);

if (options is not null)
{
Validator.ValidateObject(options, new ValidationContext(options), validateAllProperties: true);
}

_entries.Add(new Entry(factory, options ?? new ResilienceStrategyOptions()));

return this;
}

/// <inheritdoc/>
public IResilienceStrategy Build()
{
Validator.ValidateObject(Options, new ValidationContext(Options), validateAllProperties: true);

if (_entries.Count == 0)
{
return NullResilienceStrategy.Instance;
}

var strategies = new List<DelegatingResilienceStrategy>(_entries.Count);

foreach (var entry in _entries)
{
var context = new ResilienceStrategyBuilderContext(
builderName: Options.BuilderName,
strategyName: entry.Properties.StrategyName,
strategyType: entry.Properties.StrategyType);

var strategy = entry.Factory(context);

if (strategy is DelegatingResilienceStrategy delegatingStrategy)
{
strategies.Add(delegatingStrategy);
}
else
{
strategies.Add(new DelegatingStrategyWrapper(strategy));
}
}

for (var i = 0; i < strategies.Count - 1; i++)
{
strategies[i].Next = strategies[i + 1];
}

return new DelegatingStrategyWrapper(strategies[0]);
}

private sealed class Entry
{
public Entry(Func<ResilienceStrategyBuilderContext, IResilienceStrategy> factory, ResilienceStrategyOptions properties)
{
Factory = factory;
Properties = properties;
}

public Func<ResilienceStrategyBuilderContext, IResilienceStrategy> Factory { get; }

public ResilienceStrategyOptions Properties { get; }
}
}
35 changes: 35 additions & 0 deletions src/Polly.Core/Builder/ResilienceStrategyBuilderContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace Polly.Builder;

/// <summary>
/// The context used for building an individual resilience strategy.
/// </summary>
public class ResilienceStrategyBuilderContext
{
/// <summary>
/// Initializes a new instance of the <see cref="ResilienceStrategyBuilderContext"/> class.
/// </summary>
/// <param name="builderName">The name of the builder.</param>
/// <param name="strategyName">The strategy name.</param>
/// <param name="strategyType">The strategy type.</param>
public ResilienceStrategyBuilderContext(string builderName, string strategyName, string strategyType)
{
BuilderName = Guard.NotNull(builderName);
StrategyName = Guard.NotNull(strategyName);
StrategyType = Guard.NotNull(strategyType);
}

/// <summary>
/// Gets the name of the builder.
/// </summary>
public string BuilderName { get; }

/// <summary>
/// Gets the name of the strategy.
/// </summary>
public string StrategyName { get; }

/// <summary>
/// Gets the type of the strategy.
/// </summary>
public string StrategyType { get; }
}
Loading

0 comments on commit 6f3f774

Please sign in to comment.