Generics & static classes. Implement query helper

2019-08-21 11:23发布

问题:

Currently we implement a mapping service like this (the service uses automapper, and we make use of the projection feature on it for this part)

// Injected
// IGenericRepository<Entity> entityRepo

 var query = this.entityRepo
                 .FindAll(a => a.Id == someId)
                 .Take(1);

 var result = this.mappingService
                  .Map<Entity, EntityDto>(query)
                  .FirstOrDefault();

I'd like to create an extension that would allow me to do the following

var result = this.entityRepo
                 .FindAll(a => a.Id == someId)
                 .Take(1).Map<EntityDto>()   <--- Entity inferred from repo type
                 .FirstOrDefault();

My current attempt:

 public static class IQueryableExtensions
 {
     private static IMappingService mappingService;

     // will need to be called in app initialization
     public static void InitialiseMapper(IMappingService service)
     {
         mappingService = service;
     }

     public static IEnumerable<TDto> Map<TAttribute, TDto>(this IQueryable<TAttribute> value)
            where TDto : class
            where TAttribute : IEntity
     {
        return mappingService.Map<TAttribute, TDto>(value);
     }
 }

Thus currently my implementation would look like this.

var result = this.entityRepo
                     .FindAll(a => a.Id == someId)
                     .Take(1).Map<Entity,EntityDto>()
                     .FirstOrDefault();

Questions:

1) How would i go about inferring the entity type from the IQueryable object

2) I realize i cant create a constructor that takes parameters, when creating a static class. Is the way i init the mapper the best/only way?

回答1:

I tried that with reflection. The constraints are only for demo. If you want to call the reflection code multiple times be sure to cache the final methodinfo.

void Main()
{
    var a = new Entity[] {new Entity { name = "a"},new Entity { name = "b"}};

    Console.WriteLine(a.Take(1).Map<EntityDto>());
}

public class Entity
{
    public string name;
}

public class EntityDto
{
    public string dtoname;

}

public static class EntityExtensions
{
    public static IEnumerable<U> Map<T,U>(this IEnumerable<T> e) where T: Entity where U: EntityDto, new()
    {
        foreach(var a in e)
        {
            yield return new U() { dtoname = a.name };
        }
    }


    public static IEnumerable<U> Map<U>(this IEnumerable<object> e)
    {
        var method = typeof(EntityExtensions).GetMethods(BindingFlags.Static | BindingFlags.Public)     
        .Where(m => m.Name == "Map" && m.GetGenericArguments().Length == 2)
        .Single();
        method = method.MakeGenericMethod(e.GetType().GetGenericArguments()[0], typeof(U));

        return method.Invoke(null, new object[] { e}) as IEnumerable<U>;
    }
}


回答2:

1) Currently, you simply can't do that in C#. The type inference is not good enough. You can either specify all type parameters or none of them.

Edit: If you really want the version with a single parameter, you have to delete the second type parameter, type the parameter as non-generic IQueryable and deal with it. One way of doing that would be to determine the generic IQueryable<T> type at runtime. However, this requires reflection. In the case of IQueryable, you can also use the query provider to get around the reflection.

2) You can use a static type constructor.

public static class MyExtensions {
    static MyExtensions() {
        //initialization goes here
    }
}

This type constructor is even called thread-safe. However, if you manage to throw an exception here, you cannot access the MyExtensionsclass!