Search code examples
c#asp.net-coremodel-bindingsystem.text.jsonminimal-apis

Is there a way to make the ASP.NET Core model binder set properties with private setters?


I use ASP.NET Core minimal APIs and I try to have my view-models (where incoming client data gets mapped to) use private setters for their properties.

But I see that the binding stops working once I make a property have a private setter. Is there any quick fix for this? I would expect this is to be a common requirement and have an easy fix, but I can't find anything online.

Plus, I don't think I am doing anything special :

        public void AddRoutes(IEndpointRouteBuilder app)
        {
            app
                .MapPost("api/document/getById", async ([FromBody] GetDocumentRequest request, [FromServices] ISender sender) =>
                {
                    return await sender.Send(request.Query);
                })
                .WithName("GetDocumentById")
                .Produces<DocumentViewModel>(StatusCodes.Status200OK);
        }

Here, GetDocumentRequest is my view-model.


Solution

  • Since binding here would just deserialize JSON you can mark corresponding properties with JsonIncludeAttribute:

    public class GetDocumentRequest
    {
        [JsonInclude]
        public int Type { get; private set; }
    }
    

    See Non-public members and property accessors section of the docs.

    I was hoping for a more global solution, since I plan on having all my properties, for all endpoint view-models, have private setters. And they are too many of them...

    Alternatively you can look into customizing JSON contract. Something to get you started:

    builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolver =
        new DefaultJsonTypeInfoResolver
        {
            Modifiers = { SetAllSetters }
        });
    
    void SetAllSetters(JsonTypeInfo typeInfo)
    {
        // find types which need special management
        if (typeInfo.Type.Assembly == typeof(GetDocumentRequest).Assembly)
        {
            var propertyInfos = typeInfo.Type.GetProperties()
                .ToDictionary(pi => pi.Name, StringComparer.InvariantCultureIgnoreCase);
            // todo: check if overriding set method is needed
            foreach (var info in typeInfo.Properties)
            {
                var methodInfo = propertyInfos[info.Name].GetSetMethod(true);
                info.Set = (s, v) => methodInfo.Invoke(s, [v]);
            }
        }
    }
    

    Note that this implementation highly likely is less effective compared to the default one. Also I recommend to go through the Use immutable types and properties and consider options provided there (like using constructors or init-only members or records)