For anyone else wanting to to project non-db values from a db query, this project from u/Luis Aguilar was very, very helpful to me.
I had a very large legacy database (450GB) which was required to be served to OData/WebAPI.
The OData requirement meant I could not filter the source data (much) before returning it to the user. We could silo it, but apart from that it is their data to query as they wish.
More importantly, however, the legacy data was far too convoluted to expose as-is, and there was significant business logic required to collate the necessary data (Include
of navigation properties/foreign keys, lengthy clause predicates, etc).
This meant the pagination and result limiting would not be available until after the query was already materialized.
Normal shortcuts for this kind of thing involve various strategies that involve materialization/eager loading. However, due to the size of the dataset and lack of filtering, this would result in massive process memory bloat and out-of-memory crashes.
So, some code. Here's my config call, similar to what AutoMapper or OData require:
using ExpressionFramework.Projections;
using ExpressionFramework.Projections.Configuration;
public class ProjectionModelBuilder : ProjectionModel
{
protected override void OnModelCreating(ProjectionModelBuilder modelBuilder)
{
ClientDTO.ProjectionModel(modelBuilder);
OrderDTO.ProjectionModel(modelBuilder);
AnotherDTO.ProjectionModel(modelBuilder);
}
}
This design allows me to keep the projection rules in the DTO class with the rest of the business logic. Here's what the DTO-level code looks like:
public static void ProjectionModel(ProjectionModelBuilder modelBuilder)
{
modelBuilder
.Projection<ClientDTO>()
.ForSource<Client>(configuration =>
{
configuration.Property(dto => dto.Name).ExtractFrom(entity => entity.Name);
// etc
});
}
Where Client
is my Entity/EDM type, mapped to db table and a gazillion foreign keys.
To then get a translated/projected Queryable
, this is it:
IClientQueryService service = _ioc.Resolve<IClientQueryService>(); // Repository pattern
var q = service.GetClients(); // withManyNavigationIncludes
var r = q.Where<Item>(
i =>
i.Name != null
&& i.Name != ""
// lather rinse repeat, with many sub-objects navigated also
).AsQueryable();
var projectionModel = new ProjectionModelBuilder();
var s = projectionModel.Project<ClientDTO, Client>(r).AsQueryable();
Only the last two lines are relevant, but included the rest for context.
The last thing I had to do was set this.IsAutoConfigured = false;
in the constructor for ProjectionSourceTypeConfiguration.cs
in Luis' code; this allowed me to order my projection definitions manually so navigation properties inside parent classes would configure their projections successfully.
I can't thank https://stackoverflow.com/users/543712/luis-aguilar enough for his work. After writing my own LINQ Provider/ExpressionVisitor
with various generic method invocations, translations and treewalks to still have various problems, his project was a godsend.
If you do find to have to pipeline your own expression processing for performance or other reasons, I'd recommend these two answers to begin with.
typed
is going to beobject
or non-genericIQueryable
, and none of the other stuff will work. There's things you can do here, but pretty much all of them will be ugly as anything... there's no magic bullet here. As a side note, theCast
call would be to<t>
, not to<IQueryable<t>>
(if you'll forgive the awkward pseudo-syntax there) – Adrell