Späte Bindung Dynamische Auflösung von Modellen nach Eingabe des Controllers

9

Ich suche nach einer Möglichkeit, ein Modell nach Eingabe einer Aktion in einem Controller aufzulösen. Die einfachste Möglichkeit, das Problem zu beschreiben, ist:

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

Wenn Sie weitere Informationen darüber suchen, warum ich das versuche, können Sie weiterlesen, um das vollständige Bild zu erhalten

TL; DR

Ich suche nach einer Möglichkeit, eine Anforderung eines Modells mit einem Parameternamen aufzulösen, der immer aus der Abfragezeichenfolge aufgelöst wird. Wie kann ich Filter vom Start dynamisch registrieren? Ich habe eine Klasse, die sich um die Registrierung meiner Filter kümmert.

In meiner Startklasse möchte ich Filter dynamisch bei meinen restServices registrieren können. Ich habe eine Option, die ich verwende, um sie an meinen benutzerdefinierten ControllerFeatureProvider zu übergeben, die ungefähr so ​​aussieht:

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

Mein Controller verfolgt die Optionen und verwendet sie, um Filter für Paging-Endpunkte und OData bereitzustellen.

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

Ich habe Probleme herauszufinden, wie ein Modell angesichts des HttpContext dynamisch aufgelöst werden kann. Ich würde denken, etwas Ähnliches zu tun, um das Modell zu erhalten, aber dies ist Pseudocode, der nicht funktioniert

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

Nachdem ich mich in die Quelle vertieft hatte , sah ich einige vielversprechende Dinge, ModelBinderFactory und den ControllerActionInvoker Diese Klassen werden in der Pipeline für die Modellbindung verwendet.

Ich würde erwarten, dass das eine einfache Schnittstelle verfügbar macht, um einen Parameternamen aus dem QueryString aufzulösen, ungefähr so:

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

Die einzige Möglichkeit, ein Modell aus dem Modellordner aufzulösen, besteht darin, gefälschte Controller-Deskriptoren zu erstellen und eine Menge Dinge zu verspotten.

Wie kann ich spät gebundene Parameter in meinen Controller übernehmen?

Johnny 5
quelle
2
Ich sehe keine Verwendung dafür. Selbst wenn Sie das Modell basierend auf einem Zeichenfolgenparameter binden können, können Sie keine generische Methode wie GetValueFor <T> verwenden, da T während einer Kompilierungszeit aufgelöst werden muss. Dies bedeutet, dass der Aufrufer dies wissen muss der Typ von T zur Kompilierungszeit, der den Zweck der dynamischen Bindung des Typs zunichte machen würde. Dies bedeutet, dass der Erbe von DynamicControllerBase den Typ von TDTO kennen muss. Sie können versuchen, JSON im Parameter zu empfangen und jede Implementierung von DynamicControllerBase die Zeichenfolge in ein Modell deserialisieren und umgekehrt.
Jonathan Alfaro
@Darkonekt Wenn Sie sich die 'AddFilter'-Methode ansehen, haben Sie die eingegebenen generischen Parameter, die bei der Registrierung der Funktionen in einem Abschluss gespeichert werden. Es ist ein bisschen verwirrend, aber ich versichere Ihnen, dass es lebensfähig ist und funktionieren kann
Johnny 5
Ich möchte mich nicht an json anschließen, weil ich nicht die Art und Weise ändern muss, wie Webapi Parameter auf natürliche Weise auflöst
Johnny 5,
Wenn Sie etwas mehr über den Anwendungsfall und das reale Szenario erklären würden, in dem diese Art von Funktionalität erforderlich ist, würde dies viel helfen. Wahrscheinlich gibt es sogar eine einfachere Lösung. Wer weiß. Ich selbst mag es manchmal, Dinge zu komplizieren. Ich sage nur ...
Mr. Blond
@ Mr.Blond Ich habe einen generischen Rest-Service, der Rohöl- und Listenfunktionen bietet. Manchmal müssen meine Dienste Daten aus der Get-Liste filtern, aber ich möchte nicht einen vollständigen Dienst von allem schreiben müssen, was ich brauche, um einen Filter bereitzustellen
Johnny 5,

Antworten:

2

Ich stimme Ihrem Gedanken zu

Dienste müssen Daten aus der Get-Liste filtern, aber ich möchte nicht einen vollständigen Dienst von allem schreiben müssen, was ich brauche, um einen Filter bereitzustellen

Warum für jede mögliche Kombination ein Widget / Filter / Endpunkt schreiben?

Stellen Sie einfach grundlegende Operationen bereit, um alle Daten / Eigenschaften abzurufen. Verwenden Sie dann GraphQL, damit der Endbenutzer es nach seinen Anforderungen filtern ( modellieren ) kann .

Von 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
quelle
Ich habe überlegt, GraphQL zu verwenden, bin aber zu sehr an meine aktuelle Implementierung gebunden
Johnny 5,
2

Wir haben dies getan, unser Code verweist auf diese Site: https://prideparrot.com/blog/archive/2012/6/gotchas_in_explicit_model_binding

Wenn Sie sich unseren Code ansehen, besteht der Trick darin, eine FormCollection in Ihrer Controller-Methode zu akzeptieren und dann die Modellordner-, Modell- und Formulardaten zu verwenden:

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

(Hinweis: Die Website scheint nicht erreichbar zu sein. Der Link führt zu archive.org.)

Brian sagt Reinstate Monica
quelle
Vielen Dank für Ihre Hilfe. Derzeit verwende ich MVC nicht. ZB (Es kann mehr als einen Eingabeparameter geben) Ich muss die Parameter anhand des Namens auflösen. Zusätzlich verwende ich .Net-Core. Ich denke, dies ist für die ältere Version .net geschrieben. Bitte vervollständigen Sie den Methodenstub: damit die Antwort akzeptiert wird:this.Resolve<MyCustomType>("MyParamName");
Johnny 5
Da ich keine Umgebung habe, um dies minimal zu reproduzieren, kann ich das nicht - ich entschuldige mich dafür, dass ich die Dotnetcore-Anforderung nicht erfüllt habe.
Brian sagt Reinstate Monica
Ich kann es in .Net-Core übersetzen, das ist in Ordnung. Wenn Sie mir zeigen, wie man nach Parameternamen auflöst, akzeptiere ich
Johnny 5.
Dies ist die naheliegendste Antwort, die ich wollte, also gebe ich dir das Kopfgeld
Johnny 5
0

Am Ende habe ich dynamische Controller geschrieben. Um das Problem als Workaround zu lösen.

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

Ich bin im Moment schwer darin, die Funktion in der Methode zu codieren, aber ich bin sicher, dass Sie herausfinden können, wie Sie sie bei Bedarf weitergeben können.

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
quelle