Późne wiązanie Dynamicznie rozwiązuj modele po wejściu do kontrolera

9

Szukam sposobu rozwiązania modelu po uruchomieniu akcji w kontrolerze, najprostszym sposobem opisania problemu byłoby:

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

Jeśli szukasz więcej informacji o tym, dlaczego próbuję to zrobić, możesz kontynuować czytanie, aby uzyskać pełny obraz

TL; DR

Szukam sposobu na rozwiązanie modelu żądania, biorąc pod uwagę nazwę parametru, która zawsze będzie rozstrzygana z ciągu zapytania. Jak mogę dynamicznie zarejestrować filtry od uruchomienia. Mam zajęcia, które zajmą się rejestracją moich filtrów.

W mojej klasie startowej chcę móc dynamicznie rejestrować filtry w moich usługach restServices. Mam opcje, których używam do przekazania do mojego niestandardowego sterownika ControllerFeatureProvider, który z grubsza wygląda tak:

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

Mój kontroler będzie śledził opcje i użyje ich do zapewnienia filtrów dla punktów końcowych stronicowania i 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>();
    }
}

Mam problem z wymyśleniem, jak dynamicznie rozwiązać model, biorąc pod uwagę HttpContext, pomyślałbym, aby zrobić coś takiego, aby uzyskać model, ale to pseudo-kod, który nie działa

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

Po wkopaniu do źródła zobaczyłem kilka obiecujących rzeczy ModelBinderFactory i ControllerActionInvoker Te klasy są używane w potoku do wiązania modelu,

Spodziewałbym się ujawnić prosty interfejs do rozwiązania nazwy parametru z QueryString, coś takiego:

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

Jednak jedynym sposobem, w jaki widzę rozwiązanie modelu z segregatora modeli, jest tworzenie fałszywych deskryptorów kontrolera i wyśmiewanie mnóstwa rzeczy.

Jak mogę zaakceptować parametry późno związane do mojego kontrolera?

Johnny 5
źródło
2
Nie widzę w tym zastosowania. Co więcej, nawet jeśli możesz powiązać model na podstawie parametru ciągu .... nie byłbyś w stanie użyć ogólnej metody, takiej jak GetValueFor <T>, ponieważ T musi być rozwiązany czas kompilacji .... oznacza to, że osoba dzwoniąca musi wiedzieć typ T w czasie kompilacji, który pokonałby cel dynamicznego wiązania tego typu. Oznacza to, że spadkobierca DynamicControllerBase musi znać typ TDTO .... Jedną rzeczą, którą możesz wypróbować, jest otrzymanie JSON w parametrze i każda implementacja DynamicControllerBase deserializuje ciąg do modelu i viceversa.
Jonathan Alfaro
@Darkonekt, jeśli spojrzysz na metodę „AddFilter”, masz wpisane ogólne parametry, które są przechowywane w zamknięciu podczas rejestracji funcs. To trochę mylące, ale zapewniam, że jest opłacalne i może działać
Johnny 5
Nie chcę podłączać się do JSON, ponieważ nie chcę zmieniać sposobu, aby webapi naturalnie rozpoznawało parametry
Johnny 5
Jeśli wyjaśniłbyś nieco więcej o przypadku użycia i prawdziwym scenariuszu, w którym taka funkcjonalność jest konieczna, bardzo by to pomogło. Prawdopodobnie istnieje jeszcze prostsze rozwiązanie ... kto wie. Jeśli chodzi o mnie, czasami lubię komplikować rzeczy… Mówię tylko…
Pan Blond
@ Mr.Blond Mam ogólną usługę odpoczynku, która zapewnia funkcje crud i get list. Czasami moje usługi muszą filtrować dane z listy pobierania, ale nie chcę pisać całej usługi ze wszystkich potrzebnych mi do zapewnienia filtru
Johnny 5

Odpowiedzi:

2

Zgadzam się z twoją myślą

usługi muszą filtrować dane z listy pobierania, ale nie chcę pisać całej usługi ze wszystkich potrzebnych mi do zapewnienia filtru

Po co pisać widget / filtr / punkt końcowy dla każdej możliwej kombinacji?

Wystarczy zapewnić podstawowe operacje, aby uzyskać wszystkie dane / właściwości. Następnie użyj GraphQL, aby zezwolić użytkownikowi końcowemu na filtrowanie ( modelowanie ) zgodnie z ich potrzebami.

Z 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
źródło
Zastanawiam się nad użyciem GraphQL, ale jestem zbyt przywiązany do mojej obecnej implementacji
John 5
2

Zrobiliśmy to, nasz kod odwołuje się do tej witryny: https://prideparrot.com/blog/archive/2012/6/gotchas_in_explicit_model_binding

W szczególności, patrząc na nasz kod, jaka jest sztuczka polegająca na zaakceptowaniu FormCollection w metodzie kontrolera, a następnie użyciu spoiwa modelu, modelu i danych formularza:

Przykład pochodzi z linku:

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

(Uwaga: strona prawdopodobnie nie działa, link do archive.org)

Brian mówi Przywróć Monikę
źródło
Dzięki za pomoc, obecnie nie używam MVC, np. (Może być więcej niż 1 parametr wejściowy) Muszę rozwiązać parametry według nazwy. Dodatkowo używam .Net-Core. Myślę, że to jest napisane dla starszej wersji .net. Proszę uzupełnić kod źródłowy: aby odpowiedź została zaakceptowana:this.Resolve<MyCustomType>("MyParamName");
Johnny 5
Ponieważ nie mam środowiska do minimalnego odtworzenia tego, nie będę w stanie tego zrobić - przepraszam za brak wymogu dotnetcore.
Brian mówi Przywróć Monikę
Mogę to przetłumaczyć na .Net-Core, to w porządku, jeśli pokażesz mi, jak rozwiązać według nazwy parametru, zaakceptuję
Johnny 5
To jest najbliższa odpowiedź, której chciałem, więc dam ci nagrodę
John 5
0

Skończyło się na pisaniu kontrolerów dynamicznych. Aby rozwiązać problem jako obejście.

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

Na razie ciężko koduję func w metodzie, ale jestem pewien, że możesz dowiedzieć się, jak go przekazać, jeśli potrzebujesz.

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
źródło