Merge remote-tracking branch 'origin/patch/3.5.2' into develop/3.6.0
This commit is contained in:
commit
54af5ea714
|
|
@ -126,7 +126,7 @@ services.Configure<RecurringTaskOptions>(options =>
|
|||
});
|
||||
|
||||
services.Configure<RuntimeOptions>(options => { options.InactivityThreshold = TimeSpan.FromSeconds(15); });
|
||||
services.Configure<BookmarkQueuePurgeOptions>(options => options.Ttl = TimeSpan.FromSeconds(10));
|
||||
services.Configure<BookmarkQueuePurgeOptions>(options => options.Ttl = TimeSpan.FromSeconds(3600));
|
||||
services.Configure<CachingOptions>(options => options.CacheDuration = TimeSpan.FromDays(1));
|
||||
services.Configure<IncidentOptions>(options => options.DefaultIncidentStrategy = typeof(ContinueWithIncidentsStrategy));
|
||||
services.AddHealthChecks();
|
||||
|
|
|
|||
105
src/apps/Elsa.Server.Web/Workflows/triggers.json
Normal file
105
src/apps/Elsa.Server.Web/Workflows/triggers.json
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
{
|
||||
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
|
||||
"id": "3b42d3276206e00f",
|
||||
"definitionId": "fb27085e78433f79",
|
||||
"name": "Triggers",
|
||||
"createdAt": "2025-10-09T07:37:31.521686\u002B00:00",
|
||||
"version": 3,
|
||||
"toolVersion": "3.5.0.0",
|
||||
"variables": [],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"outcomes": [],
|
||||
"customProperties": {},
|
||||
"isReadonly": false,
|
||||
"isSystem": false,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"options": {
|
||||
"usableAsActivity": false,
|
||||
"autoUpdateConsumingWorkflows": false
|
||||
},
|
||||
"root": {
|
||||
"id": "83f95a4ca749c950",
|
||||
"nodeId": "Workflow1:83f95a4ca749c950",
|
||||
"name": "Flowchart1",
|
||||
"type": "Elsa.Flowchart",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"notFoundConnections": [],
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {},
|
||||
"activities": [
|
||||
{
|
||||
"path": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "triggers"
|
||||
}
|
||||
},
|
||||
"supportedMethods": {
|
||||
"typeName": "List\u003CString\u003E",
|
||||
"expression": {
|
||||
"type": "Object",
|
||||
"value": "[\u0022GET\u0022]"
|
||||
}
|
||||
},
|
||||
"authorize": {
|
||||
"typeName": "Boolean",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
"policy": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal"
|
||||
}
|
||||
},
|
||||
"requestTimeout": null,
|
||||
"requestSizeLimit": null,
|
||||
"fileSizeLimit": null,
|
||||
"allowedFileExtensions": null,
|
||||
"blockedFileExtensions": null,
|
||||
"allowedMimeTypes": null,
|
||||
"exposeRequestTooLargeOutcome": false,
|
||||
"exposeFileTooLargeOutcome": false,
|
||||
"exposeInvalidFileExtensionOutcome": false,
|
||||
"exposeInvalidFileMimeTypeOutcome": false,
|
||||
"parsedContent": null,
|
||||
"files": null,
|
||||
"routeData": null,
|
||||
"queryStringData": null,
|
||||
"headers": null,
|
||||
"result": null,
|
||||
"id": "607c2042a187fe5a",
|
||||
"nodeId": "Workflow1:83f95a4ca749c950:607c2042a187fe5a",
|
||||
"name": "HttpEndpoint1",
|
||||
"type": "Elsa.HttpEndpoint",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": true,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": -316.431640625,
|
||||
"y": -470.017578125
|
||||
},
|
||||
"size": {
|
||||
"width": 195.1328125,
|
||||
"height": 67.9765625
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"connections": []
|
||||
}
|
||||
}
|
||||
|
|
@ -217,11 +217,16 @@ public static class ObjectConverter
|
|||
var sourceTypeConverter = TypeDescriptor.GetConverter(underlyingSourceType);
|
||||
|
||||
if (sourceTypeConverter.CanConvertTo(underlyingTargetType))
|
||||
{
|
||||
var isValid = targetTypeConverter.IsValid(value);
|
||||
|
||||
if (isValid)
|
||||
return sourceTypeConverter.ConvertTo(value, 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.
|
||||
}
|
||||
}
|
||||
|
||||
if (underlyingTargetType.IsEnum)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ public class FlowScope
|
|||
/// </summary>
|
||||
/// <param name="activity">The activity to check.</param>
|
||||
/// <returns>The visit count of the activity.</returns>
|
||||
private long GetActivityVisitCount(IActivity activity) => ActivitiesVisitCount.TryGetValue(activity.Id, out var count) ? count : 0;
|
||||
private long GetActivityVisitCount(IActivity activity) => ActivitiesVisitCount.GetValueOrDefault(activity.Id, 0);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a visit to the specified connection and records whether it was followed.
|
||||
|
|
@ -46,7 +46,7 @@ public class FlowScope
|
|||
/// <param name="followed">Indicates whether the connection was followed.</param>
|
||||
public void RegisterConnectionVisit(Connection connection, bool followed)
|
||||
{
|
||||
string connectionId = connection.ToString();
|
||||
var connectionId = connection.ToString();
|
||||
ConnectionVisitCount.TryAdd(connectionId, 0);
|
||||
ConnectionVisitCount[connectionId]++;
|
||||
ConnectionLastVisitFollowed[connectionId] = followed;
|
||||
|
|
@ -57,14 +57,14 @@ public class FlowScope
|
|||
/// </summary>
|
||||
/// <param name="connection">The connection to check.</param>
|
||||
/// <returns>The visit count of the connection.</returns>
|
||||
private long GetConnectionVisitCount(Connection connection) => ConnectionVisitCount.TryGetValue(connection.ToString(), out var count) ? count : 0;
|
||||
private long GetConnectionVisitCount(Connection connection) => ConnectionVisitCount.GetValueOrDefault(connection.ToString(), 0);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the last visit to the specified connection was followed.
|
||||
/// </summary>
|
||||
/// <param name="connection">The connection to check.</param>
|
||||
/// <returns>True if the connection was followed on the last visit, otherwise false.</returns>
|
||||
private bool GetConnectionLastVisitFollowed(Connection connection) => ConnectionLastVisitFollowed.TryGetValue(connection.ToString(), out var followed) ? followed : false;
|
||||
public bool GetConnectionLastVisitFollowed(Connection connection) => ConnectionLastVisitFollowed.GetValueOrDefault(connection.ToString(), false);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether all inbound connections to the specified activity have been visited.
|
||||
|
|
@ -76,7 +76,7 @@ public class FlowScope
|
|||
{
|
||||
var forwardInboundConnections = flowGraph.GetForwardInboundConnections(activity);
|
||||
var outboundActivityVisitCount = GetActivityVisitCount(activity);
|
||||
var minConnectionVisitCount = forwardInboundConnections.Min(c => GetConnectionVisitCount(c));
|
||||
var minConnectionVisitCount = forwardInboundConnections.Min(GetConnectionVisitCount);
|
||||
return minConnectionVisitCount > outboundActivityVisitCount;
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ public class FlowScope
|
|||
{
|
||||
var forwardInboundConnections = flowGraph.GetForwardInboundConnections(activity);
|
||||
var outboundActivityVisitCount = GetActivityVisitCount(activity);
|
||||
var maxConnectionVisitCount = forwardInboundConnections.Max(c => GetConnectionVisitCount(c));
|
||||
var maxConnectionVisitCount = forwardInboundConnections.Max(GetConnectionVisitCount);
|
||||
return maxConnectionVisitCount > outboundActivityVisitCount
|
||||
&& forwardInboundConnections.Any(c => GetConnectionVisitCount(c) == maxConnectionVisitCount && GetConnectionLastVisitFollowed(c));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,8 +83,8 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP
|
|||
await AssignIdentities(materializedWorkflow.Workflow, cancellationToken);
|
||||
var workflowDefinition = await AddOrUpdateAsync(materializedWorkflow, cancellationToken);
|
||||
|
||||
if (indexTriggers)
|
||||
await IndexTriggersAsync(workflowDefinition, cancellationToken);
|
||||
if (indexTriggers && workflowDefinition.IsPublished)
|
||||
await _triggerIndexer.IndexTriggersAsync(workflowDefinition, cancellationToken);
|
||||
|
||||
return workflowDefinition;
|
||||
}
|
||||
|
|
@ -260,8 +260,6 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP
|
|||
}
|
||||
}
|
||||
|
||||
private async Task IndexTriggersAsync(WorkflowDefinition workflowDefinition, CancellationToken cancellationToken) => await _triggerIndexer.IndexTriggersAsync(workflowDefinition, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Syncs the items in the primary list with existing items in the secondary list, even when the object instances are not the same (but their IDs are).
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ public class TriggerIndexer : ITriggerIndexer
|
|||
// Collect new triggers **if the workflow is published**.
|
||||
var newTriggers = workflow.Publication.IsPublished
|
||||
? await GetTriggersInternalAsync(workflow, cancellationToken).ToListAsync(cancellationToken)
|
||||
: new List<StoredTrigger>(0);
|
||||
: new(0);
|
||||
|
||||
// Diff triggers.
|
||||
var diff = Diff.For(currentTriggers, newTriggers, new WorkflowTriggerEqualityComparer());
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using Elsa.Workflows.ComponentTests.Materializers;
|
|||
using Elsa.Workflows.ComponentTests.WorkflowProviders;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Elsa.Workflows.Runtime.Filters;
|
||||
using Humanizer;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
|
|
@ -20,6 +21,8 @@ public class ReloadWorkflowTests : AppComponentTest
|
|||
private readonly IWorkflowDefinitionManager _workflowDefinitionManager;
|
||||
private readonly IWorkflowDefinitionService _workflowDefinitionService;
|
||||
private readonly IWorkflowDefinitionsReloader _workflowDefinitionsReloader;
|
||||
private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher;
|
||||
private readonly ITriggerStore _triggerStore;
|
||||
|
||||
public ReloadWorkflowTests(App app) : base(app)
|
||||
{
|
||||
|
|
@ -27,7 +30,9 @@ public class ReloadWorkflowTests : AppComponentTest
|
|||
_workflowDefinitionsReloader = Scope.ServiceProvider.GetRequiredService<IWorkflowDefinitionsReloader>();
|
||||
_workflowBuilderFactory = Scope.ServiceProvider.GetRequiredService<IWorkflowBuilderFactory>();
|
||||
_workflowDefinitionService = Scope.ServiceProvider.GetRequiredService<IWorkflowDefinitionService>();
|
||||
_workflowDefinitionPublisher = Scope.ServiceProvider.GetRequiredService<IWorkflowDefinitionPublisher>();
|
||||
_activityRegistry = Scope.ServiceProvider.GetRequiredService<IActivityRegistry>();
|
||||
_triggerStore = Scope.ServiceProvider.GetRequiredService<ITriggerStore>();
|
||||
var workflowsProviders = Scope.ServiceProvider.GetRequiredService<IEnumerable<IWorkflowsProvider>>();
|
||||
_testWorkflowProvider = (TestWorkflowProvider)workflowsProviders.First(x => x is TestWorkflowProvider);
|
||||
}
|
||||
|
|
@ -37,9 +42,9 @@ public class ReloadWorkflowTests : AppComponentTest
|
|||
{
|
||||
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||
await _workflowDefinitionManager.DeleteByDefinitionIdAsync("f68b09bc-2013-4617-b82f-d76b6819a624", CancellationToken.None);
|
||||
var firstResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "reload-test"));
|
||||
var firstResponse = await client.SendAsync(new(HttpMethod.Get, "reload-test"));
|
||||
await _workflowDefinitionsReloader.ReloadWorkflowDefinitionsAsync();
|
||||
var secondResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "reload-test"));
|
||||
var secondResponse = await client.SendAsync(new(HttpMethod.Get, "reload-test"));
|
||||
Assert.Equal(HttpStatusCode.NotFound, firstResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, secondResponse.StatusCode);
|
||||
}
|
||||
|
|
@ -103,6 +108,36 @@ public class ReloadWorkflowTests : AppComponentTest
|
|||
await _workflowDefinitionManager.DeleteByDefinitionIdAsync(definitionId, CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reloading_AfterPublishingNewVersion_ShouldPersistTriggers()
|
||||
{
|
||||
// Get the initial workflow definition.
|
||||
const string definitionId = "f68b09bc-2013-4617-b82f-d76b6819a624";
|
||||
var initialDefinition = await _workflowDefinitionService.FindWorkflowDefinitionAsync(definitionId, VersionOptions.Published, CancellationToken.None);
|
||||
Assert.NotNull(initialDefinition);
|
||||
|
||||
// Assert that triggers exist initially.
|
||||
var initialTrigger = await _triggerStore.FindAsync(new(){ WorkflowDefinitionId = definitionId}, CancellationToken.None);
|
||||
Assert.NotNull(initialTrigger);
|
||||
|
||||
// Publish a new version of the workflow.
|
||||
var draftDefinition = await _workflowDefinitionPublisher.GetDraftAsync(definitionId, VersionOptions.Latest);
|
||||
Assert.NotNull(draftDefinition);
|
||||
await _workflowDefinitionPublisher.PublishAsync(draftDefinition, CancellationToken.None);
|
||||
|
||||
// Assert we are at version 2.
|
||||
var v2Definition = await _workflowDefinitionService.FindWorkflowDefinitionAsync(definitionId, VersionOptions.Published, CancellationToken.None);
|
||||
Assert.NotNull(v2Definition);
|
||||
Assert.Equal(2, v2Definition.Version);
|
||||
|
||||
// Reload the workflow definitions.
|
||||
await _workflowDefinitionsReloader.ReloadWorkflowDefinitionsAsync();
|
||||
|
||||
// Assert that triggers still exist after reload.
|
||||
var reloadedTrigger = await _triggerStore.FindAsync(new(){ WorkflowDefinitionId = definitionId}, CancellationToken.None);
|
||||
Assert.NotNull(reloadedTrigger);
|
||||
}
|
||||
|
||||
private async Task<MaterializedWorkflow> BuildWorkflowAsync(string definitionId, string definitionVersionId, int version)
|
||||
{
|
||||
var builder = _workflowBuilderFactory.CreateBuilder();
|
||||
|
|
@ -114,6 +149,6 @@ public class ReloadWorkflowTests : AppComponentTest
|
|||
builder.WorkflowOptions.UsableAsActivity = true;
|
||||
var workflow = await builder.BuildWorkflowAsync();
|
||||
workflow.Name = definitionId;
|
||||
return new MaterializedWorkflow(workflow, _testWorkflowProvider.Name, TestWorkflowMaterializer.MaterializerName);
|
||||
return new(workflow, _testWorkflowProvider.Name, TestWorkflowMaterializer.MaterializerName);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{ca65b7d5-57e8-4932-b8b8-732d040c9b3a}</ProjectGuid>
|
||||
|
|
@ -23,6 +23,9 @@
|
|||
<None Update="Scenarios\DependencyWorkflowsPublishing\Workflows\parent.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Scenarios\ImplicitJoins\Workflows\decision-implicit-join-on-both-outcomes.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Scenarios\ImplicitJoins\Workflows\fork-decision-join-converge.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
namespace Elsa.Workflows.Core.UnitTests.ObjectConversion
|
||||
{
|
||||
public class Person
|
||||
{
|
||||
public double? Age { get; set; }
|
||||
namespace Elsa.Workflows.Core.UnitTests.ObjectConversion;
|
||||
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
public class Person
|
||||
{
|
||||
public double? Age { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Elsa.Workflows.Core.UnitTests.ObjectConversion;
|
||||
|
||||
public class PersonTypeConverter : TypeConverter
|
||||
{
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
|
||||
{
|
||||
// Allow conversion from string to Person
|
||||
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
|
||||
}
|
||||
|
||||
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
|
||||
{
|
||||
// Allow conversion from Person to string
|
||||
return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
|
||||
}
|
||||
|
||||
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value is string json)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<Person>(json);
|
||||
}
|
||||
|
||||
return base.ConvertFrom(context, culture, value);
|
||||
}
|
||||
|
||||
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
|
||||
{
|
||||
if (destinationType == typeof(string) && value is Person person)
|
||||
{
|
||||
return JsonSerializer.Serialize(person);
|
||||
}
|
||||
|
||||
return base.ConvertTo(context, culture, value, destinationType);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using System.ComponentModel;
|
||||
using System.Dynamic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
|
@ -352,4 +353,99 @@ public class Tests
|
|||
if(shouldThrow)
|
||||
Assert.Throws<TypeConversionException>(() => input.ConvertTo(targetType, options));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertTo_WithRegisteredPersonTypeConverter_ConvertsFromJsonToPerson()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
TypeDescriptor.AddAttributes(typeof(Person), new TypeConverterAttribute(typeof(PersonTypeConverter)));
|
||||
|
||||
var json = "{\"Name\":\"Alice\",\"Age\":30}";
|
||||
|
||||
// Act
|
||||
var result = json.ConvertTo<Person>(_objectConverterOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<Person>(result);
|
||||
Assert.Equal("Alice", result.Name);
|
||||
Assert.Equal(30, result.Age);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Clean up type descriptor cache so other tests aren't affected
|
||||
TypeDescriptor.Refresh(typeof(Person));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertTo_WithRegisteredPersonTypeConverter_NullInput_ReturnsNull()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
TypeDescriptor.AddAttributes(typeof(Person), new TypeConverterAttribute(typeof(PersonTypeConverter)));
|
||||
|
||||
string? json = null;
|
||||
|
||||
// Act
|
||||
var result = json.ConvertTo<Person>(_objectConverterOptions);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TypeDescriptor.Refresh(typeof(Person));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertTo_WithRegisteredPersonTypeConverter_ConvertsFromPersonToJson()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
TypeDescriptor.AddAttributes(typeof(Person), new TypeConverterAttribute(typeof(PersonTypeConverter)));
|
||||
|
||||
var person = new Person { Name = "Bob", Age = 42 };
|
||||
|
||||
// Act
|
||||
var result = person.ConvertTo<string>(_objectConverterOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
var json = Assert.IsType<string>(result);
|
||||
Assert.Contains("\"Name\":\"Bob\"", json);
|
||||
Assert.Contains("\"Age\":42", json);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Clean up to avoid polluting TypeDescriptor globally
|
||||
TypeDescriptor.Refresh(typeof(Person));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertTo_WithRegisteredPersonTypeConverter_NullPersonToString_ReturnsNull()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
TypeDescriptor.AddAttributes(typeof(Person), new TypeConverterAttribute(typeof(PersonTypeConverter)));
|
||||
Person? person = null;
|
||||
|
||||
// Act
|
||||
var result = person.ConvertTo<string>(_objectConverterOptions);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TypeDescriptor.Refresh(typeof(Person));
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue