Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v8.1.9 preview-02 release #142

Merged
merged 5 commits into from
Oct 22, 2024
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,18 @@ Previous classification is not required if changes are simple or all belong to t

## [8.1.9]

### Major Changes

- Created a new project `Encamina.Enmarcha.Aspire` to handle configurations and extensions for Aspire.
- Added `ResourceBuilderExtensions` class in `Encamina.Enmarcha.Aspire.Extensions`, which provides extension methods for configuring resources.
- The method `WithEnvironment<T>` was added to allow adding an array of environment variables to resources in a type-safe manner.

### Minor Changes

- Added the `AuthenticationRequired` property to `SmtpClientOptions.cs`, which is set to `true` by default. This indicates that authentication is required to connect to the SMTP server. If set to `false`, the server does not require authentication, meaning no username or password is needed for the connection.
- Added the `AtLeastOneRequiredAttribute` Data Annotation to validate that at least one of the specified properties has a value.
- Enchanced `JsonUtils` with new methods: `FastCheckIsJson` and `IsAnAdaptiveCard`.
- Added new `AtLeastOneRequiredSchemaFilter` to ensure OpenAPI schemas enforce that at least one of the specified properties is required, by modifying the schema to use the `anyOf` rule in Swagger documentation generation.

## [8.1.8]

Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

<PropertyGroup>
<VersionPrefix>8.1.9</VersionPrefix>
<VersionSuffix>preview-01</VersionSuffix>
<VersionSuffix>preview-02</VersionSuffix>
</PropertyGroup>

<!--
Expand Down
8 changes: 7 additions & 1 deletion Enmarcha.sln
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Encamina.Enmarcha.SemanticK
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Encamina.Enmarcha.Conversation.Abstractions", "src\Encamina.Enmarcha.Conversation.Abstractions\Encamina.Enmarcha.Conversation.Abstractions.csproj", "{F15072A5-238C-4584-87D2-916EDE9A6BAB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Encamina.Enmarcha.Conversation", "src\Encamina.Enmarcha.Conversation\Encamina.Enmarcha.Conversation.csproj", "{854C7D01-FF78-4D69-9EC7-8ECBA0E0FA74}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Encamina.Enmarcha.Conversation", "src\Encamina.Enmarcha.Conversation\Encamina.Enmarcha.Conversation.csproj", "{854C7D01-FF78-4D69-9EC7-8ECBA0E0FA74}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Encamina.Enmarcha.Aspire", "src\Encamina.Enmarcha.Aspire\Encamina.Enmarcha.Aspire.csproj", "{68632D97-CE5C-4817-9ECF-E8A47993A287}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -394,6 +396,10 @@ Global
{854C7D01-FF78-4D69-9EC7-8ECBA0E0FA74}.Debug|Any CPU.Build.0 = Debug|Any CPU
{854C7D01-FF78-4D69-9EC7-8ECBA0E0FA74}.Release|Any CPU.ActiveCfg = Release|Any CPU
{854C7D01-FF78-4D69-9EC7-8ECBA0E0FA74}.Release|Any CPU.Build.0 = Release|Any CPU
{68632D97-CE5C-4817-9ECF-E8A47993A287}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{68632D97-CE5C-4817-9ECF-E8A47993A287}.Debug|Any CPU.Build.0 = Debug|Any CPU
{68632D97-CE5C-4817-9ECF-E8A47993A287}.Release|Any CPU.ActiveCfg = Release|Any CPU
{68632D97-CE5C-4817-9ECF-E8A47993A287}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,9 @@
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Encamina.Enmarcha.Core\Encamina.Enmarcha.Core.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Reflection;

using Encamina.Enmarcha.Core.DataAnnotations;

using Microsoft.OpenApi.Models;

using Swashbuckle.AspNetCore.SwaggerGen;

namespace Encamina.Enmarcha.AspNet.OpenApi.Swashbuckle.SchemaFilters;

/// <summary>
/// This filter enforces that at least one of a specified set of properties must be required in an OpenAPI schema.
/// It processes types decorated with the <see cref="AtLeastOneRequiredAttribute"/>.
/// </summary>
public sealed class AtLeastOneRequiredSchemaFilter : ISchemaFilter
{
/// <summary>
/// Applies the filter to modify the OpenAPI schema based on the <see cref="AtLeastOneRequiredAttribute"/>.
/// It removes individual property requirements and replaces them with an 'anyOf' requirement
/// for the specified properties, ensuring that at least one of them is present in the request.
/// </summary>
/// <param name="schema">The <see cref="OpenApiSchema"/> to modify.</param>
/// <param name="context">
/// The <see cref="SchemaFilterContext"/> containing metadata about the context, including the type being processed.
/// </param>
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
var attribute = context.Type.GetCustomAttribute<AtLeastOneRequiredAttribute>();
if (attribute != null)
{
// Add 'anyOf' to the schema with the specified properties
schema.AnyOf = attribute.PropertyNames.Select(propertyName => new OpenApiSchema
{
Required = new HashSet<string> { propertyName },
}).ToList();
}
}
}
19 changes: 19 additions & 0 deletions src/Encamina.Enmarcha.Aspire/Encamina.Enmarcha.Aspire.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<PropertyGroup>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting" Version="8.2.1" />
</ItemGroup>

<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;

using Encamina.Enmarcha.Aspire.Extensions;

namespace Encamina.Enmarcha.Aspire.Extensions;

/// <summary>
/// Provides extension methods for configuring Aspire resources.
/// </summary>
public static class ResourceBuilderExtensions
{
/// <summary>
/// Adds an environment variable array to the resource.
/// </summary>
/// <typeparam name="T">The resource type.</typeparam>
/// <param name="builder">The resource builder.</param>
/// <param name="name">The name of the environment variable.</param>
/// <param name="values">The array of values of the environment variable.</param>
/// <returns>A resource configured with the specified environment variable.</returns>
public static IResourceBuilder<T> WithEnvironment<T>(this IResourceBuilder<T> builder, string name, string[] values) where T : IResourceWithEnvironment
{
return builder.WithEnvironment(context =>
{
for (var i = 0; i < values.Length; i++)
{
context.EnvironmentVariables[$"{name}:{i}"] = values[i];
}
});
}
}
30 changes: 30 additions & 0 deletions src/Encamina.Enmarcha.Aspire/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Enmarcha Aspire

[![Nuget package](https://img.shields.io/nuget/v/Encamina.Enmarcha.Aspire)](https://www.nuget.org/packages/Encamina.Enmarcha.Aspire)

Aspire Resource Builder Extensions is a set of tools designed to simplify the configuration of cloud-native resources with environment variables. This package is ideal for projects that need to manage distributed application resources in a cloud environment using environment-specific configurations.

## Setup

### Nuget package

First, [install NuGet](http://docs.nuget.org/docs/start-here/installing-nuget). Then, install [Encamina.Enmarcha.Aspire](https://www.nuget.org/packages/Encamina.Enmarcha.Aspire) from the package manager console:

PM> Install-Package Encamina.Enmarcha.Aspire

### .NET CLI:

[Install .NET CLI](https://learn.microsoft.com/en-us/dotnet/core/tools/). Next, install [Encamina.Enmarcha.Aspire](https://www.nuget.org/packages/Encamina.Enmarcha.Aspire) from the .NET CLI:

dotnet add package Encamina.Enmarcha.Aspire

## How to use

### Resource Builder Extensions

The `ResourceBuilderExtensions` class provides a set of extension methods to configure Aspire resources.

```csharp
var agentProjects = builder.AddProject<Projects.AgentProjects>(appIdProjects)
.WithEnvironment(Constants.Settings.Options.PendingTaskStatuses, pendingTaskStatuses)
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;

using Encamina.Enmarcha.Core.Extensions;

using ValidationResultMessages = Encamina.Enmarcha.Core.DataAnnotations.Resources.ValudationResultMessages;

namespace Encamina.Enmarcha.Core.DataAnnotations;

/// <summary>
/// Attribute that enforces validation on a class by requiring at least one of the specified properties to have a non-null or non-empty value.
/// </summary>
/// <remarks>
/// This attribute is typically used on models to ensure that at least one of the specified properties is populated.
/// If none of the specified properties have a value, the validation will fail.
/// </remarks>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class AtLeastOneRequiredAttribute : ValidationAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="AtLeastOneRequiredAttribute"/> class with the specified property names.
/// </summary>
/// <param name="propertyNames">An array of property names that must be validated.</param>
public AtLeastOneRequiredAttribute(params string[] propertyNames)
{
PropertyNames = propertyNames;
}

/// <summary>
/// Gets the array of property names that must be validated to ensure at least one has a value.
/// </summary>
public string[] PropertyNames { get; }

/// <summary>
/// Validates that at least one of the specified properties has a non-null or non-empty value.
/// </summary>
/// <param name="value">The object to validate, typically the class instance this attribute is applied to.</param>
/// <param name="validationContext">Provides contextual information about the validation operation.</param>
/// <returns>
/// <see cref="ValidationResult.Success"/> if at least one property has a value; otherwise, a <see cref="ValidationResult"/> indicating the error.
/// </returns>
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
if (value is null)
{
return ValidationResult.Success;
}

var type = value.GetType();
var atLeastOneHasValue = false;

foreach (var propertyName in PropertyNames)
{
var property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
if (property == null)
{
return new ValidationResult(ValidationResultMessages.ResourceManager.GetFormattedStringByCurrentCulture(nameof(ValidationResultMessages.PropertyNotFound), propertyName));
}

var propertyValue = property.GetValue(value);
if (propertyValue is string str)
{
if (!string.IsNullOrWhiteSpace(str))
{
atLeastOneHasValue = true;
break;

Check warning on line 66 in src/Encamina.Enmarcha.Core/DataAnnotations/AtLeastOneRequiredAttribute.cs

View workflow job for this annotation

GitHub Actions / CI

Refactor the code in order to remove this break statement. (https://rules.sonarsource.com/csharp/RSPEC-1227)

Check warning on line 66 in src/Encamina.Enmarcha.Core/DataAnnotations/AtLeastOneRequiredAttribute.cs

View workflow job for this annotation

GitHub Actions / CI

Refactor the code in order to remove this break statement. (https://rules.sonarsource.com/csharp/RSPEC-1227)
}
}
else if (propertyValue != null)

Check warning on line 69 in src/Encamina.Enmarcha.Core/DataAnnotations/AtLeastOneRequiredAttribute.cs

View workflow job for this annotation

GitHub Actions / CI

Add the missing 'else' clause with either the appropriate action or a suitable comment as to why no action is taken. (https://rules.sonarsource.com/csharp/RSPEC-126)

Check warning on line 69 in src/Encamina.Enmarcha.Core/DataAnnotations/AtLeastOneRequiredAttribute.cs

View workflow job for this annotation

GitHub Actions / CI

Add the missing 'else' clause with either the appropriate action or a suitable comment as to why no action is taken. (https://rules.sonarsource.com/csharp/RSPEC-126)
{
atLeastOneHasValue = true;
break;

Check warning on line 72 in src/Encamina.Enmarcha.Core/DataAnnotations/AtLeastOneRequiredAttribute.cs

View workflow job for this annotation

GitHub Actions / CI

Refactor the code in order to remove this break statement. (https://rules.sonarsource.com/csharp/RSPEC-1227)

Check warning on line 72 in src/Encamina.Enmarcha.Core/DataAnnotations/AtLeastOneRequiredAttribute.cs

View workflow job for this annotation

GitHub Actions / CI

Refactor the code in order to remove this break statement. (https://rules.sonarsource.com/csharp/RSPEC-1227)
}
}

if (!atLeastOneHasValue)
{
var propertyNames = string.Join(", ", PropertyNames);
return new ValidationResult(ValidationResultMessages.ResourceManager.GetFormattedStringByCurrentCulture(nameof(ValidationResultMessages.MissingRequiredProperty), propertyNames));
}

return ValidationResult.Success;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
/// <param name="conditionalValue">The value that triggers the requirement when the condition is met.</param>
/// <param name="allowEmpty">Determines whether the decorated property, if it is a string, can be empty (i.e., <see cref="string.Empty"/> when the condition is met.</param>
/// <param name="failOnAnyCondition">Determines whether the validation should fail if any of the conditions are met.</param>
public RequiredIfAttribute(string conditionalPropertyName, object conditionalValue, bool allowEmpty = false, bool failOnAnyCondition = true)

Check warning on line 26 in src/Encamina.Enmarcha.Core/DataAnnotations/RequiredIfAttribute.cs

View workflow job for this annotation

GitHub Actions / CI

Use the overloading mechanism instead of the optional parameters. (https://rules.sonarsource.com/csharp/RSPEC-2360)

Check warning on line 26 in src/Encamina.Enmarcha.Core/DataAnnotations/RequiredIfAttribute.cs

View workflow job for this annotation

GitHub Actions / CI

Use the overloading mechanism instead of the optional parameters. (https://rules.sonarsource.com/csharp/RSPEC-2360)

Check warning on line 26 in src/Encamina.Enmarcha.Core/DataAnnotations/RequiredIfAttribute.cs

View workflow job for this annotation

GitHub Actions / CI

Use the overloading mechanism instead of the optional parameters. (https://rules.sonarsource.com/csharp/RSPEC-2360)

Check warning on line 26 in src/Encamina.Enmarcha.Core/DataAnnotations/RequiredIfAttribute.cs

View workflow job for this annotation

GitHub Actions / CI

Use the overloading mechanism instead of the optional parameters. (https://rules.sonarsource.com/csharp/RSPEC-2360)
: this([conditionalPropertyName], [conditionalValue], allowEmpty, failOnAnyCondition)
{
}
Expand All @@ -38,7 +38,7 @@
/// <exception cref="ArgumentException">
/// Thrown when the number of items in <paramref name="conditionalPropertyNames"/> and items in <paramref name="conditionalValues"/> do not match.
/// </exception>
public RequiredIfAttribute(string[] conditionalPropertyNames, object[] conditionalValues, bool allowEmpty = false, bool failOnAnyCondition = true)

Check warning on line 41 in src/Encamina.Enmarcha.Core/DataAnnotations/RequiredIfAttribute.cs

View workflow job for this annotation

GitHub Actions / CI

Use the overloading mechanism instead of the optional parameters. (https://rules.sonarsource.com/csharp/RSPEC-2360)

Check warning on line 41 in src/Encamina.Enmarcha.Core/DataAnnotations/RequiredIfAttribute.cs

View workflow job for this annotation

GitHub Actions / CI

Use the overloading mechanism instead of the optional parameters. (https://rules.sonarsource.com/csharp/RSPEC-2360)
{
if (conditionalPropertyNames.Length != conditionalValues.Length)
{
Expand Down Expand Up @@ -69,7 +69,7 @@

if (propertyInfo == null)
{
return new ValidationResult(ValudationResultMessages.ResourceManager.GetFormattedStringByCurrentCulture(nameof(ValudationResultMessages.MissingPropertyRequiredIf), propertyName));
return new ValidationResult(ValudationResultMessages.ResourceManager.GetFormattedStringByCurrentCulture(nameof(ValudationResultMessages.PropertyNotFound), propertyName));
}

var propertyValue = propertyInfo.GetValue(objInstance);
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,13 @@
<data name="ValuieIsInvalidUri" xml:space="preserve">
<value>The value of '{0}' must be a valid and well formed URI!</value>
</data>
<data name="MissingPropertyRequiredIf" xml:space="preserve">
<data name="PropertyNotFound" xml:space="preserve">
<value>Property {0} not found!</value>
</data>
<data name="PropertiesConditionsMissmatchRequiredIf" xml:space="preserve">
<value>The number of conditional property names must match the number of expected conditional values.</value>
</data>
<data name="MissingRequiredProperty" xml:space="preserve">
<value>At least one of the following properties must be provided: {0}.</value>
</data>
</root>
67 changes: 67 additions & 0 deletions src/Encamina.Enmarcha.Core/JsonUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,74 @@
/// <param name="anonymousType">The anonymous type that identifies the type to deserialize from a JSON string.</param>
/// <param name="options">A valid instance of <see cref="JsonSerializerOptions"/> with options for the deserialization.</param>
/// <returns>A valid instance of <typeparamref name="T"/> obtained from the JSON deserialization.</returns>
public static T? DeserializeAnonymousType<T>(string json, T anonymousType, JsonSerializerOptions? options = default) => JsonSerializer.Deserialize<T>(json, options);

Check warning on line 23 in src/Encamina.Enmarcha.Core/JsonUtils.cs

View workflow job for this annotation

GitHub Actions / CI

Use the overloading mechanism instead of the optional parameters. (https://rules.sonarsource.com/csharp/RSPEC-2360)

/// <summary>
/// Performs a fast check to determine if the input string is a JSON object.
/// This method only checks if the string starts with '{' and ends with '}'.
/// It does not validate the entire JSON structure.
/// </summary>
/// <param name="input">The input string to check.</param>
/// <returns>
/// <c>true</c> if the input starts with '{' and ends with '}'; otherwise, <c>false</c>.
/// </returns>
public static bool FastCheckIsJson(string input)
{
input = input.Trim();
return input.StartsWith('{') && input.EndsWith('}');
}

/// <summary>
/// Determines whether the given JSON string represents an Adaptive Card.
/// An Adaptive Card must have a 'type' property with the value 'AdaptiveCard' and a Adaptive Card schema '$schema'.
/// </summary>
/// <param name="input">The input string to check, which is expected to be a JSON object.</param>
/// <returns>
/// <c>true</c> if the input is a valid JSON object representing an Adaptive Card; otherwise, <c>false</c>.
/// </returns>
public static bool IsAnAdaptiveCard(string input)
{
if (string.IsNullOrWhiteSpace(input))
{
return false;
}

if (!FastCheckIsJson(input))
{
return false;
}

try
{
using var document = JsonDocument.Parse(input);
var root = document.RootElement;

if (root.ValueKind != JsonValueKind.Object)
{
return false;
}

// Check for required properties: 'type' and '$schema'
if (!root.TryGetProperty("type", out var typeElement) ||
!root.TryGetProperty("$schema", out var schemaElement))
{
return false;
}

return string.Equals(typeElement.GetString(), @"AdaptiveCard", StringComparison.OrdinalIgnoreCase) &&
schemaElement.GetString()!.EndsWith(@"adaptivecards.io/schemas/adaptive-card.json", StringComparison.OrdinalIgnoreCase);
}
catch (JsonException)
{
// input is not a valid JSON object
return false;
}
catch (InvalidOperationException)
{
// Handle potential exceptions due to invalid JSON structure
return false;
}
}
}

#pragma warning restore IDE0060
Loading