Associação tardia Resolver dinamicamente os modelos depois de entrar no controlador

9

Estou procurando uma maneira de resolver um modelo depois de entrar em uma ação em um controlador, a maneira mais simples de descrever o problema seria:

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

Se você estiver procurando mais informações sobre o motivo pelo qual estou tentando fazer isso, continue lendo para obter uma imagem completa.

TL; DR

Estou procurando uma maneira de resolver uma solicitação de modelo, dado um nome de parâmetro que sempre será resolvido a partir da string de consulta. Como posso registrar dinamicamente filtros na inicialização? Eu tenho uma classe que vai lidar com o registro dos meus filtros.

Na minha classe de inicialização, desejo registrar filtros dinamicamente nos meus restServices. Eu tenho as opções que estou usando para passar para o ControllerFeatureProvider personalizado, que é mais ou menos assim:

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

O My Controller controlará as opções e as usará para fornecer filtros para pontos de extremidade de paginação e 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>();
    }
}

Estou tendo problemas para descobrir como resolver dinamicamente um modelo, dado o HttpContext, acho que faria algo assim para obter o modelo, mas esse é um pseudo-código que não 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);
}

Depois de pesquisar na fonte, vi algumas coisas promissoras ModelBinderFactory e ControllerActionInvoker Essas classes são usadas no pipeline para ligação de modelo,

Eu esperaria que exponha uma interface simples para resolver um nome de parâmetro do QueryString, algo como isto:

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

No entanto, a única maneira que vejo para resolver um modelo a partir do fichário é criar descritores falsos de controladores e zombar de várias coisas.

Como posso aceitar parâmetros de ligação atrasada no meu contoller?

johnny 5
fonte
2
Não vejo utilidade para isso. Além disso, mesmo que você possa vincular o modelo com base em um parâmetro de seqüência de caracteres .... você não seria capaz de usar um método genérico como GetValueFor <T> porque T deve ser resolvido em um tempo de compilação .... isso significa que o chamador deve saber o tipo de T em tempo de compilação, o que anularia o objetivo de vincular dinamicamente o tipo. Isso significa que o herdeiro de DynamicControllerBase deve conhecer o tipo de TDTO .... Uma coisa que você pode tentar é receber JSON no parâmetro e cada implementação de DynamicControllerBase desserializa a string para um modelo e vice-versa.
Jonathan Alfaro
@Darkonekt Se você olhar para o método 'AddFilter', terá os parâmetros genéricos digitados que são armazenados em um fechamento quando você registra as funções. É um pouco confuso, mas eu asseguro-lhe o seu trabalho viável e pode
johnny 5
Eu não quero para ligar a json, porque eu não quero ter que mudar a maneira de WebAPI naturalmente parâmetros resolver
johnny 5
Se você explicasse um pouco mais sobre o caso de uso e o cenário da vida real em que esse tipo de funcionalidade é necessária, isso ajudaria bastante. Provavelmente existe uma solução mais simples disponível .. quem sabe. Quanto a mim, às vezes eu gosto de complicar as coisas .. Só estou dizendo ..
Sr. Blond
@ Mr.Blond Eu tenho um serviço de descanso genérico que fornece funcionalidade de lista bruta e de obtenção. Às vezes meus serviços precisam filtrar dados de lista get, mas eu não quero ter que escrever um serviço completo de toda a necessidade I para proporcionar um filtro
johnny 5

Respostas:

2

Concordo com seu pensamento

os serviços precisam filtrar dados da lista get, mas não quero escrever um serviço inteiro de tudo o que preciso para fornecer um filtro

Por que escrever um widget / filtro / terminal para todas as combinações possíveis?

Simplesmente forneça operações básicas para obter todos os dados / propriedades. Em seguida, use o GraphQL para permitir que o usuário final o filtre ( modele ) de acordo com suas necessidades.

Do 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
fonte
Eu estive pensando em usar GraphQL mas estou muito amarrado em minha implementação atual
johnny 5
2

Fizemos isso, nosso código faz referência a este site: https://prideparrot.com/blog/archive/2012/6/gotchas_in_explicit_model_binding

Especificamente, olhando para o nosso código, o que faz o truque é aceitar um FormCollection no método do controlador e, em seguida, usar os dados do fichário, do modelo e do formulário:

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

(Nota: o site parece estar inativo, o link é para archive.org)

Brian diz Restabelecer Monica
fonte
Obrigado pela sua ajuda. Atualmente, não estou usando o MVC, por exemplo (pode haver mais de um parâmetro de entrada). Preciso resolver os parâmetros pelo nome. Além disso, estou usando o .Net-Core. Acho que isso foi escrito para a versão mais antiga .net. Por favor, complete o método stub: para que a resposta seja aceita:this.Resolve<MyCustomType>("MyParamName");
johnny 5 /
Como não tenho um ambiente para reproduzir isso minimamente, não poderei fazer isso - peço desculpas por ter perdido o requisito do dotnetcore.
Brian diz Reinstate Monica
Eu posso traduzi-lo para .Net-Core Isso é bom, Se você me mostrar como resolver pelo nome do parâmetro eu vou aceitar
johnny 5
Esta é a coisa mais próxima a resposta que eu queria, então eu vou dar-lhe a recompensa
johnny 5
0

Acabei escrevendo controladores dinâmicos. Para resolver o problema como uma solução 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;
}

Estou codificando a função no método por enquanto, mas tenho certeza de que você pode descobrir como transmiti-la, se precisar.

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
fonte