-
Notifications
You must be signed in to change notification settings - Fork 130
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
Support AutoMapper's ProjectTo in DataSourceLoader #367
Comments
No problem at all. Your repo :) |
Hello @statler I'm trying to sketch a possible design for the DTO mapping feature. I see the following key points:
The most generic implementation can be a pair of user-defined functions:
Does it look right? Do I miss any additional requirements? |
Hi Aleksey Can we assume that your implementation will use Automapper, or at least read its mappings from the Automapper config? I would really recommend that just slots in automapper as this will provide simple and complex mapping that would form the basis for your projections, and the great majority of anyone projecting to Dtos will already have automapper definitions as there is no other sane way to do this. I expect that if you were rolling your own, you would only be able to do simple property name replacement in the Dto, and that would defeat the purpose because Dtos regularly use complex mappings e.g. here is one of mine
Whether it is automapper or another system, you will need a config that deals with cases where the property names change. The automapper config would be ideal. The question becomes, does the query specify property names using the Order or the OrderDto. While the consumer might realistically expect that all of their query would be in terms of the OrderDto, this is not the solution. The projection is happening after the filter and sort because in many cases the filter needs to operate on properties not available in the OrderDto, so properties in the query may contain at least some properties relative to Order, and not necessarily the OrderDto. The problem then becomes that this negates the ability to sort on properties only contained in the OrderDto that are only available after the projection, including calculated properties such as OrderDto.SumValues. One option might be (though it would be a bit of a design change) to introduce a new set of operators which can assume the ProductDto properties, something like Change
To
This would have stacks of advantages, and increase the power of the library considerably. It would also deal with the question of when and where to use Order vs OrderDto properties. You could even make the library smart enough that if the property in the base Filter (for example) is not present in Order, but does exist in OrderDto, then it is applied in PostProjection.Filter. This would ensure compatibility with the widgets (even backwards compatibility), and make the projection issues invisible on the client side. E.g if a datagrid is showing a property OrderDto.SumValues, and the user filters on that column, the widget will send {"Filter",[["SumValues",">=", 10]]. If the library tests and determines that there is no property on Order called OrderValue, but there is a property on OrderDto, then it moves the property to PostProjection.Filter. Doing this with the ordering would fix issue #388 This would eliminate the need for any property substitutions, and everything could be done with automapper. No changes would be necessary for the existing widgets, as everything is done in the library. It should make implementation relatively simple too. Effectively instead of the existing library which effectively just does this (after all expression trees etc., and ignoring sorts and aggregations);
You are simply doing this
This would also allow clientside stores to compile complex queries and sorts either pre or post projection (or both) You could call the projection something like this;
where _mapper.ConfigurationProvider is the mapping configuration from automapper - IConfigurationProvider. I think this would be preferable to
The first option would make it easier to do a single pass and identify any properties that need to move from Filter or Sort into PostProjection.Filter and PostProjection.Sort |
Thanks @statler for your detailed reply. Now it's clear that member renaming is not sufficient.
I'm inclined to think that it would be better to integrate with Automapper in a separate library or a plugin. Your code snippets are a good illustration of how this can be done. However, I see that there's a need for built-in projection support, so that developers don't need to manually access/cast/iterate I don't think that we want to make the library smart enough to automatically handle various mapping options and edge cases. |
I figured the automatic handling of mapping would be out, but it seems to me that the inclusion of a post projection option would be really quite easy and would not break anything in the existing architecture. All that would be required would be; On the javascript side allow for the specification of additional options e.g.
In the C# library, simply running through the expression tree code a second time, just with the LoadOptions changed to only include the PostProjection Filter, Sort and Group. The easiest way to do this would probably be to create a generic overload of Load (Load). I would do a PR for it, but I can't get my head arount the grouping and where I can insert into the code so that I am always applying the projection to the IQueryable rather than the Group - Also, some of the Expression work is a bit different to how I work trees :( The mapping code is REALLY simple
|
It seems so. However, as you noticed, grouping is a tough subject. Also, interoperability with existing options (
PRs are welcome. If you do, please include unit tests. I think it's essential to test how well Automapper works with SQL translation. Refer to the recent ticket on this topic. |
Do you need to control projections from the client side? Isn't it sufficient to control mappings on the server? |
As per the example, the additional options are for specifying Post-projection filter, sort and group.
This solves several issues;
|
What are the data types of the |
Same as Filter, Sort and Group in the base level of the options. When it gets to the server, you would invoke EXACTLY the same expressioncompilers you do for a normal filter, sort , group - you just do it after the projection e.g.
|
I see the following arguments against such an API:
Consider a data grid sorted by two columns. The first column belongs to the original model, and the second column is a projection or a computed property. If I understand correctly, this will imply: {
sort: [ { selector: "PropOfModel" } ]
postProjection: {
sort: [ { selector: "PropOfDTO" } ]
}
} In case of two-pass |
Actually, filter can also be problematic. Example - grid's search panel generates [
["PropOfModel","contains","abc"],
"or",
["PropOfDTO","contains","abc"]
] |
I also have need for this implementation. While our development team managed to use ProjectTo to with the DataSourceLoader.Load() method to list data for the grid we can't filter nor order by many of the columns. |
Hello @Arafel-BR
Do you mean the specific 'post-projection' idea discussed above or support for
Do you have a code sample or a project that illustrates the issue? You can share it here or via Support Center. |
Hi Aleksey I have come around to the conclusion that you are correct on the post-projection filtering. Not only;
but also, it will cause paging problems - because the paging would happen before the post-projection and then the subsequent operation would only work on a subset of the data. That said, it would still be handy to be able to specify an automap projection for the original reasons. All this would need to do would be to apply the ProjectTo method to the end of the expression E.g.
It would be relatively easy to implement I think. While it introduces an additional dependency, the benefit is immense. |
Hi Aleksey Me again. Further to your comment in #378 I have looked into the CustomAccessorCompiler as an option. The CustomAccessorCompiler is awesome! It will actually resolve the trivial cases like we discuss in #378, but I am still at an impasse with more complex sort scenarios - and with the exact same problem for grouping. My specific example at the moment is - I have a field called Status in the Dto, that is calculated like this:
It is not even a direct result of the projection. With the filtering, this is simple to resolve with the CustomFilterCompilers.RegisterBinaryExpressionCompiler like this;
However, because the grouping is done before the projection, I can see no way that I can get the grouping to work. Nor can I see a way to use the CustomAccessorCompiler for the sort. Would it be at all possible just to either:
I see option 2 as being quite powerful. You could add an additional parameter that is an enum describing what operation the expression is being used in. Right now, any column that is displayed in a grid that is calculated through projection cannot be grouped, and not sorted - unless relatively trivial. |
CustomAccessorCompilers.Register((target, accessorText) => {
if(target.Type == typeof(Ncr) && accessorText == "Status") {
return Expression.Condition(
Expression.Equal(Expression.PropertyOrField(target, "CloseOutDate"), Expression.Constant(null)),
Expression.Constant("Closed Out"),
Expression.Condition(
Expression.Or(
Expression.NotEqual(Expression.PropertyOrField(target, "ApprovalDate"), Expression.Constant(null)),
Expression.GreaterThan(Expression.PropertyOrField(target, "ApprovalsCount"), Expression.Constant(0))
),
Expression.Constant("Approved"),
Expression.Constant("Open")
)
);
}
return null;
}); Resulting expressions:
However, I'm not sure whether LINQ providers will be able to translate these into SQL. |
OK, so that is officially a working answer. It does in fact successfully transpose the LINQ to SQL. Fantastic work - very much appreciated. The SQL is
|
I have ended up writing extension methods that are used as follows
When using this code you need to explicitly deal with any properties that may be present in the Dto but not the base object using CustomAccessorCompiler, but only if they may be filtered, grouped or sorted. If you do implement CustomAccessorCompiler on these properties, all issues with grouping, filtering and sorting just disappear and the AspNet.data projection Just Works. If you need to work with the objects post projection, refer #338 (comment) The code for these is below (includes some sync methods too, and overloads for expressions);
[Edited 23/1/2020 to fix grouping, skip and take] |
Aleksey Just extending on this, I have written code that automatically adds all of the Automapper mappings as CustomAccessors. This now gives us an end-to-end solution for managing projections seamlessly inside the aspnet library. At the moment though, you would have to do this using my extensions. Is there any way you would consider integrating this into the library? All that would be necessary is to add my extension methods (above post) for the LoadDto & LoadDtoAsync methods, and some derivative of the following code in the customaccessors to provide the automatic mapping of projections. Essentially what this does it provide access to every field that is explicitly mapped in automapper for use in grouping, sorting and filtering with no additional code. There is also a helper to make customaccessors much easier to write in those instance where an explicit accessor is necessary. You can create an accessor by adding it to the RegisterBasicAccessors like this;
The full code is
|
Your results are impressive! The When I take the most recent code snippet, the following members are missing:
I'd prefer a separate GitHub project with a separate NuGet package. By the analogy with these contrib-style projects. |
Thanks Aleksey
Example automapper code
|
Although I cannot promise that we'll arrange this code into a repository or a package, we at DevExpress appreciate your efforts. Your code stays safe in this ticket, our support engineers are aware of it, and they will direct users with similar inquiries here. |
As @Arafel-BR even I also need this kind of feature. Maybe as a plugin library if DevExpress doesn't want to make this beautiful library smarter. I am going to explain my problem:I'm trying to create a project following the "Clean Architecture" so I've four layers: Domain, Infrastructure, Application, User interface (currently Blazor) where: Infrastructure has a dependency on Domain and Application.
This architecture gives me the possibility to work inside the domain layer using domain entities, domain language and so on. The application layer has the goal of receiving command/query with parameters, translate the request to the domain languages and finally translate the result in a specific DTO for the request's result. Currently, if I use DevExtreme.AspNet.Data I have to take a decision on which roads I want to walk: The easiest road:break the Clean Architecture and give the User Interface a dependency on the Domain layer so I can use all the functionalities of DatasourceLoader like applying filtering/grouping/so on directly on the SQL query. The road that I would like:maintaining the Clean Architecture, so maintaining an abstract layer between the User interface and the domain layer. @AlekseyMartynov, @statler I would like to ask you if there is already a plugin library. Note: I am using AutoMapper too. |
Automapper allows you to keep these benefits:
class OrderDTO {
public int ID { get; set; }
public DateTime? Date { get; set; }
}
class AppImpl {
NorthwindContext _nwind;
public AppImpl(NorthwindContext nwind) {
_nwind = nwind;
}
IMapper _mapper = new MapperConfiguration(cfg => cfg
.CreateMap<Order, OrderDTO>()
.ForMember(vm => vm.ID, m => m.MapFrom(o => o.OrderId))
.ForMember(vm => vm.Date, m => m.MapFrom(o => o.OrderDate))
).CreateMapper();
public IQueryable<OrderDTO> GetDataForView123() {
return _nwind.Orders.ProjectTo<OrderDTO>(_mapper.ConfigurationProvider);
}
} var loadResult = DataSourceLoader.Load(app.GetDataForView123(), new DataSourceLoadOptions {
Filter = new[] { "Date", ">", "2011-11-11" },
Sort = new[] { new SortingInfo { Selector = "Date" } },
Take = 10
}); SQL:
|
@AlekseyMartynov this is a smart solution. I've tried it and it works! So the idea behind this code is:
right? |
Correct. For a more detailed description, check the relevant Automapper docs. |
I was so happy to have read this issue. I can't express how happy I am to see a work-around. Great work to @statler for the brains and grunt work, and thank you @AlekseyMartynov for providing direction where it was necessary and remaining active. I would like to ask if it's possible to have a complete DevExpress example created, illustrating this workaround in the same fashion as the other examples? |
This is now available out of the box with the fork at https://github.com/statler/DevExtreme.AspNet.Data |
As described in https://www.devexpress.com/Support/Center/Question/Details/T758528/modify-the-datasourceloader-to-support-projection-as-part-of-the-original-query-operation and referenced threads - repeated below for simplicity
In a nutshell, the issue is this;
It is best practice in EF to return a DTO rather than the original object. Regardless of best practice, efficiency demands in so in my application as I have tables with large text fields that are not necessary for populating lists and would increase the size of the payload over 100x. I get the data for my lists using DataSourceLoader GET controllers, and I use filtering, sorting and grouping in the DataSourceLoader extensively. I ProjectTo to ensure that my payload from SQL to API, and my payload from API to client are efficient and contain no more data that is necessary.
At the moment, it is impossible to perform operations on the full set of object properties, but return only a subset using ProjectTo. Any property specified in the options e.g. a filter occurs after the ProjectTo, so the property is not available for filtering at that point in the SQL. As per the ticket, you cannot simply operate on the data after it is returned, as it breaks other elements of the returned set for more complex operations like grouping.
Also, a Select is not the answer as this requires far too much hard coding to move between types - this is what automapper and ProjectTo are for.
At the moment I have created a workaround that;
This works, but it would be far better if the datasourceloader could be modified to append my projection so it occurs after the datasourceloader filtering / sorting / grouping. I can't see that this would require much modification.
My code below for anyone else with this issue.
The text was updated successfully, but these errors were encountered: