Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Splitting ISieveCustomFilterMethods into multiple files #104

Open
wobbince opened this issue Dec 17, 2020 · 12 comments
Open

Splitting ISieveCustomFilterMethods into multiple files #104

wobbince opened this issue Dec 17, 2020 · 12 comments

Comments

@wobbince
Copy link

Hi,

Is it possible to split the ISieveCustomFilterMethods implementation into multiple small, manageable files?

I.e. one per entity?

Thanks.

@hasanmanzak
Copy link
Collaborator

Maybe partial classes?

The dependency injection is tied to the interface. So you can not define and declare to inject multiple concrete classes for the ISieveCustomSortMethods and ISieveCustomFilterMethods interfaces.

I don't like partial classes, so I come around to that behaviour with this:

  • wrapping the SieveProcessor with own ISpfProcessor<TEntity> and a concrete SpfProcessor<TEntity> class.
  • introducing a custom method configuration
  • introducing builder to that configuration
  • templating a CustomSpfMethodsOf{something} class with ISieve.. interfaces be implemented to a text
  • templating the method signiture to a text
  • entity registration builder
  • runtime compilation for every registered entity to have its own SieveProcessor with that CustomSpfMethods class with methods injected which was defined by the method configuration builder
  • register a singleton ISpfProcessorFactory to DI
  • When needed, get the factory, get the processor of entity from factory, apply spf to IQueryable instace of that entity using retrieved processor.

Kind of an OVERKILL approach. But.. I think it's very powerful and very flexible when declaring custom methods or registering entities to able used with spf, for some I may not want to use by default.

Therefore I can use something like this:

// service registration
services.AddSingleton<ISpfProcessorFactory>
(
  provider =>
  {
    var factory = new SpfProcessorFactory()
    .UseSieveSpfProcessor()
    .UseSpfFor<IUser>()
    ...
    ...
    .UseSpfFor<IProject>()
    ;

    factory.BuildProcessors();

    return factory;
  }
);

// simple IQueryable extension
public static class AccessExtensions
{
    public static ISpfResult<TEntity> ApplySpf<TEntity>(this IQueryable<TEntity> queryable, ISpfProcessorFactory spfProcessorFactory, Maybe<ISpfData> maybeSpfData)
        where TEntity : class, IEntity
    {
        var spfProcessor = spfProcessorFactory.GetProcessor<TEntity>();

        var processResult = spfProcessor.Process(queryable, maybeSpfData);

        return processResult;
    }
}

// in an IQueryable operator, say a repository or an ORM access level
public override Result<ISpfResult<TEntity>> QueryByFilter<TEntity>(Maybe<ISpfData> maybeSpfData, Expression<Func<TEntity, bool>> filter = null)
{
    var query = ExposeQueryable(filter);

    var processResult = query.ApplySpf(SpfProcessorFactory, maybeSpfData);

    return Result.Ok(processResult);
}

@wobbince
Copy link
Author

Hi @hasanmanzak,

Very useful information.

Do you have a sample of this that you could share?

Thanks.

@kdunham
Copy link

kdunham commented Apr 2, 2021

@hasanmanzak your approach may be valuable to some but for simplicity sake it would be nice to be able to do as @wobbince suggested and it seems to me it should be simple to achieve. .Net DI allows for adding multiple instances of the same interface (using IServiceCollection.AddScoped instead of IServiceCollection.TryAddScoped) which can be injected via ICollection<T> where T in this case could be the ISieveCustomFilterMethods interface. Is it unreasonable for the sieve processor to then loop through each filter method class provided in this way to then know which methods have been defined?

@ziadadeela
Copy link

@hasanmanzak Is there any new update about supporting this in any coming release?

@brianebeling
Copy link

We used Vertical Slicing in our Architecture and therefore put the ISieveCustomFilterMethods in the same place as our endpoint.
We were expecting a similar behaviour as with AutoMapper, where it wouldn't matter how many instances there are.

Is there an update on this?

@boris612
Copy link

I also had the need to split custom sieve filters into different files (even different projects). I solved the problem by creating a composite sieve filter by creating IL code to copy methods from existing filters into a new filter.

public class CompositeSieveCustomFilterBuilder 
{
  private static Type? type = null;
  public static Type Build(params Type[] types)
  {
    if (type != null) return type;

    AssemblyName assemblyName = new AssemblyName("DynamicAssembly");
    AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
    ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicModule");
    TypeBuilder typeBuilder = moduleBuilder.DefineType("DynamicCompositeSieveCustomFilter", TypeAttributes.Public);
    typeBuilder.AddInterfaceImplementation(typeof(ISieveCustomFilterMethods));

    foreach (Type t in types)
    {
      if (t.GetInterface(nameof(ISieveCustomFilterMethods)) == null)
        throw new Exception($"{t.Name} does not implement ISieveCustomFilterMethods");

      foreach (MethodInfo method in t.GetMethods(BindingFlags.Instance | BindingFlags.Public))
      {
        //method signature should be
        //IQueryable<T> Name(IQueryable<T> source, string op, string[] values)
        var parameters = method.GetParameters();
        if (parameters.Length == 3 
          && parameters[0].ParameterType == method.ReturnType
          && parameters[1].ParameterType == typeof(string) 
          && parameters[2].ParameterType == typeof(string[])) 
        {
          MethodBuilder methodBuilder = typeBuilder.DefineMethod(method.Name, method.Attributes, method.ReturnType, method.GetParameters().Select(p => p.ParameterType).ToArray());
          ILGenerator il = methodBuilder.GetILGenerator();
          il.Emit(OpCodes.Ldarg_0);
          il.Emit(OpCodes.Ldarg_1);
          il.Emit(OpCodes.Ldarg_2);
          il.Emit(OpCodes.Ldarg_3);
          il.Emit(OpCodes.Callvirt, method);
          il.Emit(OpCodes.Ret);
        }
      }
    }

    type = typeBuilder.CreateType();
    if (type == null)
    {
      throw new Exception("DynamicCompositeSieveCustomFilter cannot be created");
    }
    return type;
  }
}

Then, in the Program.cs, dependency is registrered as

builder.Services.AddScoped<ISieveCustomFilterMethods>(factory =>
      {
        Type compositeSieve = CompositeSieveCustomFilterBuilder.Build(typeof(FirstCustomFilters), typeof(SecondCustomFilters));
        object? instance = Activator.CreateInstance(compositeSieve);
        if (instance == null)
          throw new Exception("Activator.CreateInstance(compositeSieve) returned null!");
        return (ISieveCustomFilterMethods) instance;
      });

@kdunham
Copy link

kdunham commented Dec 30, 2022

@boris612 I like it! Shouldn't really be necessary but it is a viable workaround.

@saifulaiub123
Copy link

saifulaiub123 commented Apr 4, 2023

Really a good solution for making it workable. I've modified the below code to pass the types dynamically

builder.services.AddScoped<ISieveCustomFilterMethods>(factory =>
            {
                var type = typeof(ISieveCustomFilterMethods);
                var types = AppDomain.CurrentDomain.GetAssemblies()
                    .SelectMany(x => x.GetTypes())
                    .Where(x => type.IsAssignableFrom(x) 
                    ).ToArray();
                Type compositeSieve = CompositeSieveCustomFilterBuilder.Build(types);
                object? instance = Activator.CreateInstance(compositeSieve);
                if (instance == null)
                    throw new Exception("Activator.CreateInstance(compositeSieve) returned null!");
                return (ISieveCustomFilterMethods)instance;
            });

@saifulaiub123
Copy link

I also had the need to split custom sieve filters into different files (even different projects). I solved the problem by creating a composite sieve filter by creating IL code to copy methods from existing filters into a new filter.

public class CompositeSieveCustomFilterBuilder 
{
  private static Type? type = null;
  public static Type Build(params Type[] types)
  {
    if (type != null) return type;

    AssemblyName assemblyName = new AssemblyName("DynamicAssembly");
    AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
    ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicModule");
    TypeBuilder typeBuilder = moduleBuilder.DefineType("DynamicCompositeSieveCustomFilter", TypeAttributes.Public);
    typeBuilder.AddInterfaceImplementation(typeof(ISieveCustomFilterMethods));

    foreach (Type t in types)
    {
      if (t.GetInterface(nameof(ISieveCustomFilterMethods)) == null)
        throw new Exception($"{t.Name} does not implement ISieveCustomFilterMethods");

      foreach (MethodInfo method in t.GetMethods(BindingFlags.Instance | BindingFlags.Public))
      {
        //method signature should be
        //IQueryable<T> Name(IQueryable<T> source, string op, string[] values)
        var parameters = method.GetParameters();
        if (parameters.Length == 3 
          && parameters[0].ParameterType == method.ReturnType
          && parameters[1].ParameterType == typeof(string) 
          && parameters[2].ParameterType == typeof(string[])) 
        {
          MethodBuilder methodBuilder = typeBuilder.DefineMethod(method.Name, method.Attributes, method.ReturnType, method.GetParameters().Select(p => p.ParameterType).ToArray());
          ILGenerator il = methodBuilder.GetILGenerator();
          il.Emit(OpCodes.Ldarg_0);
          il.Emit(OpCodes.Ldarg_1);
          il.Emit(OpCodes.Ldarg_2);
          il.Emit(OpCodes.Ldarg_3);
          il.Emit(OpCodes.Callvirt, method);
          il.Emit(OpCodes.Ret);
        }
      }
    }

    type = typeBuilder.CreateType();
    if (type == null)
    {
      throw new Exception("DynamicCompositeSieveCustomFilter cannot be created");
    }
    return type;
  }
}

Then, in the Program.cs, dependency is registrered as

builder.Services.AddScoped<ISieveCustomFilterMethods>(factory =>
      {
        Type compositeSieve = CompositeSieveCustomFilterBuilder.Build(typeof(FirstCustomFilters), typeof(SecondCustomFilters));
        object? instance = Activator.CreateInstance(compositeSieve);
        if (instance == null)
          throw new Exception("Activator.CreateInstance(compositeSieve) returned null!");
        return (ISieveCustomFilterMethods) instance;
      });

This is really a good solution. But if the custom filter class has a constructor where we need to inject any service then how we can modify your code to achieve this?

public class CustomFilter : ISieveCustomFilterMethods
{
private readonly ISomeService _someService;
public CustomFilter(ISomeService someService)
{
_someService = someService //How can I resolve this service. It's always null here
}
public IQueryable filter(IQueryable, string op, string[] values)
{
var data = _someService.SomeMethod();/// How can I get value from this method?
}
}

@boris612
Copy link

boris612 commented Apr 9, 2023

But if the custom filter class has a constructor where we need to inject any service then how we can modify your code to achieve this?

public class CustomFilter : ISieveCustomFilterMethods { 
 private readonly ISomeService _someService; 
 public CustomFilter(ISomeService someService) { 
      _someService = someService //How can I resolve this service. It's always null here 
 } 
 public IQueryable filter(IQueryable, string op, string[] values) { 
      var data = _someService.SomeMethod(); // How can I get value from this method? 
 } 
}

This cannot be done, because that constructor is never invoked. If you add a breakpoint in your filter method and try to see what is this.GetType(), you'll notice that this is not of type CustomFilter, but DynamicCompositeSieveCustomFilter which has filter method which we have dynamically created.

There is a dirty workaround to achieve what you want, by generating IL code for the constructor of DynamicCompositeSieveCustomFilter which takes IServiceProvider as an argument.

 typeBuilder.AddInterfaceImplementation(typeof(ISieveCustomFilterMethods)); //we had this line already, and the next block is a new one

 FieldBuilder fieldBuilder = typeBuilder.DefineField("serviceProvider", typeof(IServiceProvider), FieldAttributes.Private);
 var constructorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.HasThis, new[] { typeof(IServiceProvider) });
 ILGenerator ctorIL = constructorBuilder.GetILGenerator();
 LocalBuilder localBuilder = ctorIL.DeclareLocal(typeof(IServiceProvider));  
 ctorIL.Emit(OpCodes.Ldarg_1);
 ctorIL.Emit(OpCodes.Stloc, localBuilder);
 ctorIL.Emit(OpCodes.Ldarg_0);
 ctorIL.Emit(OpCodes.Ldloc, localBuilder);
 ctorIL.Emit(OpCodes.Stfld, fieldBuilder);
 ctorIL.Emit(OpCodes.Ret);

  ... //existing code

Then the previous code

builder.Services.AddScoped<ISieveCustomFilterMethods>(factory =>
      {
        Type compositeSieve = CompositeSieveCustomFilterBuilder.Build(typeof(FirstCustomFilters), typeof(SecondCustomFilters));
        object? instance = Activator.CreateInstance(compositeSieve);
        if (instance == null)
          throw new Exception("Activator.CreateInstance(compositeSieve) returned null!");
        return (ISieveCustomFilterMethods) instance;
      });

has to be changed into

builder.Services.AddScoped<ISieveCustomFilterMethods>(factory =>
      {
        Type compositeSieve = CompositeSieveCustomFilterBuilder.Build(typeof(FirstCustomFilters), typeof(SecondCustomFilters));
        object? instance = Activator.CreateInstance(compositeSieve, factory); //we have add an IServiceProvider instance
        if (instance == null)
          throw new Exception("Activator.CreateInstance(compositeSieve) returned null!");
        return (ISieveCustomFilterMethods) instance;
      });

Then you can change your code to something like this

public class CustomFilter : ISieveCustomFilterMethods { 
 private IServiceProvider serviceProvider;
 
 public IQueryable filter(IQueryable, string op, string[] values) { 
      ISomeService someService = this.serviceProvider.GetService<ISomeService>();
    ...
 } 
}

@saifulaiub123
Copy link

@boris612 Thanks for your reply. This is a great solution and I am able to solve my problem.

@anthonylevine
Copy link

A little late to this, but if you're using this Sieve within an endpoint, you can always create a factory to resolve the filter methods. As simple as:

services.AddScoped<ISieveCustomFilterMethods>(provider => 
{
    var currentContext = provider.GetRequiredService<IHttpContextAccessor>().HttpContext;
    var currentPath = currentContext.Request.Path.ToString();
    if(currentPath.Contains("/user/search")) {
        return new UserSieveFilterService();
    }
   ....
   return new DefaultSieveFilterService();
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants