How to automatically set default value of API model’s property with value from appSettings.json?

I’m creating a web API in Core 3.1 and I have PagingParams model that I will use on many controller actions. The model has PageNumber and PageSize parameters, both of which have a default value:

public class PagingParams
{
    [Range(1, Int32.MaxValue, ErrorMessage = "The field {0} must be greater than 0.")]
    public int? PageNumber { get; set; } = 1;

    [Range(1, 10000)] //TODO: Get 10000 from appsettings
    public int? PageSize { get; set; } = 10000; //TODO: Get 10000 from appsettings
}

The PageSize parameter value needs to be set to 10000 and same with the max value in its Range attribute. This prevents the user from getting more than 10k items per page.

I want to be able to retrieve that value from appSettings.json so I can modify it without republishing the the application each time. Throughout the code I’ve been using IOptionsSnapshot to read values from appSettings.json, but it appears that can’t be done here because the model is required to have a parameterless constructor, resulting in the following error:

System.InvalidOperationException: Could not create an instance of type ‘WebAPI.Models.PagingParams’. Model bound complex types must not be abstract or value types and must have a parameterless constructor. Alternatively, give the ‘pagingParams’ parameter a non-null default value.

How could I set up the model so that by default the int is taken from appSettings and set in the two places?

This is how the class is used:

[Route("api/[controller]")]
    [ApiController]
    public class UsersController : ControllerBase
    {
        private readonly IMapper _mapper;
        private readonly IUserRepo _userRepo;

        public MissingNamesController(IMapper mapper, IUserRepo userRepo)
        {
            _mapper = mapper;
            _userRepo = userRepo;
        }

        [HttpGet]
        public IEnumerable<UserDto> Get([FromQuery] UsersQueryParams queryParams, [FromQuery] PagingParams pagingParams)
        {
            IEnumerable<User> models = _userRepo.Get(queryParams, pagingParams);
            IEnumerable<UserDto> dtoModels = _mapper.Map<IEnumerable<UserDto>>(models);
            return dtoModels;
        }
    }
}

NOTE: My question is similar to this one, except that I want to be able to set this up so it’s automated. I want to avoid calling a method each time I use the PagingParams class on an action and then having to manually call ModelState.IsValid and return the model error. With my current set up, the Range attribute automatically handles returning of a model error and I don’t have to do anything further.

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

What you want is to conveniently have some properties initialized with some default values. The core mechanism is just simple like you instantiate an object and set its properties before it’s being used in the next pipelines. There are some ways to achieve it:

  • Set the properties right in the class declaration: This is not what you want because you want to populate the default values from some settings (at runtime).
  • Set the properties everywhere you have the object: This means in the current context, you need the default values loaded from the settings/configuration so that you can initialize the properties.

Usually you do it the second way. However to avoid repetitive code as much as possible, we need to find a point in the pipeline (code execution – request processing flow) where you can obtain the object and initialize its properties (loaded from configuration) in one place.

It’s fairly easy that in your case the PagingParams is not instantiated everywhere in your code, it just comes from controller actions (instantiated by model binding). So in that case we can use action filter (either IActionFilter or IAsyncActionFilter works) as the point we can obtain the argument (of type PagingParams) and modify it in whatever way we want (initialize the properties with loaded default values from the configuration).

It’s pretty simple like this:

public class InitPagingParamsAttribute : IActionFilter
{
    public void OnActionExecuted(ActionExecutedContext context)
    {
        
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        var pp = context.ActionArguments.Values.OfType<PagingParams>().FirstOrDefault();
        //this means there is at least 1 PagingParams in the controller action
        //NOTE that you can get all args of PagingParams, but looks like usually 
        //there should be just 1 arg of such type (in one same controller action)
        if (pp != null)
        {
            //modify the pp here
            //...
        }
    }
}

In the Startup.cs file, you can configure the MVC middleware to register the global action filter. Without doing that, you need to decorate the attribute InitPagingParamsAttribute on the controller or action method you want.
public void ConfigureServices(IServiceCollection services){
    //...
    services.AddMvc(o => {
        o.Filters.Add<InitPagingParamsAttribute>();
    });
    //...
}

NOTE: you can inject your IOptionsSnapshot in the InitPagingParamsAttribute constructor. So you can load the default values from the configuration there.


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x