Skip to content

Commit

Permalink
Add ReplaceIllegalFieldNameCharactersAttribute, add related tests, fi…
Browse files Browse the repository at this point in the history
…xes issue OData#1269
  • Loading branch information
marcus905 committed Jul 5, 2024
1 parent 599dc1b commit a2d55f4
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.Linq;

namespace Microsoft.AspNetCore.OData.Formatter.Attributes
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
sealed class ReplaceIllegalFieldNameCharactersAttribute : Attribute
{
//constant collection of illegal characters
private static readonly string[] illegalChars = new string[] { "@", ":", "." };
public string ReplaceAt { get; }
public string ReplaceColon { get; }
public string ReplaceDot { get; }

public ReplaceIllegalFieldNameCharactersAttribute(string replaceAt, string replaceColon, string replaceDot)
{
//assert that the replacements are not illegal either
if (illegalChars.Contains(replaceAt) || illegalChars.Contains(replaceColon) || illegalChars.Contains(replaceDot))
{
throw new ArgumentException("Replacement characters cannot be illegal characters");
}

ReplaceAt = replaceAt;
ReplaceColon = replaceColon;
ReplaceDot = replaceDot;
}

public ReplaceIllegalFieldNameCharactersAttribute(string replaceAnyIllegal)
{
if (illegalChars.Contains(replaceAnyIllegal))
{
throw new ArgumentException("Replacement character cannot be an illegal character");
}

ReplaceAt = replaceAnyIllegal;
ReplaceColon = replaceAnyIllegal;
ReplaceDot = replaceAnyIllegal;
}

public ReplaceIllegalFieldNameCharactersAttribute()
{
ReplaceAt = "_";
ReplaceColon = "_";
ReplaceDot = "_";
}

public string Replace(string fieldName)
{
return fieldName.Replace("@", ReplaceAt).Replace(":", ReplaceColon).Replace(".", ReplaceDot);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
using Microsoft.AspNetCore.OData.Common;
using System.Threading.Tasks;
using Microsoft.AspNetCore.OData.Deltas;
using Microsoft.AspNetCore.OData.Formatter.Attributes;

namespace Microsoft.AspNetCore.OData.Formatter.Serialization
{
Expand Down Expand Up @@ -551,6 +552,20 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R
// Try to add the dynamic properties if the structural type is open.
AppendDynamicProperties(resource, selectExpandNode, resourceContext);

// check if the type is annotated with ReplaceIllegalFieldNameCharactersAttribute and replace the illegal characters in the field names
var resourceInstance = resourceContext.ResourceInstance;
if (resourceInstance != null)
{
var replaceIllegalFieldNameCharactersAttribute = resourceInstance.GetType().GetCustomAttribute<ReplaceIllegalFieldNameCharactersAttribute>();
if (replaceIllegalFieldNameCharactersAttribute != null)
{
foreach (var property in resource.Properties)
{
property.Name = replaceIllegalFieldNameCharactersAttribute.Replace(property.Name);
}
}
}

if (selectExpandNode.SelectedActions != null)
{
IEnumerable<ODataAction> actions = CreateODataActions(selectExpandNode.SelectedActions, resourceContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.OData.Extensions;
using Microsoft.AspNetCore.OData.Formatter;
using Microsoft.AspNetCore.OData.Formatter.Attributes;
using Microsoft.AspNetCore.OData.Formatter.Value;
using Microsoft.AspNetCore.OData.Tests.Commons;
using Microsoft.AspNetCore.OData.Tests.Extensions;
Expand Down Expand Up @@ -232,9 +235,107 @@ public void TryGetContentHeaderODataOutputFormatter_ThrowsArgumentNull_Type()
ExceptionAssert.ThrowsArgumentNull(() => ODataOutputFormatter.TryGetContentHeader(null, null, out _), "type");
}

[Fact]
public void SerializeIllegalUnannotatedObject_ThrowsInvalidOperationException()
{
// Arrange
var illegalObject = new IllegalUnannotatedObject
{
DynamicProperties = new Dictionary<string, object>
{
{ "Inv@l:d.", 1 }
}
};

ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<IllegalUnannotatedObject>("IllegalUnannotatedObjects");
IEdmModel model = builder.GetEdmModel();
IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("IllegalUnannotatedObjects");
EntitySetSegment entitySetSeg = new EntitySetSegment(entitySet);
HttpRequest request = RequestFactory.Create(opt => opt.AddRouteComponents("odata", model));
request.ODataFeature().RoutePrefix = "odata";
request.ODataFeature().Model = model;
request.ODataFeature().Path = new ODataPath(entitySetSeg);

OutputFormatterWriteContext context = new OutputFormatterWriteContext(
request.HttpContext,
(s, e) => null,
objectType: typeof(IllegalUnannotatedObject),
@object: illegalObject);

ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { ODataPayloadKind.Resource });
formatter.SupportedMediaTypes.Add("application/json");

// Act & Assert
Assert.Throws<ODataException>(() => formatter.WriteResponseBodyAsync(context, Encoding.UTF8).GetAwaiter().GetResult());
}

// positive test as above
[Fact]
public void SerializeIllegalAnnotatedObject_ReturnsFixedValidObject()
{
// Arrange
var illegalObject = new IllegalAnnotatedObject
{
DynamicProperties = new Dictionary<string, object>
{
{ "Inv@l:d.", 1 }
}
};

ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<IllegalAnnotatedObject>("IllegalAnnotatedObject");
IEdmModel model = builder.GetEdmModel();
IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("IllegalAnnotatedObject");
EntitySetSegment entitySetSeg = new EntitySetSegment(entitySet);
HttpRequest request = RequestFactory.Create(opt => opt.AddRouteComponents("odata", model));
request.ODataFeature().RoutePrefix = "odata";
request.ODataFeature().Model = model;
request.ODataFeature().Path = new ODataPath(entitySetSeg);

OutputFormatterWriteContext context = new OutputFormatterWriteContext(
request.HttpContext,
(s, e) => null,
objectType: typeof(IllegalAnnotatedObject),
@object: illegalObject);

ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { ODataPayloadKind.Resource });
formatter.SupportedMediaTypes.Add("application/json");

// Set the Response.Body to a new MemoryStream to capture the response
var memoryStream = new MemoryStream();
context.HttpContext.Response.Body = memoryStream;

// Act
formatter.WriteResponseBodyAsync(context, Encoding.UTF8).GetAwaiter().GetResult();

memoryStream.Position = 0;
var content = new StreamReader(memoryStream).ReadToEnd();
var jd = System.Text.Json.JsonDocument.Parse(content);
var root = jd.RootElement;

// Assert
// check that the JSON response contains the fixed property name and its value is 1
Assert.Equal(1, root.GetProperty("Inv_l_d_").GetInt32());
}

private class Customer
{
public int Id { get; set; }
}

private class IllegalUnannotatedObject
{
[Key]
public int Id { get; set; }
public IDictionary<string, object> DynamicProperties { get; set; }
}

[ReplaceIllegalFieldNameCharacters]
private class IllegalAnnotatedObject : IllegalUnannotatedObject
{

}

}
}

0 comments on commit a2d55f4

Please sign in to comment.