-1

I have 2 classes that represent an entity; a data transfer class and a domain class. I have a method that takes a linq expression for the domain class and I want to translate that into the same linq expression for the data transfer class.

class Strategy
{
  public string Name { get; set; }
}

class StrategyDto
{
  public string NameColumn { get; set; }
}

public async Task<Strategy> FirstAsync(Expression<Func<Strategy, bool>> clause)
{
  // this.source is an IQueryable<StrategyDto>.
  StrategyDto strategyDto = await this.source.FirstAsync(clause); // clause can't be used here cause it's based on the domain model, not StrategyDto. How do I translate it?
  Strategy strategyDomain = strategyDto.ToDomainObject();
  return strategyDomain.
}

Example call:

Strategy someStrategy = await queryset.FirstAsync(strat => strat.Name == "Some strategy");

How do I apply the clause based on the domain class to the list of DTOs? Note that the fields may slightly differ in name as sometimes there's some translation between the classes.

Ian Kirkpatrick
  • 1,861
  • 14
  • 33
  • You are asking for magic. You need a mapping somewhere. A mapping that must be explorable at runtime to convert the expressions from transfer class to domain class. Starting from this mapping _perhaps_ (and I'll repeat _perhaps_) it is possibile to do something... But probably it would be complex and full of limitations. In general you would use expression trees and create a converter of expression trees between the two "formats" of objects. This converter would then use the mapping. – xanatos Dec 15 '20 at 21:50

1 Answers1

1

EDIT

You could use AutoMapper. In your app startup, you need to register a mapping configuration between StrategyDto and Strategy

var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<StrategyDto, Strategy>()
        .ForMember(strategy => strategy.Name, options => options.MapFrom(dto => dto.NameColumn));
});

Then at runtime, you can use AutoMapper to project the source to the expected type

public async Task<Strategy> FirstAsync(Expression<Func<Strategy, bool>> clause)
{
    return await this.source
        .ProjectTo<Strategy>(config)
        .FirstAsync(clause);
}

Original answer:

If you're only interested in supporting a basic 'equals' operator, you could try generating the predicate dynamically:

// The type could be passed in as a parameter or via generics
var dbType = typeof(StrategyDto);
// The property name could be found through reflection
var dbProperty = "NameColumn";

var parameter = Expression.Parameter(dbType, "x");
var property = Expression.Property(parameter, dbProperty);
var value = Expression.Constant("Some strategy");
var equals = Expression.Equal(property, value);
var lambda = Expression.Lambda(equals, parameter);

If you want to take any predicate and be able to translate it, then that gets more complicated. You need to implement ExpressionVisitor and replace parameter expressions with the new type, and replace property expressions with the correct property name. You can find an example of parameter replacement here

Andrew Williamson
  • 8,299
  • 3
  • 34
  • 62
  • If you're just going to hard code it you might as well just write `Expression> e = strat => strat.NameColumn== "Some strategy"`. Using code like this doesn't do anything useful. – Servy Dec 15 '20 at 20:45
  • Maybe I wasn't clear enough - by generating an expression dynamically, you can at least vary the type and the property name – Andrew Williamson Dec 15 '20 at 20:46
  • But you don't know the type or the property name to use. – Servy Dec 15 '20 at 20:48
  • See the updates. It's not hard to create a custom attribute, or store the mapping metadata in a dictionary, so that this expression can be generated at runtime – Andrew Williamson Dec 15 '20 at 20:50
  • But they don't have that in their code. – Servy Dec 15 '20 at 20:59
  • If the asker wants an explanation of how to use reflection, I feel like that's a separate question. I've added an example of how to achieve this with AutoMapper instead – Andrew Williamson Dec 15 '20 at 22:08
  • I like the automapper idea. I ended up doing something else in the meantime so I'll have to try that out when I get time to make sure it works. I kind of figured I'd have to do some mapping somewhere. It gets more complicated when my columns have different types (ex. List objects in domain models are JSON strings in Dto) which idk if automapper supports so it may just be way too complicated to be worth trying. But I do like the idea – Ian Kirkpatrick Dec 16 '20 at 14:32
  • Have a look at Entity Framework Core's [ValueConverter](https://learn.microsoft.com/en-us/ef/core/modeling/value-conversions). If the only difference between your DTO and your database models is column names and value types, you could possibly achieve all this directly through Entity Framework – Andrew Williamson Dec 16 '20 at 18:37