Liaison tardive des modèles à résolution dynamique après entrée dans le contrôleur


9

Je cherche un moyen de résoudre un modèle après avoir entré une action dans un contrôleur, la manière la plus simple de décrire le problème serait:

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

Si vous cherchez plus d'informations sur les raisons pour lesquelles j'essaie de le faire, vous pouvez continuer à lire pour obtenir l'image complète

TL; DR

Je cherche un moyen de résoudre un modèle d'une demande, étant donné un nom de paramètre qui sera toujours résolu à partir de la chaîne de requête Comment puis-je enregistrer dynamiquement des filtres depuis le démarrage. J'ai une classe qui va gérer l'enregistrement de mes filtres.

Dans ma classe de démarrage, je veux pouvoir enregistrer dynamiquement des filtres avec mes restServices. J'ai une option que j'utilise pour passer à mon ControllerFeatureProvider personnalisé qui ressemble à peu près à ceci:

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

Mon contrôleur gardera une trace des options et les utilisera pour fournir des filtres pour les points de terminaison de pagination et 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>();
    }
}

J'ai du mal à trouver comment résoudre dynamiquement un modèle avec le HttpContext, je penserais à faire quelque chose comme ça pour obtenir le modèle mais c'est un pseudo-code qui ne fonctionne pas

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

Après avoir creusé dans la source, j'ai vu des choses prometteuses ModelBinderFactory et ControllerActionInvoker Ces classes sont utilisées dans le pipeline pour la liaison de modèle,

Je m'attendrais à ce que l'expose une interface simple pour résoudre un nom de paramètre à partir de QueryString, quelque chose comme ceci:

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

Cependant, la seule façon que je vois pour résoudre un modèle à partir du classeur de modèles est de créer de faux descripteurs de contrôleur et de se moquer d'une tonne de choses.

Comment puis-je accepter des paramètres liés tardivement dans mon contrôleur?


2
Je ne vois pas d'utilité pour cela. De plus, même si vous pouvez lier le modèle en fonction d'un paramètre de chaîne ... vous ne pourrez pas utiliser de méthode générique comme GetValueFor <T> car T doit être résolu au moment de la compilation .... cela signifie que l'appelant doit savoir le type de T au moment de la compilation, ce qui irait à l'encontre du but de lier dynamiquement le type. Cela signifie que l'héritier de DynamicControllerBase doit connaître le type de TDTO .... Une chose que vous pouvez essayer est de recevoir JSON dans le paramètre et que chaque implémentation de DynamicControllerBase désérialise la chaîne en un modèle et vice versa.
Jonathan Alfaro

@Darkonekt si vous regardez la méthode 'AddFilter', vous avez les paramètres génériques typés qui sont stockés dans une fermeture lorsque vous enregistrez les funcs. C'est un peu déroutant mais je vous assure que c'est viable et peut fonctionner
johnny 5

Je ne veux pas me connecter à json, car je ne veux pas avoir à changer la façon dont webapi résout naturellement les paramètres
johnny 5

Si vous expliquiez un peu plus le cas d'utilisation et le scénario réel où ce type de fonctionnalité est nécessaire, cela aiderait beaucoup. Il existe probablement une solution encore plus simple .. qui sait. Quant à moi, j'aime parfois compliquer les choses .. Je dis juste ..
M. Blond

@ Mr.Blond J'ai un service de repos générique qui fournit des fonctionnalités crud et get list. Parfois, mes services doivent filtrer les données de la liste d'obtention, mais je ne veux pas avoir à écrire un service complet de tout ce dont j'ai besoin pour fournir un filtre
johnny 5

Réponses:


2

Je suis d'accord avec votre idée

les services doivent filtrer les données de la liste get, mais je ne veux pas avoir à écrire un service complet de tout ce dont j'ai besoin pour fournir un filtre

Pourquoi écrire un widget / filtre / point de terminaison pour chaque combinaison possible?

Fournissez simplement des opérations de base pour obtenir toutes les données / propriétés. Utilisez ensuite GraphQL pour permettre à l'utilisateur final de le filtrer ( modéliser ) selon ses besoins.

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


J'ai envisagé d'utiliser GraphQL mais je suis trop lié à mon implémentation actuelle
johnny 5

2

Nous l'avons fait, notre code fait référence à ce site: https://prideparrot.com/blog/archive/2012/6/gotchas_in_explicit_model_binding

Plus précisément, en examinant notre code, qu'est-ce que l'astuce consiste à accepter une FormCollection dans votre méthode de contrôleur, puis à utiliser le classeur de modèle, le modèle et les données de formulaire:

Exemple tiré du lien:

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

(Remarque: le site semble être en panne, le lien est vers archive.org)


Merci pour votre aide. Actuellement, je n'utilise pas MVC, par exemple (il peut y avoir plus d'un paramètre d'entrée) J'ai besoin de résoudre les paramètres par le nom. De plus, j'utilise .Net-Core, je pense que cela est écrit pour l'ancienne version .net. Veuillez compléter le talon de la méthode: pour que la réponse soit acceptée:this.Resolve<MyCustomType>("MyParamName");
johnny 5

Comme je n'ai pas d'environnement pour reproduire cela de façon minimale, je ne pourrai pas le faire - je m'excuse d'avoir manqué l'exigence dotnetcore.
Brian dit de rétablir Monica

Je peux le traduire en .Net-Core, c'est très bien, si vous me montrez comment résoudre par nom de paramètre, j'accepterai
johnny 5

C'est la chose la plus proche de la réponse que je voulais, alors je vais vous donner la prime
johnny 5

0

J'ai fini par écrire des contrôleurs dynamiques. Pour résoudre le problème en guise de solution.

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

Je suis en train de coder durement la fonction dans la méthode mais je suis sûr que vous pouvez comprendre comment la transmettre si vous en avez besoin.

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();
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.