Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Late binding Dynamically Resolve Models after entering controller

I'm looking for a way to resolve a model after entering into an action in a controller , the simplest way to describe the problem would be:

public DTO[] Get(string filterName)
{
    //How can I do this
    this.Resolve<MyCustomType>("MyParamName");
}

If you're looking for more information on why I'm trying to do that you can continue reading to get the full picture

TL;DR

I'm looking for a way to resolve a model a request, given a parameter name that will always be resolved from query string How can I dynamically register filters from the startup. I have a class which is going to handle registering my filters.

In my startup class I want to be able to dynamically register filters with my restServices. I have an options that I'm using to pass to my custom ControllerFeatureProvider which roughly looks like so:

public class DynamicControllerOptions<TEntity, TDTO>
{
    Dictionary<string, Func<HttpContext, Expression<Func<TEntity, bool>>>> _funcNameToEndpointResolverMap
        = new Dictionary<string, Func<HttpContext, Expression<Func<TEntity, bool>>>>();
    Dictionary<string, List<ParameterOptions>> _filterParamsMap = new Dictionary<string, List<ParameterOptions>>();

    public void AddFilter(string filterName, Expression<Func<TEntity, bool>> filter)
    {
        this._funcNameToEndpointResolverMap.Add(filterName, (httpContext) =>  filter);
    }
    public void AddFilter<T1>(string filterName, Func<T1, Expression<Func<TEntity, bool>>> filterResolver,
        string param1Name = "param1")
    {
        var parameters = new List<ParameterOptions> { new ParameterOptions { Name = param1Name, Type = typeof(T1) } };
        this._filterParamsMap.Add(filterName, parameters);
        this._funcNameToEndpointResolverMap.Add(filterName, (httpContext) => {
            T1 parameter = this.ResolveParameterFromContext<T1>(httpContext, param1Name);
            var filter = filterResolver(parameter);
            return filter;
        });
    }
}

My Controller will keep track of the options and use them to provide filters for paging endpoints and OData.

public class DynamicControllerBase<TEntity, TDTO> : ControllerBase
{
    protected DynamicControllerOptions<TEntity, TDTO> _options;
    //...

    public TDTO[] GetList(string filterName = "")
    {
        Expression<Func<TEntity, bool>> filter = 
            this.Options.ResolveFilter(filterName, this.HttpContext);
        var entities = this._context.DbSet<TEntity>().Where(filter).ToList();
        return entities.ToDTO<TDTO>();
    }
}

I'm having trouble figuring out how to dynamically resolve a model given the HttpContext, I would think to do something like this to obtain the model but this is pseudo-code that doesn't work

private Task<T> ResolveParameterFromContext<T>(HttpContext httpContext, string parameterName)
{
    //var modelBindingContext = httpContext.ToModelBindingContext();
    //var modelBinder = httpContext.Features.OfType<IModelBinder>().Single();
    //return modelBinder.BindModelAsync<T>(parameterName);
}

After Digging into the Source, I saw some promising things ModelBinderFactory and the ControllerActionInvoker These classes are used in the pipeline for model binding,

I would expect the to expose a simple interface to resolve a parameter name from the QueryString, something like this:

ModelBindingContext context = new ModelBindingContext();
return context.GetValueFor<T>("MyParamName");

However, the only way I see to resolve a model from the model binder is to create fake controller descriptors and mock a ton of things.

How can I accept late bound parameters into my contoller?

like image 297
johnny 5 Avatar asked Nov 12 '19 23:11

johnny 5


2 Answers

I agree with your thought

services need to filter data from get list, but I dont want to have to write an entire service of all I need to to provide a filter

Why write a widget/filter/endpoint for every possible combination?

Simply provide basic operations to get all data/properties. Then use GraphQL to allow the end user to filter (model) it to their needs.

From GraphQL

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.

like image 147
ΩmegaMan Avatar answered Oct 23 '22 05:10

ΩmegaMan


We've done this, our code references this site: https://prideparrot.com/blog/archive/2012/6/gotchas_in_explicit_model_binding

Specifically, looking at our code, what does the trick is accepting a FormCollection in your controller method and then using the model binder, model, and form data:

Example taken from link:

public ActionResult Save(FormCollection form)
{
var empType = Type.GetType("Example.Models.Employee");
var emp = Activator.CreateInstance(empType);

var binder = Binders.GetBinder(empType);

  var bindingContext = new ModelBindingContext()
  {
    ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => emp, empType),
    ModelState = ModelState,
    ValueProvider = form
  };      

  binder.BindModel(ControllerContext, bindingContext);

  if (ModelState.IsValid)
  {
   _empRepo.Save(emp);

    return RedirectToAction("Index");
  }

return View();
}

(Note: the site appears to be down, link is to to archive.org)

like image 2
brnlmrry Avatar answered Oct 23 '22 05:10

brnlmrry