Unión tardía Resolver modelos dinámicamente después de ingresar al controlador

9

Estoy buscando una manera de resolver un modelo después de entrar en una acción en un controlador, la forma más sencilla de describir el problema sería:

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

Si está buscando más información sobre por qué estoy tratando de hacerlo, puede continuar leyendo para obtener una imagen completa

TL; DR

Estoy buscando una manera de resolver una solicitud de un modelo, dado un nombre de parámetro que siempre se resolverá a partir de la cadena de consulta ¿Cómo puedo registrar dinámicamente los filtros desde el inicio? Tengo una clase que se encargará de registrar mis filtros.

En mi clase de inicio quiero poder registrar dinámicamente filtros con mis restServices. Tengo una opción que estoy usando para pasar a mi ControllerFeatureProvider personalizado que se ve más o menos así:

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;
        });
    }
}

Mi controlador hará un seguimiento de las opciones y las usará para proporcionar filtros para puntos finales de paginación y 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>();
    }
}

Tengo problemas para descubrir cómo resolver dinámicamente un modelo dado el HttpContext, creo que haría algo como esto para obtener el modelo, pero este es un pseudocódigo que no funciona

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);
}

Después de profundizar en la fuente, vi algunas cosas prometedoras ModelBinderFactory y ControllerActionInvoker Estas clases se usan en la tubería para el enlace de modelos,

Esperaría que exponga una interfaz simple para resolver un nombre de parámetro de QueryString, algo como esto:

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

Sin embargo, la única forma en que veo resolver un modelo desde la carpeta del modelo es crear descriptores de controladores falsos y burlarse de un montón de cosas.

¿Cómo puedo aceptar parámetros enlazados tarde en mi controlador?

johnny 5
fuente
2
No veo un uso para esto. Además, incluso si puede vincular el modelo en función de un parámetro de cadena ... no podrá utilizar un método genérico como GetValueFor <T> porque T debe resolverse en un momento de compilación ... esto significa que la persona que llama debe saber El tipo de T en tiempo de compilación que anularía el propósito de vincular dinámicamente el tipo. Esto significa que el heredero de DynamicControllerBase debe conocer el tipo de TDTO ... Una cosa que puede intentar es recibir JSON en el parámetro y hacer que cada implementación de DynamicControllerBase deserialice la cadena a un modelo y viceversa.
Jonathan Alfaro el
@Darkonekt si observa el método 'AddFilter', tiene los parámetros genéricos escritos que se almacenan en un cierre cuando registra los funcs. Es un poco confuso, pero le aseguro que es viable y puede funcionar
Johnny 5
No quiero conectarme a JSON, porque no quiero tener que cambiar la forma en que Webapi resuelve los parámetros de forma natural
Johnny 5
Si explica un poco más sobre el caso de uso y el escenario de la vida real donde este tipo de funcionalidad es necesaria, eso ayudaría mucho. Probablemente hay incluso una solución más simple disponible ... quién sabe. En cuanto a mí mismo, a veces me gusta complicar las cosas ... Solo digo ...
Sr. Blond
@ Mr.Blond Tengo un servicio de descanso genérico que proporciona la funcionalidad Crud y Get List. A veces, mis servicios necesitan filtrar datos de la lista de obtención, pero no quiero tener que escribir un servicio completo de todo lo que necesito para proporcionar un filtro
johnny 5

Respuestas:

2

Estoy de acuerdo con tu pensamiento

los servicios necesitan filtrar datos de la lista de obtención, pero no quiero tener que escribir un servicio completo de todo lo que necesito para proporcionar un filtro

¿Por qué escribir un widget / filtro / punto final para cada combinación posible?

Simplemente proporcione operaciones básicas para obtener todos los datos / propiedades. A continuación, utilice GraphQL para permitir que el usuario final para filtro ( modelo ) a sus necesidades.

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

ΩmegaMan
fuente
He estado considerando usar GraphQL pero estoy demasiado vinculado a mi implementación actual
johnny 5
2

Hemos hecho esto, nuestro código hace referencia a este sitio: https://prideparrot.com/blog/archive/2012/6/gotchas_in_explicit_model_binding

Específicamente, observando nuestro código, ¿de qué sirve aceptar una FormCollection en su método de controlador y luego usar la carpeta de modelo, el modelo y los datos del formulario:

Ejemplo tomado del enlace:

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();
}

(Nota: el sitio parece estar inactivo, el enlace es a archive.org)

Brian dice reinstalar a Mónica
fuente
Gracias por su ayuda. Actualmente, no estoy usando MVC, por ejemplo (puede haber más de 1 parámetro de entrada). Necesito resolver los parámetros por el nombre. Además, estoy usando .Net-Core. Creo que esto está escrito para la versión anterior .net. Complete el código auxiliar del método: para que se acepte la respuesta:this.Resolve<MyCustomType>("MyParamName");
johnny 5
Como no tengo un entorno para reproducir esto de manera mínima, no podré hacerlo; pido disculpas por no cumplir el requisito de dotnetcore.
Brian dice que reinstala a Mónica el
Puedo traducirlo a Net-Core que está bien, Si me muestras cómo resolver el nombre de parámetro Voy a aceptar
johnny 5
Esto es lo más parecido a la respuesta que quería, así que te daré la recompensa
johnny 5
0

Terminé escribiendo controladores dinámicos. Para resolver el problema como una solución alternativa.

private static TypeBuilder GetTypeBuilder(string assemblyName)
{
    var assemName = new AssemblyName(assemblyName);
    var assemBuilder = AssemblyBuilder.DefineDynamicAssembly(assemName, AssemblyBuilderAccess.Run);
    // Create a dynamic module in Dynamic Assembly.
    var moduleBuilder = assemBuilder.DefineDynamicModule("DynamicModule");
    var tb = moduleBuilder.DefineType(assemblyName,
            TypeAttributes.Public |
            TypeAttributes.Class |
            TypeAttributes.AutoClass |
            TypeAttributes.AnsiClass |
            TypeAttributes.BeforeFieldInit |
            TypeAttributes.AutoLayout,
            null);

    return tb;
}

Estoy codificando el func en el método por el momento, pero estoy seguro de que puedes encontrar la manera de transmitirlo si lo necesitas.

public static Type CompileResultType(string typeSignature)
{
    TypeBuilder tb = GetTypeBuilder(typeSignature);

    tb.SetParent(typeof(DynamicControllerBase));

    ConstructorBuilder ctor = tb.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);

    // For this controller, I only want a Get method to server Get request
    MethodBuilder myGetMethod =
        tb.DefineMethod("Get",
            MethodAttributes.Public,
            typeof(String), new Type[] { typeof(Test), typeof(String) });

    // Define parameters
    var parameterBuilder = myGetMethod.DefineParameter(
        position: 1, // 0 is the return value, 1 is the 1st param, 2 is 2nd, etc.
        attributes: ParameterAttributes.None,
        strParamName: "test"
    );
    var attributeBuilder
        = new CustomAttributeBuilder(typeof(FromServicesAttribute).GetConstructor(Type.EmptyTypes), Type.EmptyTypes);
    parameterBuilder.SetCustomAttribute(attributeBuilder);

    // Define parameters
    myGetMethod.DefineParameter(
        position: 2, // 0 is the return value, 1 is the 1st param, 2 is 2nd, etc.
        attributes: ParameterAttributes.None,
        strParamName: "stringParam"
    );

    // Generate IL for method.
    ILGenerator myMethodIL = myGetMethod.GetILGenerator();
    Func<string, string> method = (v) => "Poop";

    Func<Test, string, string> method1 = (v, s) => v.Name + s;

    myMethodIL.Emit(OpCodes.Jmp, method1.Method);
    myMethodIL.Emit(OpCodes.Ret);

    return tb.CreateType();
}
johnny 5
fuente