I’ve got a Rest endpoint that receives the class EntityInsertRequest
public class EntityInsertRequest
{
public string Description { get; set; }
public IEnumerable<EntityFieldInsertRequest> Fields { get; set; }
}
public class EntityFieldInsertRequest
{
public string DatabaseField { get; set; }
public string Description { get; set; }
}
How can i make a filter or override in the initialization of the object to remove a default object of the list, in this case a default object of the list would be:
{
"databaseField": "",
"description": ""
}
and i wold be sending
{
"description": "FooBar",
"fields": [
{
"databaseField": "",
"description": ""
},
{
"databaseField": "Foo",
"description": "Bar"
}
]
}
I wold like it to be instantiated with an empty list and be dynamicaly replicated for other endpoints.
I can’t alter how it’s send and remove the object on the sender’s side, it needs to be on the back-end.
I’ve tried to create a JsonConverter<IEnumerable>
public class NullableIEnumerableOverride<T> : JsonConverter<IEnumerable<T>> where T : class
{
public override bool CanConvert(Type typeToConvert)
{
return typeof(System.Collections.IEnumerable).IsAssignableFrom(typeToConvert) && typeToConvert != typeof(string) && typeToConvert.GenericTypeArguments[0].IsClass;
}
public override IEnumerable<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var internalOptions = new JsonSerializerOptions(options);
var list = JsonSerializer.Deserialize<IEnumerable<T>>(ref reader);
if (list == null || !list.Any())
{
return (IEnumerable<T>)CreateEmptyInstance(typeToConvert);
}
//cant get properties of generic object with typeof(T).GetProperties()
//bool allFieldsAreDefault = list.All(item =>
//{
// var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
// return properties.All(prop =>
// {
// var value = prop.GetValue(item);
// var defaultValue = GetDefault(prop.PropertyType);
// return Equals(value, defaultValue);
// });
//});
//return allFieldsAreDefault ? (IEnumerable<T>)CreateEmptyInstance(typeToConvert) : list;
return list;
}
private static object CreateEmptyInstance(Type typeToConvert)
{
if (typeToConvert.IsGenericType && typeof(IEnumerable<>).IsAssignableFrom(typeToConvert.GetGenericTypeDefinition()))
{
Type elementType = typeToConvert.GetGenericArguments()[0];
Type listType = typeof(List<>).MakeGenericType(elementType);
return Activator.CreateInstance(listType) ?? throw new InvalidOperationException($"Unable to create instance of type {listType}.");
}
throw new InvalidOperationException($"Type {typeToConvert} is not supported.");
}
public override void Write(Utf8JsonWriter writer, IEnumerable<T>? value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
return;
}
JsonSerializerOptions jsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
JsonSerializer.Serialize(writer, value, jsonOptions);
}
private object? GetDefault(Type type)
{
return type.IsValueType ? Activator.CreateInstance(type) : null;
}
}
and in the program.cs
builder.Services.AddControllers().AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new NullableIEnumerableOverride<object>());
});
and because i want to use it on other endpoints and set it as a generic object
on program.cs
, i get this cast error:
System.InvalidCastException: Unable to cast object of type 'System.Collections.Generic.List`1[System.Object]' to type 'System.Collections.Generic.IEnumerable`1[Logic.Request.Entity.EntityFieldInsertRequest]'.
at System.Text.Json.ThrowHelper.ThrowInvalidCastException_DeserializeUnableToAssignValue(Type typeOfValue, Type declaredType)
at System.Text.Json.JsonSerializer.<UnboxOnRead>g__ThrowUnableToCastValue|50_0[T](Object value)
at System.Text.Json.JsonSerializer.UnboxOnRead[T](Object value)
at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value, Boolean& isPopulatedValue)
at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.ReadJsonAndSetMember(Object obj, ReadStack& state, Utf8JsonReader& reader)
at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value, Boolean& isPopulatedValue)
at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.ContinueDeserialize(ReadBufferState& bufferState, JsonReaderState& jsonReaderState, ReadStack& readStack)
at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.DeserializeAsync(Stream utf8Json, CancellationToken cancellationToken)
at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.DeserializeAsObjectAsync(Stream utf8Json, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonInputFormatter.ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
at Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonInputFormatter.ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
at Microsoft.AspNetCore.Mvc.ModelBinding.Binders.BodyModelBinder.BindModelAsync(ModelBindingContext bindingContext)
at Microsoft.AspNetCore.Mvc.ModelBinding.ParameterBinder.BindModelAsync(ActionContext actionContext, IModelBinder modelBinder, IValueProvider valueProvider, ParameterDescriptor parameter, ModelMetadata metadata, Object value, Object container)
at Microsoft.AspNetCore.Mvc.Controllers.ControllerBinderDelegateProvider.<>c__DisplayClass0_0.<<CreateBinderDelegate>g__Bind|0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
2
Answers
To dynamically remove the default object from the IEnumerable list in your .NET controller, you can modify the EntityInsertRequest before further processing. This can be done in a model binder or directly within the controller action.
Within the controller using LINQ to filter out default objects:
Filter the Fields: Before saving or further processing the request, filter the Fields to remove any default or empty entries.
This approach ensures that any EntityFieldInsertRequest objects where both DatabaseField and Description are empty or null will be removed from the Fields collection.
If the Fields collection is null, it will initialize an empty list to avoid null reference exceptions.
After playing around with the given code, I could not figure the generic converter out.
However, there are other options.
Request Level
You could add the logic directly to the Request object.
That would remove the "default" fields every time you set the
fields
value.This will work and a test can also be easily written to verify it.
You will have to update each Request object off course and there is no flexibility if you ever want an instance not with "default" fields removed.
If you want more flexibility as to when to remove the "default" fields, you can implement an Action Filter.
The Action Filter will be able to process the request object and depending on the concrete type call the matching "Remover" (or what ever you may want to call it) to remove the "default" items as needed.
Action Filter
Remover Interface
The EntityInsertRequest Remover
Usage/Test
The Filter and removers have to be registered off course. How might be different, depending on project type etc… I used a Web API for testing so it looked like this:
I tested this locally by creating a simple endpoint that responds with the fields to verify the "default" was removed. The
ServiceFilter
attribute can also be applied at the controller level.My
.http
file passed the data like this:Response received was this:
I also verified it works when passing only "default" items or no items at all.
Summary
While this requires an implementation of a remover per request object and will be more work than removing "default" fields in each Request object directly, it also gives you the flexibility to only apply the logic when needed.
Each implementation is still clear, easily maintained and testable.
Having a single generic converter might be less work, it will also be less flexible if requirements arise where a specific request might now need a more specific condition, which can lead to a converter that will grow quickly and become hard to maintain and harder to test for each type and scenario.