skip to Main Content

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


  1. 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.

    [HttpPost]
    public IActionResult InsertEntity([FromBody] EntityInsertRequest request)
    {
        // Remove default/empty objects from the Fields list
        request.Fields = request.Fields?.Where(field => 
            !(string.IsNullOrWhiteSpace(field.DatabaseField) && string.IsNullOrWhiteSpace(field.Description))
        ).ToList() ?? new List<EntityFieldInsertRequest>();
    
        // Continue processing the cleaned request
        // Your save or business logic here
    
        return Ok();
    }
    

    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.

    Login or Signup to reply.
  2. 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.

    public class EntityInsertRequest
    {
        private IEnumerable<EntityFieldInsertRequest> _fields = [];
    
        public string Description { get; set; } = string.Empty;
        public IEnumerable<EntityFieldInsertRequest> Fields
        {
            get => _fields;
            set { 
                _fields = value
                    .Where(field => 
                        !string.IsNullOrWhiteSpace(field.DatabaseField) || 
                        !string.IsNullOrWhiteSpace(field.Description))
                    .ToList();
            }
        }
    }
    

    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

    public class NullableIEnumerableFilter : IActionFilter
    {
        private readonly IServiceProvider _serviceProvider;
    
        public NullableIEnumerableFilter(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }
    
        public void OnActionExecuting(ActionExecutingContext context)
        {
            foreach (var argument in context.ActionArguments.Values)
            {
                switch (argument)
                {
                    case EntityInsertRequest insertRequest:
                        var remover = _serviceProvider.GetService(typeof(INullableIEnumerableRemover<EntityInsertRequest>)) as INullableIEnumerableRemover<EntityInsertRequest>;
                        remover?.Run(insertRequest);
                        break;
                }
            }
        }
    
        public void OnActionExecuted(ActionExecutedContext context) { }
    }
    

    Remover Interface

    public interface INullableIEnumerableRemover<T>
    {
        void Run(T request);
    }
    

    The EntityInsertRequest Remover

    public class EntityInsertRequestNullableIEnumerableRemover : INullableIEnumerableRemover<EntityInsertRequest>
    {
        public void Run(EntityInsertRequest request)
        {
            request.Fields = request.Fields
                .Where(f => 
                    !string.IsNullOrWhiteSpace(f.DatabaseField) ||
                    !string.IsNullOrWhiteSpace(f.Description))
                .ToList();
        }
    }
    

    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:

    builder.Services.AddScoped<NullableIEnumerableFilter>();
    builder.Services.AddScoped<INullableIEnumerableRemover<EntityInsertRequest>, EntityInsertRequestNullableIEnumerableRemover>();
    

    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.

    [HttpPost(Name = "AddEntity")]
    [ServiceFilter(typeof(NullableIEnumerableFilter))]
    public async Task<IActionResult> Post(EntityInsertRequest request)
    {
        return Ok(request.Fields);
    }
    

    My .http file passed the data like this:

    POST {{CollectionInit_HostAddress}}/entities/
    Content-Type: application/json
    
    {
        "description": "my description",
        "fields": [
            {
                "databaseField": "",
                "description": ""
            },
            {
                "databaseField": "db field 1",
                "description": "description 1"
            },
            {
                "databaseField": "",
                "description": ""
            },
            {
                "databaseField": "db field 2",
                "description": "description 2"
            }
        ]
    }
    

    Response received was this:

    [
      {
        "databaseField": "db field 1",
        "description": "description 1"
      },
      {
        "databaseField": "db field 2",
        "description": "description 2"
      }
    ]
    

    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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search