Skip to content

Commit

Permalink
Add ObjectPath to AspNetItemValueLayoutRenderer (#890)
Browse files Browse the repository at this point in the history
Co-authored-by: Burak Akgerman <burak.akgerman@huntington.com>
  • Loading branch information
bakgerman and Burak Akgerman committed Nov 27, 2022
1 parent 721f4d5 commit 3364c23
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 16 deletions.
47 changes: 45 additions & 2 deletions src/Shared/Internal/PropertyReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,31 @@ public static object GetValue<T>(string key, T container, Func<T, string, object
return null;
}

var value = evaluateAsNestedProperties ? GetValueAsNestedProperties(key, container, getVal) : getVal(container, key);
return value;
return evaluateAsNestedProperties ? GetValueAsNestedProperties(key, container, getVal) : getVal(container, key);
}

/// <summary>
/// Get value of a property
/// </summary>
/// <param name="key">key</param>
/// <param name="container">Container to perform value lookup using key</param>
/// <param name="getVal">function to get a value with this key</param>
/// <param name="objectPath">evaluate the string as a dot notated path to a aproperty in the object, returned by lookup by the key parameter</param>
/// <returns>value</returns>
public static object GetValue<T>(string key, T container, Func<T, string, object> getVal, string objectPath)
{
if (string.IsNullOrEmpty(key))
{
return null;
}

var value = getVal(container, key);
if (value == null)
{
return null;
}

return GetValueAsNestedProperties(value, objectPath);
}

private static object GetValueAsNestedProperties<T>(string key, T container, Func<T, string, object> getVal)
Expand All @@ -46,6 +69,26 @@ private static object GetValueAsNestedProperties<T>(string key, T container, Fun
return value;
}

private static object GetValueAsNestedProperties(object value, string objectPath)
{
var path = objectPath.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);

if (value != null && path?.Length > 0)
{
for (int i = 0; i < path.Length; ++i)
{
var propertyInfo = GetPropertyInfo(value, path[i]);
value = propertyInfo?.GetValue(value, null);
if (value == null)
{
break;
}
}
}

return value;
}

private static PropertyInfo GetPropertyInfo(object value, string propertyName)
{
#if !ASP_NET_CORE
Expand Down
39 changes: 33 additions & 6 deletions src/Shared/LayoutRenderers/AspNetItemValueLayoutRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,31 @@ public class AspNetItemValueLayoutRenderer : AspNetLayoutRendererBase
/// Gets or sets the item variable name.
/// </summary>
/// <docgen category='Rendering Options' order='10' />
[RequiredParameter]
[DefaultParameter]
[RequiredParameter]
public string Item { get; set; }

/// <summary>
/// Gets or sets the object-property-navigation-path for lookup of nested property.
/// In this case the Item should have have any dot notation, as the nested properties path is in this variable
/// Example:
/// Item="person";
/// ObjectPath="Name.First"
/// This will emit the First Name property of the object in HttpContext.Items woith the key of 'person' in the collection
/// </summary>
/// <docgen category='Layout Options' order='20' />
public string ObjectPath { get; set; }

/// <summary>
/// Gets or sets the item variable name.
/// </summary>
/// <docgen category='Rendering Options' order='10' />
public string Variable { get => Item; set => Item = value; }

/// <summary>
/// Gets or sets whether items with a dot are evaluated as properties or not
/// Gets or sets whether the Item string with a dot are evaluated as properties or not
/// If ObjectPath is not null, the Item should have no dot notation and nested properties will be automatically
/// invoked since ObjectPath is set
/// </summary>
/// <docgen category='Rendering Options' order='10' />
public bool EvaluateAsNestedProperties { get; set; }
Expand All @@ -71,12 +84,26 @@ public class AspNetItemValueLayoutRenderer : AspNetLayoutRendererBase
/// <inheritdoc/>
protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent)
{
var item = Item;
if (item == null)
var context = HttpContextAccessor.HttpContext;

if (Item == null)
{
return;
}

object value = null;

// Function using the Item string as the object path
if (ObjectPath == null)
{
value = PropertyReader.GetValue(Item, context?.Items, (items, key) => LookupItemValue(items, key), EvaluateAsNestedProperties);
}
// Function using the ObjectPath as the object path, hard code evaluateNestedProperties argument to true
else
{
value = PropertyReader.GetValue(Item, context?.Items, (items, key) => LookupItemValue(items, key), ObjectPath);
}

var context = HttpContextAccessor.HttpContext;
var value = PropertyReader.GetValue(item, context?.Items, (items, key) => LookupItemValue(items, key), EvaluateAsNestedProperties);
var formatProvider = GetFormatProvider(logEvent, Culture);
builder.AppendFormattedValue(value, Format, formatProvider, ValueFormatter);
}
Expand Down
165 changes: 157 additions & 8 deletions tests/Shared/LayoutRenderers/AspNetItemValueLayoutRendererTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,32 @@ public void VariableFoundRendersValue(object expectedValue)
}

[Theory, MemberData(nameof(NestedPropertyData))]
public void NestedPropertyRendersValue(string itemKey, string variable, object data, object expectedValue)
public void NestedPropertyRendersValueItem(string itemKey, string variable, object data, object expectedValue)
{
// Arrange
var (renderer, httpContext) = CreateWithHttpContext();
#if ASP_NET_CORE
httpContext.Items = new Dictionary<object, object> {{itemKey, data}};
#else
httpContext.Items.Count.Returns(1);
httpContext.Items.Contains(itemKey).Returns(true);
httpContext.Items[itemKey].Returns(data);
#endif
var culture = CultureInfo.CurrentUICulture;
renderer.Variable = variable;
renderer.EvaluateAsNestedProperties = true;
renderer.Culture = culture;

// Act
string result = renderer.Render(new LogEventInfo());

// Assert
Assert.Equal(Convert.ToString(expectedValue, culture), result);
}

[Theory, MemberData(nameof(NestedPropertyData))]
public void NestedPropertyRendersValueObjectPath(string itemKey, string variable, object data,
object expectedValue)
{
// Arrange
var (renderer, httpContext) = CreateWithHttpContext();
Expand All @@ -127,21 +152,145 @@ public static IEnumerable<object[]> VariableFoundData
{
get
{
yield return new object[] { "string" };
yield return new object[] { 1 };
yield return new object[] { 1.5 };
yield return new object[] { DateTime.Now };
yield return new object[] { Tuple.Create("a", 1) };
yield return new object[] {"string"};
yield return new object[] {1};
yield return new object[] {1.5};
yield return new object[] {DateTime.Now};
yield return new object[] {Tuple.Create("a", 1)};
}
}

public static IEnumerable<object[]> NestedPropertyData
{
get
{
yield return new object[] { "key", "key.Item1", Tuple.Create("value"), "value" };
yield return new object[] { "key", "key.Item1.Item1", Tuple.Create(Tuple.Create(1)), 1 };
yield return new object[] {"key", "key.Item1", Tuple.Create("value"), "value"};
yield return new object[] {"key", "key.Item1.Item1", Tuple.Create(Tuple.Create(1)), 1};
}
}


// Nested Properties Tests
internal class Person
{
public Name Name { get; set; }
}

internal class Name
{
public string First { get; set; }
public string Last { get; set; }
}

[Fact]
public void NestedItemRendersProperly()
{
// Arrange
var (renderer, httpContext) = CreateWithHttpContext();

string expectedValue = "John";

Person person = new Person
{
Name = new Name
{
First = "John",
Last = "Smith"
}
};

#if ASP_NET_CORE
httpContext.Items = new Dictionary<object, object>();
httpContext.Items.Add("person", person);
#else
httpContext.Items.Count.Returns(1);
httpContext.Items.Contains("person").Returns(true);
httpContext.Items["person"].Returns(person);
#endif
renderer.Item = "person.Name.First";

renderer.EvaluateAsNestedProperties = true;

// Act
string result = renderer.Render(new LogEventInfo());

// Assert
Assert.Equal(expectedValue, result);
}


[Fact]
public void NestedObjectPathRendersProperly()
{
// Arrange
var (renderer, httpContext) = CreateWithHttpContext();

string expectedValue = "Smith";

Person person = new Person
{
Name = new Name
{
First = "John",
Last = "Smith"
}
};

#if ASP_NET_CORE
httpContext.Items = new Dictionary<object, object>();
httpContext.Items.Add("person", person);
#else
httpContext.Items.Count.Returns(1);
httpContext.Items.Contains("person").Returns(true);
httpContext.Items["person"].Returns(person);
#endif
renderer.Item = "person";
renderer.ObjectPath = "Name.Last";

renderer.EvaluateAsNestedProperties = false;

// Act
string result = renderer.Render(new LogEventInfo());

// Assert
Assert.Equal(expectedValue, result);
}

[Fact]
public void NestedObjectPathRendersProperlyII()
{
// Arrange
var (renderer, httpContext) = CreateWithHttpContext();

string expectedValue = "Smith";

Person person = new Person
{
Name = new Name
{
First = "John",
Last = "Smith"
}
};

#if ASP_NET_CORE
httpContext.Items = new Dictionary<object, object>();
httpContext.Items.Add("person", person);
#else
httpContext.Items.Count.Returns(1);
httpContext.Items.Contains("person").Returns(true);
httpContext.Items["person"].Returns(person);
#endif
renderer.Item = "person";
renderer.ObjectPath = "Name.Last";

renderer.EvaluateAsNestedProperties = false;

// Act
string result = renderer.Render(new LogEventInfo());

// Assert
Assert.Equal(expectedValue, result);
}
}
}

0 comments on commit 3364c23

Please sign in to comment.